Attribura

Attribura iOS SDK

Capture the answer to your in-app “How did you hear about us?” question and tie it to the content you posted — including the word-of-mouth and App Store Search traffic no tracking link can see. Headless: you build the sheet, the SDK just tracks.

GitHub repo ↗·llms.txt ↗iOS 13+ · Swift 5.9+ · zero deps

Paste this into Claude Code

It carries your own token and points at the full guide. One paste and the whole setup is done — the steps below are the same thing by hand.

Set up Attribura in this iOS app.

Read https://app.attribura.com/sdk/llms.txt first — it is the complete integration guide.

Then:
1. Add the Swift package https://github.com/martindochamp/attribura-ios
2. Configure it at launch with token "atb_ingest_xxx" and baseURL https://api.attribura.com
3. Declare my onboarding screens in order, and call Attribura.step("…") at the top of each one
4. Build a "How did you hear about us?" sheet and call Attribura.reportSource() when the user picks an answer
5. Call Attribura.observePurchases() once, right after configure, and buy through
   Attribura.purchase(product) instead of product.purchase(...)
6. Call Attribura.setUserId() as soon as we know who the user is, if the app has accounts

Match this project's existing style and architecture. Show me the diff before applying it.

1 — Install

In Xcode: File → Add Package Dependencies… and paste https://github.com/martindochamp/attribura-ios. Or add it to your Package.swift:

swift
.package(url: "https://github.com/martindochamp/attribura-ios", from: "0.4.0")

2 — Get your token

Copy your ingest token from Settings → Integrations → Attribura SDK. It's the same per-org token the Superwall/Stripe webhooks use, and it's write-only— it can't read your data back.

3 — Configure once

swift
import Attribura

// Once, as early as possible (App init / didFinishLaunching):
Attribura.configure(
    token: "atb_ingest_xxx",
    baseURL: URL(string: "https://api.attribura.com")!,
    // Optional — your onboarding screens, in order. Only needed for the funnel.
    onboarding: ["welcome", "goal", "source", "notifications", "paywall"]
)

Signed in? Your real ingest token is already filled in above — just copy.

That line is also your retention. From 0.4.0, configure reports one app open per install per day by itself, and Overview draws Day 1, Day 7 and Day 28 by install cohort, split by the channel each install came from. Apple has no such report. There is no call to add.

4 — Report the answer

swift
// When the user taps an answer in YOUR sheet.
// No userId needed: the SDK's own anonymous id ties the answer to the purchase.
// Pass one anyway if your app has accounts, or if your paywall provider reports
// its own id (Superwall's app_user_id) and you want both to line up.
Attribura.reportSource(.instagram)

The SDK ships no UI. Here's a minimal SwiftUI sheet you own and wire up:

swift
import SwiftUI
import Attribura

struct HeardAboutUsSheet: View {
    let userId: String
    var onDone: () -> Void

    private let options: [(String, AttributionSource)] = [
        ("Instagram", .instagram), ("TikTok", .tiktok), ("YouTube", .youtube),
        ("A friend", .friend), ("A podcast", .podcast), ("App Store search", .appStoreSearch),
    ]

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("How did you hear about us?").font(.title3.bold())
            ForEach(options, id: \.0) { label, source in
                Button(label) {
                    Attribura.reportSource(source, userId: userId,
                                           prompt: "How did you hear about us?")
                    onDone()
                }
                .buttonStyle(.bordered)
            }
        }
        .padding()
    }
}

Randomize the option order

People rushing onboarding tend to tap the same position every time (usually the first one), which quietly biases your data toward whatever you list first. Shuffle the options per user so that noise averages out:

swift
// Randomize the order once per user so "always tap the first option"
// habits don't skew your data — .shuffled() gives a fresh order each init.
private let options: [(String, AttributionSource)] = [
    ("Instagram", .instagram), ("TikTok", .tiktok), ("YouTube", .youtube),
    ("A friend", .friend), ("A podcast", .podcast), ("App Store search", .appStoreSearch),
].shuffled()

5 — Track the onboarding funnel (optional)

Declare your screens once, in order, in configure(onboarding:) above. Then mark each one as it appears. Attribura records the drop-off, split by the channel each run said it came from — the part a general analytics tool cannot show you, because it never asked the question. Pull it with the get_onboarding_funnel MCP tool or the matching data API endpoint.

swift
// At the top of each onboarding screen.
struct GoalScreen: View {
    var body: some View {
        content.onAppear { Attribura.step("goal") }
    }
}

// As soon as you know who the user is — this attaches the id to the WHOLE run,
// including the steps recorded before sign-in.
Attribura.setUserId(currentUserId)

Put the question early

The funnel can only be split from the step where the answer arrives downwards: a run that quits before it never reported a channel, so the screens above it show a total and no breakdown. Moving “how did you hear about us?” one screen earlier lights up one more step you can split by channel. Asked on screen one, though, the question costs some completion; after one easy commitment and before your first real ask is the sweet spot.

A step name you did not declare comes back as undeclared, so a typo shows up here rather than as a funnel row that looks wrong weeks later. And if one session can onboard twice — a sign-out that returns to the start — call Attribura.newRun() so the second pass is counted.

6 — Report the money

An answer on its own tells you where people come from. Add this and it tells you what they paid, which is the number the answer was worth asking for. Two lines, and nothing to configure in App Store Connect.

swift
import StoreKit

// 1. Once, right after configure:
Attribura.observePurchases()

// 2. Instead of product.purchase(...) — this is the line that credits the post:
let result = try await Attribura.purchase(product)

Attribura.purchase is the line that matters. It adds the id that answered the question, buys through StoreKit, and reports the sale the moment it succeeds — result is the same Product.PurchaseResult, so your switch over it doesn't change. A plain product.purchase() is not seen by the SDK: Apple hands a purchase made in the app back through its own result, not through observePurchases()'s stream.

Every transaction is forwarded exactly as Apple signed it and checked against Apple's own root certificate before it counts. The SDK never calls finish() on your transactions — only your app knows when it has delivered what was bought.

Test it before you ship it

Sandbox purchases are real signatures on fake money, and they are welcome: they are kept apart so no revenue figure counts them. Buy something in the sandbox, answer your own question first, and watch the whole chain work end to end before a real customer ever tries it. A purchase made against an Xcode StoreKit configuration file is signed by Xcode, not Apple, and is refused: set the scheme's StoreKit Configuration to None first.

Two things worth knowing. A renewal or a refund that happens while the app is closed arrives at the next launch, not the moment Apple books it — a first purchase, the one that carries the attribution, is immediate. And if purchases also reach Attribura through Superwall or Stripe, you do not have to unplug anything: once an app reports Apple's own transactions, those become the ones that count, and the webhook stops crediting the same sale a second time.

Sources

Linkable channels — cross-checked against your recent posts:

instagramtiktokyoutubexredditlinkedinthreadsfacebook

Dark social — unlinkable, but the traffic every link misses:

googleSearchappStoreSearchfriendpodcastnewsletterother(…)

How it resolves

The answerMethodConfidence
One recent post on that channelself_report_verified0.60 — pins the exact post
Linkable channel, 0 or several recent postsself_report0.45 — channel-level
Dark social (friend, podcast, App Store search…)dark_social0.40 — no link could see it

A deterministic signal (the campaign token) always wins — a survey answer never overrides a certified link. Attribution stays confidence-scored and directional, never deterministic.

Integrating with an AI assistant

Paste the SDK's llms.txtinto Claude or ChatGPT and ask it to wire the SDK into your app — it's a complete, self-contained integration guide.