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

# Inbound webhooks

> Connect Adapty, Superwall, AppsFlyer, Adjust, Singular, CleverTap and Pushwoosh webhooks to AppDNA: authentication, setup, user ids, and how their events are counted.

## Overview

AppDNA can receive webhooks from billing, attribution and messaging providers. Each authenticated delivery is stored
and converted into AppDNA's canonical events with `source: "<provider>"`. RevenueCat has its own page:
[RevenueCat](/integrations/revenuecat).

<Warning>
  **Mappings are enabled per provider after a confirmed real delivery.** Authentication works for every provider below
  today. Each provider's event mapping — other than RevenueCat's — is switched on only after a real delivery from that
  provider has confirmed its payload. Until then, authenticated deliveries are **stored, not counted**; they are kept
  without the URL token and without the personal fields AppDNA removes for that provider — Adapty: e-mail, phone number
  and user attributes; Superwall: user attributes other than `appdna_user_id`; CleverTap: profile e-mail, phone, name and
  push token; AppsFlyer and Singular: IP address; Adjust and Pushwoosh: nothing is removed — and are replayed into
  analytics once the mapping is enabled.
</Warning>

<Note>
  **Provider events count for revenue, not for activity.** Provider events count for revenue, lifetime revenue and
  attribution. They never count as sessions, active users, retention or billable MAU — those come only from the AppDNA SDK
  running in your app. Purchases and renewals from a billing provider (RevenueCat, Adapty, Superwall) do count toward
  MTPU (monthly tracked paying users), once per user after de-duplication with SDK purchases. Once a billing provider
  reports a subscription, its renewals are counted from the provider only; the SDK's device renewal for that subscription
  adds no revenue.
</Note>

## How each provider counts

| Class | Providers | Events | Counted as purchase revenue? |
| - | - | - | - |
| **Revenue sources** | AppDNA's own billing, RevenueCat, Adapty, Superwall | `purchase_completed`, `subscription_*`, `purchase_refunded` | **Yes** |
| **Attribution-only** | AppsFlyer, Adjust, Singular | `attributed_install`, `reengagement`, `reattribution`, `attributed_in_app_event`, `ad_revenue` | **No.** In-app revenue they report is kept as `reported_revenue`, `reported_revenue_currency`, `reported_revenue_usd`; ad revenue as `ad_revenue_usd` |
| **Engagement-only** | CleverTap, Pushwoosh | `push_*`, `email_*`, `in_app_message_*`, `messaging_campaign_sent` | No revenue |

* **One purchase, one count.** When several sources report the same store transaction, AppDNA keeps one, by precedence
  RevenueCat > Adapty > Superwall > the AppDNA SDK > events you tracked yourself.
* **Do not track** canonical purchase or attribution event names yourself with `AppDNA.track` when the corresponding
  webhook is connected; use custom names for your own UI analytics.
* Events from a messaging or attribution provider never trigger AppDNA journeys or e-mail automations. Billing-provider
  purchases do, like SDK purchases — except replayed events (deliveries stored before the provider's mapping was
  enabled), which never trigger journeys or e-mail automations.
* When several attribution providers are connected, AppsFlyer > Adjust > Singular decides a user's attribution.

## Authentication and secrets

Each provider uses one of three mechanisms. The integration page shows the right controls.

| Mechanism | Providers | What you configure |
| - | - | - |
| **Authorization header** | Adapty, AppsFlyer, CleverTap, Pushwoosh | The webhook URL plus an Authorization value `Bearer <secret>` generated by AppDNA |
| **Token in the URL** | Adjust, Singular | A webhook URL that includes `&token=<secret>` generated by AppDNA. The provider offers no header authentication, so the token travels in the URL; AppDNA excludes these URLs from its request logs |
| **Signed deliveries (Svix)** | Superwall | The plain webhook URL, plus Superwall's own signing secret pasted into AppDNA's **Webhook signing secret** field — **mandatory** |

* **Reveal, Copy and Rotate are owner-only.** The Authorization value, or the tokenised URL, is masked on the
  integration page; only the organisation owner can reveal, copy or rotate it. Right after an admin connects an
  integration, the connect dialog shows the plain URL and, for the owner, the credential once. Rotating invalidates the
  old credential immediately — paste the new one into the provider.
* **Superwall** has no AppDNA-generated secret: paste Superwall's signing secret (`whsec_…`). It can be replaced, not
  removed. Signatures older than 5 minutes are rejected.
* Failed authentication returns **401**. The reason is logged on AppDNA's side, never the secret.
* **Disconnected integrations drop deliveries.** While an integration is disconnected, authenticated deliveries are
  answered with **200** (so the provider does not retry for hours or disable the endpoint) and discarded. Reconnecting
  keeps the existing secret.

## Pass the AppDNA user id to each provider

AppDNA links a provider event to your user through the id you pass to `AppDNA.identify`. Give each provider **the same
id**:

| Provider | Where to pass the AppDNA `identify` id |
| - | - |
| Adapty | `Adapty.identify(<id>)` |
| Superwall | `Superwall.shared.identify(userId: <id>)` **before** the purchase, **and** the Superwall user attribute `appdna_user_id` = `<id>` |
| AppsFlyer | `setCustomerUserID(<id>)` |
| Adjust | the global callback parameter `appdna_user_id` = `<id>` |
| Singular | the Singular custom user id = `<id>` |
| CleverTap | `onUserLogin` with `Identity` = `<id>` |
| Pushwoosh | `Pushwoosh.setUserId(<id>)` |

Events from a user the provider does not yet know are stored and counted under a pseudonymous placeholder, and are
linked to the user when a later event from the same provider carries the id.

