Skip to main content

Webhooks

AppDNA sends HTTP POST requests to your server when events occur in your app. Webhooks let you sync data to your backend, trigger workflows, update CRMs, feed events into data warehouses, or power any custom integration.

Overview

When you create a webhook endpoint in the AppDNA dashboard, you choose which event types to subscribe to. When a matching event occurs, AppDNA delivers a signed JSON payload to your endpoint URL via HTTP POST. Every delivery includes an HMAC-SHA256 signature so you can verify the payload originated from AppDNA and was not tampered with.

Setup

1

Navigate to Webhooks

Go to Console > Settings > Webhooks in the AppDNA dashboard.
2

Add an endpoint

Click Create Endpoint and enter your HTTPS URL. Non-HTTPS URLs are not supported in production.
3

Select event types

Choose which event types this endpoint should receive. You can select individual events or subscribe to all events.
4

Save and copy the signing secret

After saving, the dashboard displays your signing secret. Copy it immediately — it is only shown once. You will use this secret to verify webhook signatures.
The signing secret is only displayed once when you create the endpoint. If you lose it, you must rotate the secret from the endpoint settings, which invalidates the previous secret.

Event Types

AppDNA supports 16 webhook event types organized by category:

Onboarding

Surveys

Billing

Push

Email

In-App Messages

Journeys

Experiments

SDK

System


Payload Structure

Every webhook payload follows a consistent structure:
The data object contents vary by event type. Refer to the specific event type documentation in the dashboard for the full schema of each event.

Payment completed data

A store-driven webhook is recorded in the same database write as the store notification it belongs to, and handed to webhook delivery once that write has committed: on the App Store before AppDNA’s other follow-up work (the entitlement sync and the internal billing event), on Google Play after the entitlement sync and before the internal events. It is marked sent only when its delivery records are stored, so it is not lost when the hand-off or the work after it fails: the store redelivers the notification and a webhook still pending is handed off then. This is at-least-once: if the hand-off succeeded but marking it sent did not, the redelivery hands it off again, as a new delivery with a new id — so your endpoint can, rarely, see the same store event twice (for payment.completed, the same data.transaction_id), never none — unless it is still undelivered 24 hours after it was recorded, or every allowed attempt was started and the last one never finished, in which case it is marked failed (see Retry Policy) and can be retried by hand. Purchases that reach AppDNA only through an inbound provider webhook (RevenueCat, Adapty and similar) send no payment.completed.
Changed in the September 2026 server release. payment.completed differs from the previous release in its payload and in which charges send it, and payment.failed and subscription.canceled changed too.payment.completed payload:
  • data.transaction_id was the AppDNA subscription id, the same for every renewal. It is now the store’s transaction id: the App Store transactionId, or the Google Play order id.
  • data.subscription_id is new: the AppDNA subscription id that transaction_id used to carry.
  • data.price is null when no price is known. It was 0.
  • data.currency is null when price is null or the store reported no currency. It was USD when no currency was recorded.
  • For an App Store transaction reported after a later one, product_id, price and currency are that transaction’s own. They were the subscription’s current ones.
Now sent: an App Store resubscribe, an App Store paid offer that starts, restarts or immediately upgrades a subscription, and a Google Play subscription recovering from account hold.No longer sent: a free-trial start, a scheduled App Store downgrade (or any App Store renewal-preference change other than an immediate upgrade), an App Store transaction whose price is 0, a Family Sharing member’s access, a Google Play charge notification processed after Play has moved the subscription to grace period, account hold, paused or expired, and a Google Play renewal or purchase notification processed while the subscription is revoked or refunded in AppDNA.Google Play:
  • A notification whose purchase token Google Play answers with 400, 404 or 410 now sends nothing. It used to be processed from the notification data alone, and could send a webhook only when AppDNA already had the subscription (or the notification was a new purchase for a package name with an active Google Play credential).
  • A notification for a purchase token that belongs to another app, or one AppDNA cannot attribute to a single app (no ?app= on the push endpoint and several apps share the package name), now sends nothing.
  • What a notification answers now depends on the app’s Google Play credential. In each of these cases it used to be processed from the notification data alone, and could send a webhook only when AppDNA already had the subscription (or the notification was a new purchase for a package name with an active Google Play credential):
    • The app has a Google Play credential, but its service-account JSON is missing, invalid or incomplete: 5xx, so Pub/Sub keeps redelivering the notification until the credential is fixed.
    • The push endpoint carries ?app= and that app has no active Google Play credential: 404.
    • The push endpoint has no ?app= and no app’s active Google Play credential has the notification’s package name: 200, and the notification is dropped.
  • payment.completed product_id follows the product of Google Play’s current line item. It used to stay the product already on the subscription: the one the app last verified, or, for a subscription first seen through a Play purchase notification, that notification’s subscriptionId. A Play notification never changed it.
