> ## Documentation Index
> Fetch the complete documentation index at: https://docs.evomarketing.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Billing connectors

> Send purchases, renewals, and refunds to Dialed straight from RevenueCat, Superwall, Stripe, Shopify, or Apple

A connector lets your billing provider report revenue server-side, so attribution does not depend on a browser beacon or an SDK call firing.

Create one per provider in the client portal under **Results → Attribution → Connect your billing**. Dialed generates one inbound URL per provider:

```
https://dialedapi.evomarketing.co/api/webhooks/attribution/:provider/:token
```

The random URL token (`evw_…`) is the baseline authentication for every provider. Signing secrets add provider-native verification, are encrypted at rest, and are never returned to the portal — it only reports `has_signing_secret`.

## Set up your provider

<Tabs>
  <Tab title="RevenueCat">
    ### What you paste where

    In RevenueCat, open **Integrations → Webhooks → New webhook**. Paste the generated URL. Under **Authorization header value**, paste your connection token. Leave all events selected.

    No signing secret is needed — the Authorization header is the verification.

    ### The one line of code

    Right after you configure RevenueCat:

    ```swift theme={null}
    Purchases.shared.attribution.setAttributes(["evo_install_id": EVOAttribution.installId])
    ```

    ```ts theme={null}
    Purchases.setAttributes({ evo_install_id: await getEvoInstallId() })
    ```

    ### Events we ingest

    | RevenueCat type         | Becomes                                                 |
    | ----------------------- | ------------------------------------------------------- |
    | `INITIAL_PURCHASE`      | `purchase`, or `trial` when `period_type` is `TRIAL`    |
    | `NON_RENEWING_PURCHASE` | `purchase`                                              |
    | `RENEWAL`               | `renewal`                                               |
    | `PRODUCT_CHANGE`        | `renewal` when the price is positive, otherwise ignored |
    | Anything else           | Ignored                                                 |

    Amount comes from `event.price`; a trial is recorded at zero. `takehome_percentage`, when present, produces the net amount alongside the gross.

    ### How matching works

    The install id is read from `subscriber_attributes.evo_install_id.value`. If that is missing, EVO falls back to the first UUID-shaped value among the event's `aliases`, `app_user_id`, and `original_app_user_id` — which is why passing the EVO install ID as the RevenueCat app user id also works.

    ### Signature verification

    The `Authorization` header must equal the connection token, with or without a `Bearer ` prefix. A mismatch returns `401` with `invalid_authorization`.

    ### Confirm it

    Press **Send test** in RevenueCat. The portal will show **Connected**.
  </Tab>

  <Tab title="Superwall">
    ### What you paste where

    In Superwall, open **Integrations → Webhooks → Add**. Paste the generated URL and add a custom header `X-EVO-Token` with the connection token.

    If Superwall shows a signing secret for the endpoint, paste it into the connection as well — it is optional.

    ### The one line of code

    ```swift theme={null}
    Superwall.shared.setUserAttributes(["evo_install_id": EVOAttribution.installId])
    ```

    ```ts theme={null}
    Superwall.shared.setUserAttributes({ evo_install_id: await getEvoInstallId() })
    ```

    ### Events we ingest

    | Superwall type                              | Becomes                                                 |
    | ------------------------------------------- | ------------------------------------------------------- |
    | `initial_purchase`, `non_renewing_purchase` | `purchase`                                              |
    | `renewal`                                   | `renewal`                                               |
    | `product_change`                            | `renewal` when the price is positive, otherwise ignored |
    | Anything else                               | Ignored                                                 |

    Amount comes from `price`, net revenue from `proceeds`, and currency from `currencyCode`.

    ### How matching works

    The install id is read from `userAttributes.evo_install_id`. Renewals fall back to the original purchase through `originalTransactionId`.

    ### Signature verification

    When a signing secret is stored, the `Superwall-Signature` header must be the HMAC-SHA256 hex digest of the raw request body using that secret. Without a stored secret, the URL token alone authenticates the request.

    ### Confirm it

    Make a sandbox purchase. The portal will show **Connected**.
  </Tab>

  <Tab title="Stripe">
    ### What you paste where

    In Stripe, open **Developers → Webhooks → Add endpoint**. Paste the generated URL and select `checkout.session.completed`, `invoice.paid`, and `charge.refunded`. Copy the `whsec_…` signing secret Stripe shows you and paste it into the connection.

    ### The one line of code

    When you create a Checkout Session:

    ```javascript theme={null}
    client_reference_id: evo.token()
    ```

    `evo.token()` comes from the [EVO pixel](/attribution/pixel) already on the site and returns `null` when the visitor did not arrive through a creator link. `metadata.evo_cid` is read as an equivalent if `client_reference_id` is already in use.

    ### Events we ingest

    | Stripe event                                          | Becomes                                                                                                                    |
    | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
    | `checkout.session.completed`                          | `purchase` when `payment_status` is `paid`, otherwise ignored                                                              |
    | `invoice.paid`, `billing_reason: subscription_create` | `purchase` — unless a checkout purchase for the same subscription landed in the last hour, which is ignored as a duplicate |
    | `invoice.paid`, `billing_reason: subscription_cycle`  | `renewal`                                                                                                                  |
    | `charge.refunded`                                     | `refund`                                                                                                                   |
    | Anything else                                         | Ignored                                                                                                                    |

    Amounts come from `amount_total`, `amount_paid`, and `amount_refunded` respectively — already in cents.

    ### How matching works

    Click token first, from `client_reference_id` or `metadata.evo_cid`. Next, a creator code: Stripe webhooks carry discounts as ids unless expanded, so EVO checks `metadata.evo_code` (also `creator_code` and `code`), then any expanded promotion code's `code`, then each coupon's `name` and `id`. Give the coupon the creator code as its id or name, or pass the code in metadata when you create the session, and the sale credits that creator. Renewals and refunds that carry neither inherit the original purchase, matched on the subscription id or payment intent stored as `external_ref`.

    ### Signature verification

    The `Stripe-Signature` header is parsed into its `t` timestamp and `v1` signatures, and checked as an HMAC-SHA256 of `"<timestamp>.<raw body>"` with a 300-second tolerance. A failure returns `401`.

    If no signing secret is stored, the connection is flagged `unsigned` in its settings and requests are accepted on the URL token alone. Saving a secret clears the flag.

    ### Confirm it

    Send a test event from the Stripe webhook page.
  </Tab>

  <Tab title="Shopify">
    ### What you paste where

    In the Shopify admin, open **Settings → Notifications → Webhooks → Create webhook**. Event **Order creation**, format **JSON**, URL: the generated URL. Create a second webhook for **Refund creation** with the same URL.

    At the bottom of that page Shopify shows the secret your webhooks are signed with. Paste it into the connection.

    ### The one line of code

    None. Orders that started from a creator link are matched automatically from the landing page. If the [EVO pixel](/attribution/pixel) is on your theme, it also copies a newly captured token into the cart attributes, which covers visitors who come back later.

    ### Events we ingest

    | Shopify topic                                                                                   | Becomes                                                                               |
    | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
    | `orders/create` with `financial_status` of `paid`, `authorized`, `partially_paid`, or `pending` | `purchase`                                                                            |
    | `orders/create` with any other financial status                                                 | Ignored                                                                               |
    | `refunds/create`                                                                                | `refund` for the sum of successful refund transactions; ignored when that sum is zero |

    ### How matching works

    The click token is read from the `evo_cid` parameter on the order's `landing_site` URL, falling back to a `note_attributes` entry named `evo_cid`. Next, the order's `discount_codes`: the first one that is an active creator code for your brand credits that creator, so a creator code that is also a Shopify discount works with nothing else on the theme. Ordinary discounts like `SAVE10` are ignored. A refund inherits its order through `order_id`.

    ### Signature verification

    The `X-Shopify-Hmac-Sha256` header must be the base64 HMAC-SHA256 of the raw body using the stored secret. A failure returns `401`. Without a stored secret, the connection is flagged `unsigned` and the URL token alone authenticates.

    ### Confirm it

    Place a test order. The portal will show **Connected**.
  </Tab>

  <Tab title="Apple App Store">
    For an app that does not use RevenueCat or Superwall.

    ### What you paste where

    The portal asks for your app's **bundle id** when you create the connection. Apple sends notifications per app, so the connection cannot save without it.

    In App Store Connect, open your app → **App Information → App Store Server Notifications**. Set both the **Production** and **Sandbox** URLs to the generated URL, using **version 2**.

    ### The one line of code

    At purchase time, pass your EVO install ID as the `appAccountToken` so Apple's notification carries it:

    ```swift theme={null}
    try await product.purchase(options: [.appAccountToken(UUID(uuidString: EVOAttribution.installId)!)])
    ```

    StoreKit 1 uses `payment.applicationUsername`.

    ### Events we ingest

    | Apple notification                                       | Becomes                                                                 |
    | -------------------------------------------------------- | ----------------------------------------------------------------------- |
    | `SUBSCRIBED` with subtype `INITIAL_BUY` or `RESUBSCRIBE` | `purchase`, or `trial` when the offer type is `1` and the price is zero |
    | `DID_RENEW`                                              | `renewal`                                                               |
    | `ONE_TIME_CHARGE`                                        | `purchase`                                                              |
    | `REFUND`                                                 | `refund`, timestamped from `revocationDate`                             |
    | Anything else                                            | Ignored                                                                 |

    Apple reports price in thousandths, so the amount is divided by 10 to reach cents.

    ### How matching works

    The install id is `appAccountToken`, accepted only when it is a well-formed UUID. Renewals and refunds inherit the original purchase through `originalTransactionId`.

    ### Signature verification

    Apple JWS signatures are accepted only when their certificate chains terminate at the bundled Apple Root CA G3 and carry Apple's notification certificate OIDs. A failure returns `401` with `invalid_signature`.

    ### Confirm it

    Make a sandbox purchase. The portal will show **Connected**.
  </Tab>