## Provider setup

### Adapty (billing)

* In Adapty → Integrations → Webhooks, paste the AppDNA webhook URL into **both** the production and the sandbox
  endpoint fields, and paste the same Authorization value
  `Bearer <secret>` into **both** the production and the sandbox Authorization fields. Adapty's verification request on
  Save succeeds.
* Sandbox events (`environment: Sandbox`) go to AppDNA's sandbox dataset.
* If you renamed Adapty event ids, enter them under **Renamed event IDs** on the integration page so AppDNA recognises
  them.
* Mapped events: subscription and trial starts → `purchase_completed` + `subscription_started`; renewals and trial
  conversions → `subscription_renewed`; renewal cancellations → `subscription_canceled`; expirations →
  `subscription_expired`; billing issues → `subscription_renewal_failed`; refunds → `purchase_refunded`; one-time
  purchases → `purchase_completed`.
* With `billingProvider: adapty`, the AppDNA SDK keeps sending its device renewal and cancellation events; connect the
  Adapty webhook to add server-side renewal and refund data once its mapping is enabled — AppDNA then counts each event
  once.

### Superwall (billing)

* In Superwall → Integrations → Webhooks, add the AppDNA webhook URL, then copy Superwall's signing secret into AppDNA's
  **Webhook signing secret** (owner).
* Mapped events: initial purchases → `purchase_completed` (+ `subscription_started` for subscriptions), renewals →
  `subscription_renewed`, cancellations → `subscription_canceled`, expirations → `subscription_expired`, billing issues →
  `subscription_renewal_failed`; any event with a negative price → `purchase_refunded`, and so is a cancellation with
  `cancelReason: CUSTOMER_SUPPORT` (Superwall's refund carrier) — it becomes `purchase_refunded` (negative revenue), not
  `subscription_canceled`.
* Superwall sends no test webhooks. Sandbox events go to the sandbox dataset.

### AppsFlyer Push API (attribution-only)

* In AppsFlyer → Integration → API access → Push API, set method **POST**, the AppDNA URL, token name `Authorization` and value `Bearer <secret>`.
* Tick these fields: `event_type`, `appsflyer_id`, `customer_user_id`, `media_source`, `campaign`, `af_c_id`, `af_adset`,
  `af_ad`, `af_channel`, `install_time`, `is_retargeting`, `is_primary_attribution`, `campaign_type`, `event_revenue`,
  `event_revenue_currency`, `event_revenue_usd`, the `af_cost_*` fields, `country_code`, `platform`.
* **AppsFlyer's "Send test" button gets 401 by design:** AppsFlyer test messages carry no token. Test with a real
  install, or use a separate AppsFlyer app for test builds.
* AppsFlyer has no sandbox flag; every delivery counts as production.

### Adjust server callbacks (attribution-only)

* Use the full URL from **Reveal** (it includes the token) as a **global** callback with method **POST**, and add the
  single-activity ad-revenue callback if you monetise with ads.
* Include these placeholders in the body: `activity_kind`, `created_at_milli`, `installed_at`, `environment`, `adid`,
  `idfa`, `idfv`, `gps_adid`, `os_name`, `app_id`, `store`, `country`, `event_token={event}`, `event_name`,
  `revenue_float`, `currency`, `revenue_usd`, `tracker`, `tracker_name`, `network_name`, `campaign_name`,
  `adgroup_name`, `creative_name`, `is_organic`, `is_reattributed`, `match_type`, `partner_parameters`, plus the
  ad-revenue placeholders.
* Add the global callback parameter `appdna_user_id` in your app (above).
* Rotating issues a new URL — update it in Adjust.

### Singular Internal BI postbacks (attribution-only)

* Use the full URL from **Reveal** (it includes the token) as the postback URL.
* Set the Singular custom user id to the AppDNA id. Deliveries whose `fraud_status` is not `valid` are stored, not
  counted.
* Rotating issues a new URL — update it in Singular.

### CleverTap Webhooks channel (messaging)

* Create a webhook in CleverTap with method **POST**, the AppDNA URL, and on the Headers tab the key `Authorization`
  with value `Bearer <secret>`.
* **The `appdna_event` convention:** a CleverTap webhook is a campaign call, not an event stream. To record a specific
  engagement event, add the key-value `appdna_event` to the webhook with one of `push_delivered`, `push_tapped`,
  `email_sent`, `email_delivered`, `email_opened`, `email_clicked`, `email_bounced`, `email_unsubscribed`,
  `in_app_message_shown`, `in_app_message_clicked` — for example on a Live-behaviour webhook triggered by
  "Notification Clicked". Post Action webhooks record `push_delivered`; any other campaign webhook records
  `messaging_campaign_sent` for each targeted profile. Template tests (`is_test`) are stored, not counted.

### Pushwoosh Event Streaming (messaging)

* In Pushwoosh → Event Streaming, add the AppDNA URL and set the Authorization value `Bearer <secret>`.
* Mapped events: `Push Delivered` → `push_delivered`, `Push Opened` → `push_tapped`, e-mail events → `email_*`, in-app
  shows and clicks → `in_app_message_shown` / `in_app_message_clicked`.
* Pushwoosh batches events: expect up to **1 hour** of latency.

## Troubleshooting

| Symptom | Cause |
| - | - |
| The provider shows 401 | The credential does not match (rotated, or pasted wrongly); for Superwall, the signing secret is missing or wrong |
| Deliveries answer 200 but nothing appears in analytics | The provider's mapping is not enabled yet (deliveries are stored and replayed later), or the integration is disconnected |
| Events appear under anonymous users | The provider was not given the `AppDNA.identify` id (table above) |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.