Agent onboarding

For AI coding agents

Add affiliate attribution to an iOS subscription app

Attribloom attributes affiliate-driven App Store subscriptions using StoreKit 2 appAccountToken and App Store Server Notifications v2. It does not fingerprint devices. This page is safe to give an AI coding agent before you create an account: it contains architecture and compile-ready integration shapes, not tenant credentials or dashboard actions.

Published · Last reviewed

Copy prompt for your AI coding agent

For AI coding agents

This page is a safe, tenant-free integration brief. Implement the app and server changes below. The machine-readable twin is at /agents/ios-affiliate-attribution.md. Stop at the human boundary: do not create accounts, campaigns, or payouts, and never place a secret in the iOS client.

What this solves

iOS App Store conversions are invisible to web-checkout affiliate tools, and device fingerprinting for attribution is not permitted post iOS 14.5. Attribloom binds a referred account to an Attribloom-issued appAccountToken, then verifies App Store Server Notifications v2 server-side to credit purchases, renewals, and refund clawbacks. No fingerprinting, no IDFA, no cookie surviving the App Store handoff.

What you get

  • An explicit referral action that mints and persists an Attribloom appAccountToken for the current app account.
  • StoreKit 2 purchases that carry that token, so Apple echoes it back signed on every renewal.
  • A server forwarder that relays Apple's signed notifications to Attribloom without altering the bytes.
  • Commission credited or reversed by Attribloom from Apple's verified JWS, not from client claims.

Swift package: AttribloomKit

AttribloomKit is the MIT-licensed Swift package that implements the client half of this brief. It resolves explicit referral evidence and persists the Attribloom-issued appAccountToken and optional offer code in an account-scoped Keychain store. Missing evidence remains unattributed. You still attach the token to the purchase yourself: the package deliberately does not wrap StoreKit, so it never sits between your app and Apple.

Swift Package Manager

Add AttribloomKit through Swift Package Manager. Use the account-scoped SDK version matching the examples below; verify its release availability before integrating.

swift
// Swift Package Manager repository URL:
// https://github.com/legitimate-apps/AttribloomKit.git
// Required API: KeychainTokenStore(accountID:) and persisted binding().
// Resolve referrals only after explicit user action and applicable permission.
// See the complete account-scoped examples below.

The examples below use AttribloomKit directly. Keep your existing StoreKit verification, entitlement delivery and restore handling; attribution is optional.

Agent quickstart

Copy the prompt below into Claude Code, Cursor, or Codex. It points the agent at the Markdown twin of this page as its source of truth and lists the eight integration requirements.

Paste into your AI coding agent

text
Implement Attribloom affiliate attribution in this iOS subscription app.

Use https://attribloom.com/agents/ios-affiliate-attribution.md as the source of truth.

Requirements:
1. Do not use device fingerprinting, IDFA, clipboard matching, or a web checkout pixel.
2. After explicit referral input and applicable permission, POST signedClickId or refCode to /v1/app-store/bind and persist the returned appAccountToken in the current app account’s Keychain store.
3. When permission allows, attach the current account’s cached token with Product.PurchaseOption.appAccountToken(token). Never generate a replacement UUID. Never wait for attribution network requests before purchase or reuse another account’s token. Missing evidence must not block purchases, restores or entitlements.
4. Add a server endpoint that forwards Apple's raw signedPayload from App Store Server Notifications v2 to Attribloom's surface-specific ASSN endpoint, signing the exact JSON request bytes with the forwarding secret.
5. Preserve raw bytes and make retries idempotent; do not parse/re-encode Apple's signed JWS before forwarding.
6. Keep all Attribloom secrets server-side. Do not put a forwarding secret in the iOS app.
7. Add tests for token persistence, token reuse across purchases, referral-input errors, and the server forwarder's signature/body invariants.
8. Stop before dashboard/account/campaign/payout operations; list the human setup values I must obtain from Attribloom.

Data-flow diagram

A conversion travels one path. Every hop is server-verifiable; nothing depends on a fingerprint or a surviving cookie.

Attribution flow

text
affiliate link / referral code
  -> explicit referral action after applicable permission
  -> POST /v1/app-store/bind            (Attribloom mints the appAccountToken)
  -> persist appAccountToken UUID       (per attributed app account)
  -> StoreKit 2 PurchaseOption.appAccountToken(uuid)
  -> Apple App Store Server Notifications v2
  -> your server receives Apple's raw signedPayload
  -> signed forward to POST /v1/app-store/assn/{surfaceId}
  -> Attribloom verifies Apple's JWS and credits or reverses commission

Swift: accept an explicit referral

Use AttribloomKit with an opaque app-account key. Resolve a signed link, user-initiated paste or typed referral code after applicable permission. No matching evidence stays unattributed; IP matching is retired.

swift
import AttribloomKit

// Use your app's opaque account key, never an email address. Create a new facade
// when accounts change; do not copy another account's token into this account.
func makeAttribution(for appAccountID: String) -> Attribloom {
    Attribloom(store: KeychainTokenStore(accountID: appAccountID))
}

