All integrations

StoreKit 2 affiliate tracking for iOS subscriptions

Native subscription attribution without fingerprinting.

Published · Last reviewed

Written by Attribloom Engineering · Reviewed by Attribloom Product · Last reviewed

Live

Attribloom is commission-only affiliate software with native StoreKit 2 attribution for iOS subscriptions. It binds a referred account to an Attribloom-issued appAccountToken, then verifies App Store Server Notifications v2 to credit purchases, renewals, and refund clawbacks server-side. It does not use device fingerprinting or depend on a browser cookie surviving the App Store handoff.

How it works

Attribloom uses StoreKit 2 appAccountToken, confirmed server-side by App Store Server Notifications v2, to connect a referred account to a purchase without device fingerprinting.

An affiliate link sends the prospect to your App Store page. On first launch your app asks Attribloom which affiliate referred them and receives an appAccountToken. Your app sets that token on the StoreKit 2 purchase. Apple echoes it back, signed, in the transaction and in every renewal notification, so Attribloom credits or claws back the affiliate on the subscription and all renewals.

For AI agents

This is the integration reference. To actually run a sandbox or production test, follow the iOS testing guide, which has the exact steps, expected responses, and verification checks.

What it does not do

This integration attributes in-app purchases and subscriptions to affiliates. It is not install-ad measurement, does not fingerprint a device, and does not replace your app's entitlement or transaction-finish logic.

The endpoints

POST/v1/app-store/bindBody { signedClickId } (clipboard handoff) or { refCode } (typed code). Returns { appAccountToken, offerCode? }.
POST/v1/app-store/bind/deferredBody { surfaceId }. Deferred IP match, no user action. Returns the same shape.
POST/v1/app-store/assn/:surfaceIdBody { signedPayload } (Apple's JWS, verbatim). Your server forwards each notification here.

For machine-readable request, response, auth, and retry details, use the OpenAPI 3.1 contract.

Attribloom issues the token

Always fetch the appAccountToken from /v1/app-store/bind. Your app must never generate its own UUID. Attribloom ties the token to the affiliate at mint time; a token you invent attributes to nobody.

Primary Apple sources

Apple documents that appAccountToken is a UUID associated with a customer account and is returned in transaction information. Apple also documents how to enable App Store Server Notifications v2. Sources checked 2026-07-14.

1. Bind the affiliate and set the token

StoreKit 2 purchase with the Attribloom token

Set the Attribloom-issued token on the purchase. Apple returns it in the signed transaction and in App Store Server Notifications v2. No receipt-validation SDK, no fingerprinting.

swift
import Foundation
import StoreKit

struct AttribloomBindResponse: Decodable {
    let appAccountToken: UUID
    let offerCode: String?
}

enum AttribloomAPIError: Error {
    case invalidResponse
    case rejected(status: Int, body: String)
}

struct AttribloomAPI {
    let baseURL = URL(string: "https://api.attribloom.com")!

    func bind(signedClickId: String? = nil, refCode: String? = nil) async throws
        -> AttribloomBindResponse {
        struct Body: Encodable { let signedClickId: String?; let refCode: String? }
        return try await post("/v1/app-store/bind", Body(
            signedClickId: signedClickId, refCode: refCode
        ))
    }

    func bindDeferred(surfaceId: String) async throws -> AttribloomBindResponse {
        struct Body: Encodable { let surfaceId: String }
        return try await post("/v1/app-store/bind/deferred", Body(surfaceId: surfaceId))
    }

    private func post<Body: Encodable, Output: Decodable>(
        _ path: String, _ body: Body
    ) async throws -> Output {
        let endpoint = baseURL.appendingPathComponent(String(path.dropFirst()))
        var request = URLRequest(url: endpoint)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "content-type")
        request.httpBody = try JSONEncoder().encode(body)
        let (data, response) = try await URLSession.shared.data(for: request)
        guard let http = response as? HTTPURLResponse else {
            throw AttribloomAPIError.invalidResponse
        }
        guard (200..<300).contains(http.statusCode) else {
            throw AttribloomAPIError.rejected(
                status: http.statusCode,
                body: String(decoding: data, as: UTF8.self)
            )
        }
        return try JSONDecoder().decode(Output.self, from: data)
    }
}

