Skip to content
Mobile

Integrating Adapty with .NET: Server-Side Subscription Infrastructure Done Right

Adapty handles paywalls and receipt validation; your ASP.NET Core backend owns the truth. Idempotent webhook handlers, the server API for profiles and promo grants, and access levels as roles.

5 min read Updated Sep 3, 2026
Integrating Adapty with .NET: Server-Side Subscription Infrastructure Done Right

Subscription apps live or die by infrastructure nobody sees: receipt validation, renewal webhooks, paywall experiments, churn math. Adapty packages that layer — mobile SDKs, remote paywalls, A/B testing and revenue analytics — so your app ships monetization in days instead of months. But the mobile SDK is only half the story. The moment you have a .NET backend (and every serious product does), you need it to know who's subscribed, react to renewals and refunds, and grant server-side entitlements. This guide covers exactly that: integrating Adapty with a .NET backend via its server-side API and webhooks.

Flow: mobile SDK reports to Adapty cloud, webhooks notify the .NET backend, which owns entitlements

Key takeaways

  • Division of labour: the mobile SDK handles purchases and paywalls; Adapty's cloud validates with Apple/Google and tracks state; your .NET backend consumes that state and owns authorization.
  • Two integration surfaces matter server-side: the server API (query/grant access) and webhooks (state changes pushed to you).
  • Key the whole system on a stable customer_user_id — your user id, set in the SDK at login.
  • Webhooks are at-least-once: build idempotent handlers or enjoy duplicate Slack pings about the same renewal.

Architecture: who owns what

ConcernOwner
Purchase UI, StoreKit/Billing callsAdapty mobile SDK (Flutter/iOS/Android/React Native)
Receipt validation, renewal tracking, grace periodsAdapty cloud
Paywall configuration & A/B testsAdapty dashboard (remote config — no app release)
“Can this user call premium endpoints?”Your .NET backend, fed by Adapty
Cross-platform entitlement (web login, API keys)Your backend, keyed on customer_user_id

The prime directive: in the mobile SDK, call Adapty.identify(yourUserId) at login so Adapty's profile and your user row share a key. Everything server-side hinges on that join.

Consuming webhooks in ASP.NET Core

Adapty pushes subscription lifecycle events — trials started, renewals, cancellations, billing issues, refunds — to your endpoint. A production-shaped receiver:

[ApiController]
[Route("webhooks/adapty")]
public sealed class AdaptyWebhookController(
    IEntitlementService entitlements,
    IProcessedEventStore processed) : ControllerBase
{
    [HttpPost]
    public async Task<IActionResult> Handle()
    {
        var body = await new StreamReader(Request.Body).ReadToEndAsync();

        // 1) Authenticate the webhook (shared secret / signature per your Adapty config)
        if (!WebhookAuth.Verify(Request.Headers, body, _secret))
            return Unauthorized();

        var evt = JsonSerializer.Deserialize<AdaptyEvent>(body)!;

        // 2) Idempotency — Adapty delivers at-least-once
        if (!await processed.TryMarkAsync(evt.EventId))
            return Ok();

        // 3) React by event type
        var userId = evt.CustomerUserId;           // == your user id, thanks to identify()
        switch (evt.EventType)
        {
            case "subscription_started":
            case "subscription_renewed":
            case "trial_started":
                await entitlements.GrantAsync(userId, evt.AccessLevelId, evt.ExpiresAt);
                break;

            case "subscription_expired":
            case "subscription_refunded":
            case "access_level_revoked":
                await entitlements.RevokeAsync(userId, evt.AccessLevelId);
                break;

            case "billing_issue_detected":
                await entitlements.MarkGraceAsync(userId, evt.AccessLevelId);
                await notifications.SendFixPaymentNudgeAsync(userId);
                break;
        }

        return Ok();   // 2xx fast; do heavy work on a queue
    }
}

Notes that save on-call nights:

  • Return 2xx quickly and defer heavy work to a background queue — webhook senders retry slow endpoints, multiplying your load.
  • Store raw events (an adapty_events table) before processing. When analytics and entitlements disagree, the raw log arbitrates.
  • Grace periods are a state, not an expiry — keep access during billing_issue and nudge the user; most cards get fixed.

Querying and granting via the server API

Webhooks push; sometimes you need to pull — e.g. on login, or to grant promotional access from your admin panel:

public sealed class AdaptyClient(HttpClient http)
{
    // GET current profile incl. access levels
    public async Task<AdaptyProfile> GetProfileAsync(string customerUserId)
    {
        using var req = new HttpRequestMessage(HttpMethod.Get,
            $"https://api.adapty.io/api/v2/server-side-api/profile/");
        req.Headers.TryAddWithoutValidation("Authorization", $"Api-Key {secretKey}");
        req.Headers.Add("adapty-customer-user-id", customerUserId);

        var res = await http.SendAsync(req);
        res.EnsureSuccessStatusCode();
        return (await res.Content.ReadFromJsonAsync<AdaptyProfile>())!;
    }

    // Grant promotional access (support gestures, B2B deals, win-back offers)
    public Task GrantAccessAsync(string customerUserId, string accessLevel, DateTimeOffset until) =>
        http.PostAsJsonAsync(".../purchase/set/access-level/", new {
            access_level_id = accessLevel,
            expires_at = until,
        });
}

With that client, cross-platform entitlement falls out naturally: a user who subscribed on iPhone logs into your web app; your backend asks Adapty for the profile by customer_user_id and unlocks the same features. No receipts touch your code.

Access levels: design them like roles

Adapty's access levels decouple products from features: premium_monthly and premium_yearly both grant premium. Your backend should authorize on the access level, never the product id — pricing experiments then never touch authorization code. One enum, one claim, one middleware:

app.MapGet("/api/reports/advanced", (ClaimsPrincipal user) => ...)
   .RequireEntitlement("premium");   // reads the entitlement store fed by webhooks

FAQ

Why not talk to Apple/Google server APIs directly?

You can — I wrote up the raw approach in the Flutter IAP guide. You'll re-implement receipt validation, two webhook dialects, grace-period logic and analytics. Adapty is that plumbing plus paywall A/B testing; for subscription-first products the build-vs-buy math is short.

Is the mobile app the source of truth for premium?

No — the app is a cache. Truth lives in your entitlement store, updated by webhooks and reconciled via the server API. Server endpoints must check server state.

What about Flutter specifically?

Adapty ships a first-class Flutter SDK; the backend pattern here is identical regardless of app framework, which is exactly the point of the separation.

Next steps

Subscription infrastructure sits at the profitable intersection of billing correctness and user trust. Wiring Adapty into a .NET (or Laravel) backend, or migrating off hand-rolled receipt validation? Brief me on the project — or take the trainings route and level your team up in-house.

Keep reading

Related articles