</Tabs>

## Ledger semantics

Every connector event lands in the same `attribution_events` ledger as pixel and SDK conversions.

### Signs and totals

| Event type | Amount       | Counts as              |
| ---------- | ------------ | ---------------------- |
| `purchase` | Positive     | Conversion and revenue |
| `renewal`  | Positive     | Conversion and revenue |
| `trial`    | Zero         | Conversion only        |
| `refund`   | **Negative** | Revenue only           |

Refund rows store negative amounts, so an ordinary `SUM(amount_cents)` produces net revenue with no special-casing.

### Net vs gross

`amount_cents` is the gross value the provider reported. `net_cents` is the provider's own take-home figure when it supplies one — RevenueCat's `takehome_percentage` and Superwall's `proceeds`. Providers that do not supply it leave `net_cents` empty.

### Sandbox

Sandbox and test rows still flip a connection to **Connected**, so you can confirm setup end to end. They are excluded from client-facing conversion and revenue totals by the reporting serializers.

Sandbox rows are detected per provider: `environment: "SANDBOX"` (RevenueCat, Superwall), `livemode: false` (Stripe), `test: true` (Shopify), and `data.environment: "Sandbox"` (Apple).

### Deduplication

The dedup key is `<provider>:<transaction id>`, prefixed with `refund:` for refunds and `sandbox:` for sandbox events. A double-fired webhook cannot count a purchase twice, and a sandbox event never collides with the live event that shares its id.

### Matching order

For every connector event, the first rung wins:

1. **Click token** on the event — resolves at `0.95` with `resolution_method: "link"`.
2. **Creator code** typed as a discount or passed in metadata — resolves at `0.9` with `resolution_method: "code"` and bumps the code's usage counters.
3. **Install id** — the SDK install with that id, whose attribution the event adopts.
4. **Parent purchase** — for a renewal or refund only, the most recent purchase with the same `external_ref` and the same sandbox flag. This is what keeps a renewal credited after an uninstall or a rotated user id.
5. Otherwise `unattributed` at confidence `0`. The revenue still lands, so the brand total and the unattributed gap stay honest.

### Webhook responses

| Response                                   | Meaning                                                        |
| ------------------------------------------ | -------------------------------------------------------------- |
| `200 {"ok":true,"recorded":1,"ignored":0}` | Accepted                                                       |
| `404 {"error":"unknown_connection"}`       | The provider or token in the URL does not match any connection |
| `401`                                      | Signature or authorization check failed                        |
| `422`                                      | The payload could not be parsed or the ledger row was rejected |

Any error also sets the connection's status to `error` and stores the reason in `last_error`, which the portal displays. A successful delivery clears it and sets the status back to `active`.