payment.failed:
  • It is no longer sent for an App Store notification about a transaction older than the one the subscription already carries.
  • A Google Play SUBSCRIPTION_REVOKED sends it only when Play confirms the subscription is expired. It was sent for every revoke.
subscription.canceled:
  • It is now sent for the App Store too: DID_CHANGE_RENEWAL_STATUS / AUTO_RENEW_DISABLED. It was Google Play only.
  • It is sent only when Play’s state agrees: canceled, expired, or active with auto-renew off. It was sent for every SUBSCRIPTION_CANCELED.
  • data.cancel_reason can now be canceled. It was always voluntary.
  • data.effective_date is kept from when AppDNA first recorded auto-renew as off. It was the time the notification was processed.
All three billing webhooks now carry data.environment (production or sandbox). A Sandbox App Store purchase used to send payment.completed with nothing telling it apart from a production one.To detect a redelivered webhook, use the event id, which stays the same on every retry of a delivery.

Signature Verification

Every webhook request includes an x-appdna-signature header containing an HMAC-SHA256 signature of the raw request body. Always verify this signature before processing the payload. The signature is computed as:
The header value is hex-encoded and prefixed with sha256=:

Verification Examples

Always use a constant-time comparison function (e.g., crypto.timingSafeEqual, hmac.compare_digest, Rack::Utils.secure_compare) to prevent timing attacks.

Retry Policy

If your endpoint returns a non-2xx status code or does not respond within 30 seconds, AppDNA retries the delivery with exponential backoff: After the initial attempt plus 4 retries (5 attempts total), that delivery is marked as failed. You can view failed deliveries and manually retry them from the dashboard. A delivery whose first attempt never started (the AppDNA process that recorded it stopped before sending it) is sent by the retry schedule once it has waited 5 minutes, and that send is its first attempt; if the process stopped in the middle of the first send, the retry schedule’s send is its second. The attempts toward the 5 are counted like this: the first when the delivery is recorded, each retry when it is scheduled after a failed attempt, and a send that the sending process stopped in the middle of when the retry schedule picks the delivery up again — so such a send still counts, and no delivery is ever sent more than 5 times: once all 5 have started and the last one never finished, the delivery is marked failed. One AppDNA process sends a given attempt: it re-confirms that it still holds the delivery just before sending and records the result only while it still does, so two overlapping retry runs never send the same delivery twice. A test delivery (sent once, never retried) whose one send recorded no answer is marked failed after 5 minutes. The delivery log shows the attempt the delivery is on, or waiting for, as n/5 (a test delivery, sent once and never retried, as 1/1). One that is still pending or waiting to retry 24 hours after it was recorded or last retried by hand is marked failed too, instead of being delivered late. Either way the reason is in its delivery log, and it says which case it was: never sent (Never sent: …), attempted and still waiting to retry (Given up: attempted, and still waiting …), attempted with the last attempt never finished, so there is no answer to show (Given up: attempted, and the last attempt never finished …), or out of attempts (Given up: every allowed attempt was started, … or Given up: no attempt is left, …); retry it by hand to send it (that starts a new 5 attempts and a new 24 hours). After 50 consecutive failures across any events, the webhook endpoint is automatically disabled. You can re-enable the endpoint from the dashboard after fixing the issue.

Best Practices

  1. Respond with 2xx quickly. Return a 200 OK as soon as you receive the payload. Process the event asynchronously in a background job or queue. If your endpoint takes too long to respond, the delivery will be marked as failed and retried.
  2. Verify signatures on every request. Never process a webhook payload without verifying the x-appdna-signature header. This protects you from forged requests.
  3. Handle idempotency. Webhook deliveries can be retried, which means your endpoint may receive the same event more than once. Use the id field to deduplicate events. Store processed event IDs and skip duplicates.
  4. Use HTTPS. AppDNA only delivers webhooks to HTTPS endpoints, in every environment (sandbox and development included); an http:// URL is refused when you save the endpoint. The host must resolve to a public address: a URL that does not (a private or loopback address, an internal host name, a host that does not resolve) is refused when you save the endpoint, with the reason, and the same check runs again before every delivery. A redirect is not followed (it counts as a failed delivery). During development, use a tunneling tool like ngrok, which gives a local endpoint a public HTTPS URL.
  5. Monitor delivery health. Check the webhook delivery logs in the dashboard periodically. If you see a pattern of failures, investigate your endpoint’s availability and response times.
To test webhooks locally during development, use a tunneling tool like ngrok and point your webhook endpoint to the tunnel URL. Sandbox webhooks (adn_test_ environment) are fully functional and isolated from production.