# Guide to AppsFlyer Webhooks: Features and Best Practices

AppsFlyer is a mobile attribution platform. Its Push API streams raw attribution data to your servers as it happens: installs, re-engagements, in-app events and SKAdNetwork (SKAN) postbacks, each with the media source, campaign and device fields you choose. Teams use it to load attribution into their own warehouse, or to start onboarding flows as soon as an attributed install arrives.

This guide covers how Push API endpoints are set up, how to tell message types apart, how AppsFlyer's authentication token works and where it falls short, and how to handle AppsFlyer's short timeout in production.

## What are AppsFlyer webhooks?

AppsFlyer calls its webhooks the Push API. Each app can have its own endpoints, and each endpoint has an HTTP method, a URL, the message types it receives and the fields to include. Messages are flat key-value records, one per event.

There are two attribution frameworks, and they behave differently:

* **AppsFlyer attribution** messages (installs, re-engagements, reinstalls and their in-app events) are sent soon after AppsFlyer records the event, typically within minutes. An app can have up to 6 of these endpoints.
* **SKAN** messages arrive in a daily batch. Installs, redownloads and in-app events are sent the day after AppsFlyer receives the iOS postback, between about 08:00 and 11:00 UTC. Copies of the raw postbacks are sent as they arrive. An app can have 1 to 3 SKAN endpoints.

AppsFlyer doesn't sign messages. You can configure an authentication token, a header name and value per app ID, which AppsFlyer sends with each message.

## AppsFlyer webhook features

| Feature | Details |
| --- | --- |
| Configuration | **Export** > **API Access** > **Push API** > **Add Endpoint** |
| Method | `POST` or `GET`, chosen per endpoint |
| Endpoints per app | 6 for AppsFlyer attribution, 1 to 3 for SKAN |
| Message format | Flat key-value fields. Null and empty fields are never sent |
| Always included | App ID, event name, event time, and IDFA (iOS) or Advertising ID (Android) |
| Authentication | Token header, set per app ID. Not included in **Send Test** messages |
| Signature | None |
| Response required | HTTP `200` within 4 seconds (2 seconds for **Send Test**) |
| Ports and TLS | Ports 80 and 443. TLS 1.1 and TLS 1.2+ |
| Source IPs | AppsFlyer publishes server IPs to allowlist |
| Retries | Not documented. Historical data isn't resent; use Pull API |

## Common events

For AppsFlyer attribution, you identify the message type from three fields, `conversion_type`, `campaign_type` and `event_name`, or from `event_type` if you include it:

| Message type | `conversion_type` | `campaign_type` | `event_name` | `event_type` |
| --- | --- | --- | --- | --- |
| Install | `install` | UA or `organic` | `install` | `install`, `organic-install` |
| Install in-app events | `install` | UA or `organic` | Your event name | `install-in-app-event`, `organic-install-in-app-event` |
| Re-engagement | `re-engagement` | `retargeting` | `re-engagement` | `re-attribution` |
| Re-engagement in-app events | `re-engagement` | `retargeting` | Your event name | `re-engagement-in-app-event` |
| Re-attribution | `reinstall` | `retargeting` | `re-attribution` | `re-attribution` |
| Reinstall | `reinstall` | UA or `organic` | `reinstall` | `reinstall`, `organic-reinstall` |
| Re-attribution in-app events | `reinstall` | `retargeting` | Your event name | `re-attribution-in-app-event` |

AppsFlyer's table gives both re-engagement and re-attribution the `event_type` `re-attribution`, so use `conversion_type` to tell them apart. `event_type` is optional on AppsFlyer-attribution endpoints, so select it in the field list if you want to route on it.

SKAN endpoints receive everything on one endpoint and use `event_type` alone:

| `event_type` | Message |
| --- | --- |
| `skad-install` | An install |
| `skad-re-download` | A redownload (`event_name` is `install`, `skad_redownload` is true) |
| `skad-in-app-event` | An in-app event |
| `skad-postback` | A postback from iOS |
| `skad-postback-copy` | A copy of a postback |

Timestamps are UTC in the form `yyyy-mm-dd hh:mm:ss.sss`, or include an offset such as `+0900` for fields in the app's selected time zone. Because AppsFlyer omits empty fields, a field that is present on one message may be missing on the next, so parse every optional field defensively.

## Setting up AppsFlyer webhooks

1. In AppsFlyer, open **Export** > **API Access** and scroll to **Push API**. For SKAN, select **SKAdNetwork** as the attributing entity first.
2. Click **Add Endpoint** and choose `POST`. AppsFlyer also offers `GET`, but doesn't document how `GET` messages are encoded.
3. Enter the endpoint URL, select the message types, then select the fields. The most common fields are preselected. Selecting all fields doesn't add fields AppsFlyer introduces later, so revisit the list from time to time.
4. If you want in-app event messages, pick the events (up to 52, or all). An event only appears in the list after it has been recorded at least once.
5. Click **Save**. Data starts flowing straight away.
6. Configure the authentication token for the app: a header name and a token value. Store the value, for example as `APPSFLYER_AUTH_TOKEN`.
7. Click **Send Test** and check that your endpoint received the message. The test message doesn't carry the authentication token, so a receiver that enforces the token will reject it.

If AppsFlyer says the URL "is not safe", its docs tell you to contact AppsFlyer support.

## Securing AppsFlyer webhooks

Compare the token header with the value you configured, in constant time. AppsFlyer's setup page says you define the header name, but its FAQ says only the `Authorization` header is supported for tokens, so check which header your messages carry and compare the full value:

```javascript
const crypto = require('crypto');
const express = require('express');

const AUTH_HEADER_NAME = (process.env.APPSFLYER_AUTH_HEADER_NAME || 'authorization').toLowerCase();

function verifyAppsflyerToken(headerValue, expectedToken) {
  if (!headerValue || !expectedToken) return false;
  const received = Buffer.from(headerValue, 'utf8');
  const expected = Buffer.from(expectedToken, 'utf8');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const app = express();

app.post('/webhooks/appsflyer', express.raw({ type: '*/*' }), (req, res) => {
  const token = process.env.APPSFLYER_AUTH_TOKEN;
  if (!token) return res.sendStatus(500); // fail closed

  if (!verifyAppsflyerToken(req.get(AUTH_HEADER_NAME), token)) return res.sendStatus(401);

  const message = JSON.parse(req.body.toString('utf8'));
  // Route on message.event_type, or conversion_type + campaign_type + event_name, then process async.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hmac
import os

AUTH_HEADER_NAME = os.environ.get("APPSFLYER_AUTH_HEADER_NAME", "Authorization")

def verify_appsflyer_token(header_value, expected_token: str) -> bool:
    if not header_value or not expected_token:
        return False
    return hmac.compare_digest(header_value.encode("utf-8"), expected_token.encode("utf-8"))

# In FastAPI:
# verify_appsflyer_token(request.headers.get(AUTH_HEADER_NAME), os.environ["APPSFLYER_AUTH_TOKEN"])

```

Tokens are set per app ID. If you stream several apps to one endpoint, use the same token for each or look up the expected token by `app_id` after parsing.

> **Make AppsFlyer Push API production-ready.** [Hookdeck Event Gateway](/event-gateway) checks AppsFlyer's token header, answers inside the 4-second timeout, and stores every attribution message so you can replay it, which AppsFlyer itself won't do.

In Hookdeck, create an AppsFlyer source and enter the header name and the exact header value AppsFlyer sends, including any prefix such as `Bearer `. The token is optional in AppsFlyer, so leave verification off if you didn't set one. Remember that **Send Test** messages don't include the token, so they fail verification when it is on. If you choose `GET` for an endpoint, allow `GET` on the Hookdeck source as well.

## AppsFlyer webhook limitations and pain points

### A four-second timeout and no documented retries

**The Problem:** Attribution data can be lost when your endpoint is slow or down.

**Why It Happens:** AppsFlyer treats anything other than a `200` within 4 seconds as a failure, and doesn't document retrying. Push API doesn't resend historical data.

**Workarounds:**

* Return `200` as soon as the message is stored and process it later.
* Backfill gaps with AppsFlyer's Pull API, or Data Locker for SKAN.

**How Hookdeck Can Help:** Hookdeck answers AppsFlyer immediately, stores each message, and retries delivery to your service until it succeeds, so a slow database or a deploy doesn't drop installs.

### A token, not a signature

**The Problem:** The token proves the sender knows a secret, but doesn't protect the message body.

**Why It Happens:** AppsFlyer sends a static header with each message and computes nothing over the payload. Test messages don't include it at all.

**Workarounds:**

* Use HTTPS and a long random token.
* Allowlist AppsFlyer's server IPs as a second check.
* Treat requests without the token as tests, and keep them out of production data.

**How Hookdeck Can Help:** Only Hookdeck needs the AppsFlyer token. Your services receive messages from Hookdeck and don't need to store or compare it.

### Duplicate retargeting purchases

**The Problem:** One purchase shows up twice.

**Why It Happens:** If you enable both install in-app events and retargeting in-app events, a purchase made during the re-engagement window is sent twice so revenue can be attributed to both the UA and the retargeting media source.

**Workarounds:**

* Decide which attribution you want to count and filter on `conversion_type` and `campaign_type`.
* Deduplicate on fields that identify the event, such as AppsFlyer ID, event name and event time.

**How Hookdeck Can Help:** Hookdeck filters can match on body fields such as `campaign_type`, so you can route retargeting in-app events to a separate destination or drop them.

### Messages with missing fields

**The Problem:** Parsers fail when a field they expect isn't there.

**Why It Happens:** AppsFlyer never sends null or empty fields, and the set of fields depends on what you select per endpoint.

**Workarounds:**

* Treat every non-mandatory field as optional.
* Select `event_type` explicitly if you route on it.

**How Hookdeck Can Help:** Hookdeck keeps every message it receives, so you can inspect the actual field set and replay messages after fixing a parser.

## Best practices

### Use separate endpoints by message type

AppsFlyer allows up to 6 endpoints per app. Sending installs and in-app events to different endpoints keeps handlers simple and limits the effect of a failure.

### Respect AppsFlyer's privacy caution

AppsFlyer warns against using Push API to send AppsFlyer-attributed data to third parties, because of privacy rules such as CCPA and media source terms. The caution doesn't apply to SKAN data.

### Store the raw message

Keep the full message before transforming it. Attribution data is hard to recover later, and Push API won't resend it.

### Watch for CloudFront 421 errors

If your endpoint is behind Amazon CloudFront and messages go missing, AppsFlyer says to check whether CloudFront is rejecting them with status `421`.

## Conclusion

AppsFlyer's Push API streams attribution and SKAN events as flat records, protected by a token header rather than a signature, with a 4-second timeout and no documented retries. Check the token in constant time, route on `event_type` or the three type fields, parse optional fields defensively, and acknowledge quickly.

[Hookdeck Event Gateway](https://hookdeck.com) answers inside AppsFlyer's timeout and keeps every message for retry and replay.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle AppsFlyer webhooks reliably in minutes.