func purchaseWithAttribution(
    product: Product,
    clipboardSignedClickId: String?,
    attribloomSurfaceId: String
) async throws {
    let api = AttribloomAPI()
    let binding: AttribloomBindResponse
    if let signedClickId = clipboardSignedClickId {
        binding = try await api.bind(signedClickId: signedClickId)
    } else {
        binding = try await api.bindDeferred(surfaceId: attribloomSurfaceId)
    }

    // Persist and reuse this UUID for the app install. Replace UserDefaults with
    // your existing secure app storage if your data policy requires it.
    UserDefaults.standard.set(
        binding.appAccountToken.uuidString,
        forKey: "attribloom.appAccountToken"
    )

    let result = try await product.purchase(options: [
        .appAccountToken(binding.appAccountToken),
    ])
    if case .success(.verified(let transaction)) = result {
        await transaction.finish()
    }
}

The /bind response also carries an optional offerCode (a subscription offer configured per campaign in Attribloom). Show it as the referred-user discount. Keep it Apple 3.2.2 safe: the reward attaches to subscribing, never to installing.

2. Forward Apple's notifications

Point your app's App Store Server Notifications v2 URL at your own server (or forward from it), then relay each notification's signedPayload to Attribloom, HMAC-signed. Attribloom independently re-verifies Apple's JWS chain to the Apple Root CA G3; the forwarding HMAC only proves the forward came from you.

Forward each notification, HMAC-signed

Timestamp is unix seconds. Signature is sha256= + hex of HMAC-SHA256 over "${timestamp}." + rawBody. The skew window is 300 seconds, so forward promptly.

javascript
import crypto from "node:crypto";

// Forward EVERY App Store Server Notification v2 your server receives to Attribloom,
// off the critical path (a delivery failure must never affect your own unlock logic).
const SURFACE_ID = "<your-surface-id>";          // last path segment of your Forwarding URL
const SECRET     = process.env.ATTRIBLOOM_FORWARDING_SECRET; // same value as Attribloom

async function forwardToAttribloom(signedPayload) {
  const ts   = Math.floor(Date.now() / 1000).toString();    // unix SECONDS, not ms
  const body = JSON.stringify({ signedPayload });            // Apple's JWS, verbatim
  const sig  = "sha256=" + crypto
    .createHmac("sha256", SECRET)
    .update(`${ts}.`)      // timestamp + "."
    .update(body)           // then the exact bytes you POST
    .digest("hex");

  await fetch(`https://api.attribloom.com/v1/app-store/assn/${SURFACE_ID}`, {
    method:  "POST",
    headers: {
      "content-type":   "application/json",
      "x-ea-timestamp": ts,   // unix seconds string
      "x-ea-signature": sig,  // "sha256=" + hex
    },
    body,
  });
}

Responses

statusHTTPMeaning
ok200Verified, matched an affiliate, commission accrued (real for a Production app, test for a Sandbox app).
trial200Verified and recognized, but a free trial, so nothing accrues yet. Accrues when it converts to paid.
duplicate200Already processed. Delivery is idempotent, so retries and Apple re-sends are safe.
unattributed200Verified, but no affiliate token on the transaction. Nothing to credit.
ignored200Not a relevant notification type (for example a TEST notification, which carries no transaction).
ignored_non_production200A Sandbox notification reached a Production-registered app. Dropped, zero ledger. Expected when testing sandbox purchases against a prod app.

The status field on a 200 response tells you what happened.

HTTPForwarder action
200Accepted (see the status field). Do not retry.
401Bad HMAC: secret mismatch or timestamp skew over 300s. Fix the secret, retry with a fresh timestamp.
400Unverifiable Apple JWS (bad payload). Log it, do not infinite-retry.
5xxTransient Attribloom error. Retry with backoff (Attribloom is idempotent; Apple also re-delivers).

Verified end to end

The server side (/bind and /assn) is live on https://api.attribloom.com, re-verifies every Apple JWS, posts on an integer-minor-unit double-entry ledger, and deduplicates by notification UUID.

Ready to attribute App Store purchases to your affiliates. Register your app, then run a test with the iOS testing guide.

Connect your app

Give this to your AI coding agent

Continue your iOS attribution implementation

Further reading

Compare GoMarketMe alternatives
Get startedI'm an affiliateTalk to us