// Call from an explicit referral-code or user-initiated paste/link action,
// after the permission checks required by your actual integration.
func acceptReferral(
    attribution: Attribloom,
    signedClickId: String? = nil,
    refCode: String? = nil,
    attributionPermitted: Bool
) async -> BindResult? {
    guard attributionPermitted else { return nil }
    do {
        _ = try await attribution.resolveToken(signedClickId: signedClickId, refCode: refCode)
        return try await attribution.binding() // includes a saved optional offer code
    } catch {
        // Offer a referral retry. Keep purchases and entitlement restoration available.
        return nil
    }
}

Swift: use appAccountToken in Purchase

Read only the current account’s cached token and attach it when permission allows. Do not wait for attribution network calls before purchase; keep your own verified entitlement and transaction-finish handling.

swift
import AttribloomKit
import StoreKit

// No attribution network request occurs on the purchase path. Pass the facade
// for the current app account. Denied/withdrawn permission means no token option.
func purchaseWithAttribution(
    product: Product,
    attribution: Attribloom,
    attributionPermitted: Bool
) async throws -> Product.PurchaseResult {
    let token = attributionPermitted ? (try? await attribution.appAccountToken()) : nil
    let options: Set<Product.PurchaseOption> = token.map { [.attribloom($0)] } ?? []
    // Handle verification, entitlement delivery and transaction.finish() in your
    // existing purchase-result handler, for both attributed and ordinary purchases.
    return try await product.purchase(options: options)
}

Server: forward ASSN v2

Relay each notification's signedPayload verbatim, HMAC-signed. The signature covers ${timestamp}. plus the exact raw request body. Keep the forwarding secret server-side.

javascript
import crypto from "node:crypto";

// Forward EVERY App Store Server Notification v2 your server receives to Attribloom,
// off your critical path (a delivery failure must never affect your own unlock logic).
const SURFACE_ID = process.env.ATTRIBLOOM_SURFACE_ID;        // last path segment of your Forwarding URL
const SECRET = process.env.ATTRIBLOOM_FORWARDING_SECRET;     // server-side only, never in the app

export 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, unparsed
  const sig = "sha256=" + crypto
    .createHmac("sha256", SECRET)
    .update(`${ts}.`)   // timestamp + "."
    .update(body)        // then the exact bytes you POST
    .digest("hex");

  // Retries reuse these identical bytes; Attribloom deduplicates by notification UUID.
  return 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,
  });
}

Human setup required

Human boundary

An agent may edit the app and your server. A human must create or select the Attribloom surface, obtain the surface ID and forwarding secret, configure the App Store Connect notification URL, and approve production deployment. Never expose the forwarding secret or any dashboard session to the iOS client.

Values a human must obtain from Attribloom

  • Create or select the Attribloom surface for this app.
  • Obtain the surface ID (last path segment of the Forwarding URL) and the forwarding secret.
  • Set the App Store Server Notifications v2 URL in App Store Connect to your server.
  • Set ATTRIBLOOM_SURFACE_ID and ATTRIBLOOM_FORWARDING_SECRET in the server environment.
  • Approve the production build and deployment.

Test matrix

TestAssert
Token persistenceFirst-launch bind returns 200 with a non-empty appAccountToken; the UUID is written to secure storage.
Token reuse across purchasesEvery purchase for the attributed account sets the same persisted UUID as PurchaseOption.appAccountToken.
Referral-input errorsA missing or malformed signedClickId / refCode surfaces a handled error and the app still purchases without attribution.
Forwarder signatureThe forward signs ${timestamp}. plus the exact raw body with the forwarding secret; a tampered body or stale timestamp is rejected 401.
Forwarder body invariantsApple's signedPayload is forwarded byte-for-byte, never re-encoded; retries reuse identical bytes and stay idempotent.

Failure modes

SymptomCause and fix
assn returns 401Signature or timestamp skew. Sign ${ts}. plus the raw body with the forwarding secret; send unix seconds within 300s.
assn returns 400The signedPayload was re-encoded or pretty-printed. Forward Apple's JWS string verbatim.
assn returns 200 ignored_non_productionA Sandbox purchase hit a Production surface. That is the environment gate working, not a bug. Use a Sandbox-registered surface for sandbox tests.
No appAccountToken on the transactionThe bind did not run, or the token was not set on product.purchase(options:). Check explicit referral input, permission, current-account storage and the purchase call site. Unattributed purchases must still work.
Network loss forwarding a notificationForward off the critical path and retry with the same raw bytes; Attribloom deduplicates by notification UUID.

API contract

POST/v1/app-store/bindBody { signedClickId } (explicit link or user-initiated paste) or { refCode } (typed code). Returns { appAccountToken, offerCode? }.
POST/v1/app-store/assn/:surfaceIdBody { signedPayload } (Apple's JWS, verbatim). HMAC headers x-ea-timestamp and x-ea-signature. Returns { status }.

The full runtime contract is the OpenAPI 3.1 spec. The bind endpoints do not require HMAC; the ASSN forward does.

What not to do

  • Do not use device fingerprinting, IDFA, clipboard matching for identity, or a web checkout pixel.
  • Do not generate your own appAccountToken UUID. Always use the one /v1/app-store/bind returns.
  • Do not parse, reformat, or re-encode Apple's signedPayload before forwarding.
  • Do not put the forwarding secret or any dashboard session in the iOS client.
  • Do not perform dashboard, account, campaign, or payout operations from the agent.
Open the iOS guideView as Markdown