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

# Webhooks

Webhooks let your application receive real-time notifications when events happen on the Fluz platform. Instead of polling for changes, you register an HTTPS endpoint and Fluz pushes event data to you the moment something occurs — a virtual card transaction, a declined purchase, a completed deposit, a user linking their account via OAuth, and more.

<Note>
  This page covers all webhook events, which span multiple platform areas (transaction activity, deposits, widget flows, and OAuth account linking) rather than belonging to any single feature.
</Note>

Webhooks work across all application types on the Fluz platform: private apps operating on your own account, public OAuth apps acting on behalf of other users via API, and embedded widget apps.

## How it works

1. You register a webhook URL on your application in the Developer Portal.
2. You select which event types to listen for (or subscribe to all of them).
3. When a matching event occurs, Fluz sends an HTTP `POST` to your URL with a signed JSON payload.
4. Your server verifies the signature, acknowledges with a `2xx`, and processes the event.

```mermaid theme={null}
sequenceDiagram
    participant Fluz as Fluz
    participant You as Your Endpoint
 
    Fluz->>You: HTTP POST with signed JSON payload
    You-->>Fluz: 200 OK
```

If your endpoint is unavailable or returns an error, Fluz retries up to **5 times** with exponential backoff before giving up on that delivery.

## Webhooks by app type

How events are routed to your app depends on your application model.

### Private apps — your own account

Webhooks fire for activity on **your own account**. Any transaction, decline, or deposit on accounts you own triggers webhooks registered on your app.

**Common use cases:** notifications when a virtual card transaction is authorized or settled; real-time alerting on declines; monitoring deposits and balance changes on your spend accounts.

### Public apps (OAuth) — acting on behalf of users

Webhooks fire for activity on **accounts that have authorized your app**. When a user grants your app access via OAuth, their events are routed to your registered webhooks — provided the user's OAuth grant includes the required scopes. This works whether the user interacts through your direct API integration or an embedded widget; the key requirement is an OAuth relationship between the user and your app.

**Common use cases:** knowing when a user has linked (or re-linked) their account to your app; monitoring virtual card spend across connected accounts; real-time decline notifications; tracking deposit completions; receiving KYC status updates.

### Widget apps

Widget apps are a specialized public app. Events route the same way (based on the OAuth relationship), and widget apps additionally support widget-specific events like transfer completions and gift card purchases.

## Event types

Fluz only delivers an event if your application — **and**, for public/OAuth apps, the individual user's OAuth grant — holds the required scopes. Events with no required scopes (such as `OAUTH_USER_LINKED`) are delivered to any subscribed public/OAuth app.

### Transaction events

Cover the full transaction lifecycle. Apply to **all app types**.

| Event                 | Description                                                                     | Required scopes                  |
| --------------------- | ------------------------------------------------------------------------------- | -------------------------------- |
| `TRANSACTION_CREATE`  | A new transaction was created (e.g., virtual card purchase, deposit, transfer). | `LIST_PAYMENT`, `LIST_PURCHASES` |
| `TRANSACTION_UPDATE`  | A transaction's status or amount changed (e.g., settled, adjusted).             | `LIST_PAYMENT`, `LIST_PURCHASES` |
| `TRANSACTION_DECLINE` | A transaction was declined (e.g., insufficient funds, card controls).           | `LIST_PAYMENT`, `LIST_PURCHASES` |

### Deposit events

| Event              | Description                                                   | Required scopes |
| ------------------ | ------------------------------------------------------------- | --------------- |
| `DEPOSIT_COMPLETE` | A deposit from a funding source to a spend account completed. | `MAKE_DEPOSIT`  |

### OAuth events

Apply to **public/OAuth and widget apps**.

| Event               | Description                                                                                                                                       | Required scopes |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- |
| `OAUTH_USER_LINKED` | A user completed OAuth linking to your application. Fires on both the initial link and subsequent updates (e.g., when the granted scopes change). | *None*          |

`OAUTH_USER_LINKED` is delivered only to public/OAuth app types — private-app subscribers do not receive it. Because it has no scope requirement, you receive it for every user who links or re-links to your app, regardless of which scopes they grant. Use it to provision or update your local record of the connected user, capture the granted scope set, and map your own user identifier via `externalReferenceId`.

### Widget-specific events

Apply to widget and OAuth integrations where users interact through Fluz-embedded flows.

| Event                       | Description                                       | Required scopes                |
| --------------------------- | ------------------------------------------------- | ------------------------------ |
| `WIDGET_KYC_INITIATION`     | A user initiated identity verification (KYC).     | `VERIFY_KYC`                   |
| `WIDGET_DEPOSIT_COMPLETE`   | A transfer from a customer to your app completed. | `MAKE_PAYOUT_TRANSFER_SEND`    |
| `WIDGET_WITHDRAW_COMPLETE`  | A transfer from your app to a customer completed. | `MAKE_PAYOUT_TRANSFER_RECEIVE` |
| `WIDGET_PURCHASE_GIFT_CARD` | A gift card purchase completed.                   | `PURCHASE_GIFTCARD`            |

> 📘 **Naming note:** `WIDGET_DEPOSIT_COMPLETE` and `WIDGET_WITHDRAW_COMPLETE` describe customer↔app *transfers*; the `DEPOSIT`/`WITHDRAW` wording is historical. Treat "deposit" as "customer → app" and "withdraw" as "app → customer."

## Setting up webhooks

### 1. Open the Developer Portal

Go to the [Developer Portal](https://app.fluz.app/for-developers) and select your application.

### 2. Open the Webhooks section

* **OAuth apps:** **OAuth** tab → **Webhook URLs**.
* **Widget apps:** **Widget** tab → **Webhook URLs**.
* **API / private apps:** the **Webhook URLs** section in your app settings.

### 3. Add a webhook URL

Click **Add new URL** and enter your HTTPS endpoint (e.g., `https://api.yourapp.com/webhooks/fluz`).

### 4. Select events

Choose the event types you want to receive.

### 5. Save

Click **Create Webhook**. Your endpoint begins receiving events immediately.

## Managing webhooks

* **Multiple endpoints** — you can register more than one webhook URL per application.
* **Change subscribed events** — delete the webhook and recreate it with the new event selection.
* **Remove a webhook** — click **Remove** next to it. The webhook is archived immediately and stops receiving events.

## Receiving webhooks

### Request format

Every webhook is delivered as an HTTP `POST` with these headers:

| Header             | Description                                                                 |
| ------------------ | --------------------------------------------------------------------------- |
| `Content-Type`     | Always `application/json`.                                                  |
| `X-HMAC-Signature` | HMAC-SHA256 signature of the raw JSON body, signed with your app's API key. |
| `X-Event-ID`       | Unique UUID for this event — use it for deduplication.                      |

The body is a JSON object, and every payload includes an `eventType` field identifying the event.

### Endpoint requirements

* **HTTPS only** — plain HTTP endpoints are rejected at registration time.
* **Publicly accessible** and able to accept `POST` requests.
* **Respond with a `2xx` within 30 seconds.** Non-`2xx` responses or timeouts trigger retries.
* **Verify the HMAC signature** on every request.

## Verifying signatures

Every delivery includes an `X-HMAC-Signature` header — an HMAC-SHA256 hash of the **raw** JSON body, signed with your application's **API key**. Always verify it before trusting a payload.

> ⚠️ **Verify against the raw request body.** Compute the HMAC over the exact bytes Fluz sent — do **not** re-serialize the parsed JSON. Re-stringifying can reorder keys or change whitespace and cause valid signatures to fail. The examples below capture the raw body for this reason.

### Node.js (Express)

```javascript theme={null}
const express = require('express');
const crypto = require('crypto');
const app = express();
 
// Capture the raw body so the HMAC is computed over the exact bytes Fluz sent.
app.use('/webhooks/fluz', express.raw({ type: 'application/json' }));
 
function verifyFluzWebhook(rawBody, signature, apiKey) {
  const expected = crypto.createHmac('sha256', apiKey).update(rawBody).digest('hex');
  const received = Buffer.from(signature || '', 'hex');
  const computed = Buffer.from(expected, 'hex');
  return received.length === computed.length &&
         crypto.timingSafeEqual(received, computed);
}
 
app.post('/webhooks/fluz', (req, res) => {
  const signature = req.headers['x-hmac-signature'];
  const eventId = req.headers['x-event-id'];
 
  // req.body is a Buffer because of express.raw above
  if (!verifyFluzWebhook(req.body, signature, process.env.FLUZ_API_KEY)) {
    return res.status(401).send('Invalid signature');
  }
 
  // Acknowledge immediately, then process asynchronously
  res.status(200).send('OK');
 
  const event = JSON.parse(req.body.toString('utf8'));
  handleEvent(eventId, event).catch(err =>
    console.error('Webhook processing error:', err)
  );
});
```

### Python (Flask)

```python theme={null}
import hmac, hashlib
from flask import Flask, request
 
app = Flask(__name__)
 
def verify_fluz_webhook(raw_body: bytes, signature: str, api_key: str) -> bool:
    expected = hmac.new(api_key.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature or "")
 
@app.post("/webhooks/fluz")
def fluz_webhook():
    signature = request.headers.get("X-HMAC-Signature")
    event_id = request.headers.get("X-Event-ID")
 
    # request.get_data() returns the raw bytes — do NOT use request.json for verification
    if not verify_fluz_webhook(request.get_data(), signature, FLUZ_API_KEY):
        return "Invalid signature", 401
 
    enqueue(event_id, request.get_json())  # process asynchronously
    return "OK", 200
```

## Responding to webhooks

Your endpoint **must**:

* Respond with a `2xx` status within 30 seconds.
* Return quickly — acknowledge first, then process asynchronously.
* Be reachable over HTTPS. Your endpoint **must not**:
* Respond with redirects (`3xx`).
* Respond with `4xx`/`5xx` for valid webhooks (this triggers retries).

## Retry policy

| Behavior               | Detail                                      |
| ---------------------- | ------------------------------------------- |
| Max attempts           | 5                                           |
| Schedule               | Exponential backoff                         |
| Trigger                | Any non-`2xx` response or timeout           |
| Timeout                | 30 seconds per attempt                      |
| After all retries fail | Delivery is abandoned; no further attempts. |

New events resume delivery automatically once your endpoint recovers. To re-send events whose retries were already exhausted, contact support with the relevant `X-Event-ID`.

## Idempotency & ordering

Webhooks may be delivered **more than once**, and **delivery order is not guaranteed**.

* **Deduplicate** using the `X-Event-ID` header. Persist processed IDs (Redis or a database in production) and skip events you've already handled.
* **Order by data, not arrival.** If sequence matters, order by payload timestamps (`createdAt`, `updatedAt`, `transactionDateTime`) and event IDs.

```javascript theme={null}
const seen = new Set(); // use Redis or a database in production
 
async function handleEvent(eventId, event) {
  if (seen.has(eventId)) return;  // already processed — skip
  seen.add(eventId);
 
  switch (event.eventType) {
    case 'TRANSACTION_CREATE':    await onTransactionCreated(event); break;
    case 'TRANSACTION_UPDATE':    await onTransactionUpdated(event); break;
    case 'TRANSACTION_DECLINE':   await onTransactionDeclined(event); break;
    case 'DEPOSIT_COMPLETE':      await onDepositComplete(event); break;
    case 'OAUTH_USER_LINKED':     await onUserLinked(event); break;
    case 'WIDGET_KYC_INITIATION': await onKycInitiated(event); break;
    // ...handle remaining widget events
  }
}
```

## Identifying the app and user

* **User:** `userId` is the Fluz user ID. For OAuth/widget events, `externalReferenceId` maps to *your* user identifier from the OAuth flow.
* **App:** transaction payloads include `connectedAppId` and `connectedAppName`. If you route multiple apps to one endpoint, branch on `connectedAppId`. For `OAUTH_USER_LINKED`, the app is identified by `appId`.

## Payload reference

Every payload includes an `eventType`. Field availability can vary by event; handlers should ignore unrecognized fields for forward compatibility.

> 📘 **A note on `status` values.** For created/updated transactions the `status` field takes one of `PENDING`, `SETTLED`, or `FAILED`. Declined transactions carry a `status` of `DECLINED` (or `FAILED`). Earlier drafts of this page showed `COMPLETED` as a status — that value is not emitted; use `SETTLED` to detect a finalized transaction.

### Transaction created (`TRANSACTION_CREATE`)

Fired for any new transaction — virtual card purchases, deposits, transfers, and more.

```json theme={null}
{
  "eventType": "TRANSACTION_CREATE",
  "recordId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "transactionId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "userId": "550e8400-e29b-41d4-a716-446655440000",
  "transactionType": "PURCHASE",
  "amount": 42.99,
  "destination": "Coffee Shop",
  "source": "Virtual Card",
  "status": "PENDING",
  "channel": "VIRTUAL_CARD",
  "connectedAppId": "your-app-id",
  "connectedAppName": "Your App",
  "createdAt": "2025-01-15T10:30:00.000Z",
  "merchantName": "Coffee Shop",
  "merchantCity": "New York",
  "merchantState": "NY",
  "merchantCountry": "US",
  "cardLastFour": "1234",
  "virtualCardId": "vc-uuid-here",
  "cashbackRate": 0.05
}
```

| Field                                                                 | Type              | Description                                                                                                            |
| --------------------------------------------------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `eventType`                                                           | String            | Always `TRANSACTION_CREATE`.                                                                                           |
| `recordId`                                                            | String (UUID)     | Unique webhook event record ID.                                                                                        |
| `transactionId`                                                       | String (UUID)     | The transaction ID.                                                                                                    |
| `accountId`                                                           | String (UUID)     | The Fluz account ID.                                                                                                   |
| `userId`                                                              | String (UUID)     | The Fluz user ID. May be absent on some system-generated transactions.                                                 |
| `transactionType`                                                     | String            | Transaction type, e.g. `PURCHASE`, `GIFT_CARD_PURCHASE`, `DEPOSIT`, `WITHDRAWAL`, `TRANSFER`. Treat as an open string. |
| `amount`                                                              | Number            | Transaction amount in USD.                                                                                             |
| `destination`                                                         | String            | Where funds went (e.g., merchant name).                                                                                |
| `source`                                                              | String            | Funding source (e.g., `Virtual Card`).                                                                                 |
| `status`                                                              | String (enum)     | One of `PENDING`, `SETTLED`, `FAILED`.                                                                                 |
| `channel`                                                             | String            | Origin/channel of the transaction (e.g., `VIRTUAL_CARD`). Treat as an open string.                                     |
| `connectedAppId`                                                      | String            | The app the event is routed to. May be absent for private-app activity.                                                |
| `connectedAppName`                                                    | String            | Display name of the connected app.                                                                                     |
| `createdAt`                                                           | String (ISO 8601) | When the transaction was created.                                                                                      |
| `merchantName` / `merchantCity` / `merchantState` / `merchantCountry` | String            | Merchant location details.                                                                                             |
| `cardLastFour`                                                        | String            | Last four digits of the card used.                                                                                     |
| `virtualCardId`                                                       | String (UUID)     | The virtual card ID, if applicable.                                                                                    |
| `cashbackRate`                                                        | Number            | Decimal cashback rate (e.g., `0.05` = 5%).                                                                             |

### Transaction updated (`TRANSACTION_UPDATE`)

Fired when a transaction's status or details change — for example, when a pending authorization settles.

```json theme={null}
{
  "eventType": "TRANSACTION_UPDATE",
  "recordId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "transactionId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "userId": "550e8400-e29b-41d4-a716-446655440000",
  "transactionType": "PURCHASE",
  "amount": 42.99,
  "status": "SETTLED",
  "connectedAppId": "your-app-id",
  "connectedAppName": "Your App",
  "createdAt": "2025-01-15T10:30:00.000Z",
  "updatedAt": "2025-01-16T08:00:00.000Z"
}
```

Fields match `TRANSACTION_CREATE` where present, plus `updatedAt` (ISO 8601) marking when the change occurred. `userId`, `connectedAppId`, and `connectedAppName` are carried through the same way as on `TRANSACTION_CREATE`, so you can identify the user and app consistently across the lifecycle. A `status` transition to `SETTLED` is the signal that a previously pending transaction has finalized.

### Transaction declined (`TRANSACTION_DECLINE`)

Fired when a transaction is declined. Includes structured decline reasons your app can act on.

```json theme={null}
{
  "eventType": "TRANSACTION_DECLINE",
  "transactionId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "transactionType": "PURCHASE",
  "amount": 500.00,
  "fluzAmount": 500.00,
  "externalFundingAmount": 0,
  "currency": "USD",
  "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "userId": "550e8400-e29b-41d4-a716-446655440000",
  "status": "DECLINED",
  "merchantName": "Electronics Store",
  "cardLastFour": "1234",
  "virtualCardId": "vc-uuid-here",
  "isCashBalanceUsed": true,
  "isPrepaymentBalanceUsed": false,
  "isRewardsBalanceUsed": false,
  "isReserveBalanceUsed": false,
  "transactionDateTime": "2025-01-15T10:30:00.000Z",
  "declineTitle": "Transaction Declined",
  "declineReason": "Insufficient funds",
  "declineDescription": "Your account balance was not sufficient for this transaction.",
  "declineCategory": "Balance"
}
```

| Field                                                                                             | Type              | Description                                                                                                                                                                                                                          |
| ------------------------------------------------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `amount`                                                                                          | Number            | Total attempted amount.                                                                                                                                                                                                              |
| `fluzAmount`                                                                                      | Number            | Portion drawn from Fluz balance.                                                                                                                                                                                                     |
| `externalFundingAmount`                                                                           | Number            | Portion drawn from external funding.                                                                                                                                                                                                 |
| `currency`                                                                                        | String            | ISO currency code (e.g., `USD`).                                                                                                                                                                                                     |
| `status`                                                                                          | String (enum)     | One of `DECLINED`, `FAILED`.                                                                                                                                                                                                         |
| `userId`                                                                                          | String (UUID)     | The Fluz user ID. May be absent on some system-generated transactions.                                                                                                                                                               |
| `isCashBalanceUsed` / `isPrepaymentBalanceUsed` / `isRewardsBalanceUsed` / `isReserveBalanceUsed` | Boolean           | Which balance type(s) the attempt drew from.                                                                                                                                                                                         |
| `transactionDateTime`                                                                             | String (ISO 8601) | When the decline occurred.                                                                                                                                                                                                           |
| `declineTitle`                                                                                    | String            | Short, user-facing title.                                                                                                                                                                                                            |
| `declineReason`                                                                                   | String            | Short reason (e.g., `Insufficient funds`).                                                                                                                                                                                           |
| `declineDescription`                                                                              | String            | Longer, user-facing explanation.                                                                                                                                                                                                     |
| `declineCtaText`                                                                                  | String            | Suggested resolution action, when available (e.g., `Add money`).                                                                                                                                                                     |
| `declineCategory`                                                                                 | String            | High-level decline grouping. Common values include `Balance`, `Limit`, `Validation`, `Virtual Card`, and `Acquiring`. Treat as an open string and see [Decline Codes](/features/decline-codes) for the full set and per-code detail. |

Additional optional fields may be present depending on the transaction (e.g. `merchantId`, `merchantCity`, `merchantState`, `merchantCountry`, `cardDisplayName`, `virtualCardProgram`, `channel`, `bankAccountNickname`, `bankAccountLastFour`). Ignore any you don't use.

### Deposit complete (`DEPOSIT_COMPLETE`)

Fired when a deposit from a funding source to a spend account completes.

```json theme={null}
{
  "eventType": "DEPOSIT_COMPLETE",
  "userId": "550e8400-e29b-41d4-a716-446655440000",
  "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "externalReferenceId": "your-reference-123",
  "amount": 50.00
}
```

### OAuth user linked (`OAUTH_USER_LINKED`)

Fired when a user completes OAuth linking to your application — on both the initial link and on subsequent updates (for example, when the user re-authorizes with a different set of scopes). Public/OAuth and widget apps only. No scope is required to receive this event.

```json theme={null}
{
  "eventType": "OAUTH_USER_LINKED",
  "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "userId": "550e8400-e29b-41d4-a716-446655440000",
  "appId": "55555555-5555-4555-8555-555555555555",
  "externalReferenceId": "your-reference-123",
  "scopes": [
    "LIST_PAYMENT",
    "MAKE_DEPOSIT"
  ]
}
```

| Field                 | Type          | Description                                                                                                                                           |
| --------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `eventType`           | String        | Always `OAUTH_USER_LINKED`.                                                                                                                           |
| `accountId`           | String (UUID) | The Fluz account ID that linked to your app.                                                                                                          |
| `userId`              | String (UUID) | The Fluz user ID that linked to your app.                                                                                                             |
| `appId`               | String (UUID) | Your application's ID.                                                                                                                                |
| `externalReferenceId` | String        | *Optional.* Your user identifier passed through the OAuth flow. Present for widget app types and any flow that supplied one; may be absent otherwise. |
| `scopes`              | String\[]     | The distinct scope keys granted by this user (e.g. `LIST_PAYMENT`, `MAKE_DEPOSIT`).                                                                   |

Because `OAUTH_USER_LINKED` fires again on re-link/updates, treat it as an upsert: create the connected user on first receipt, and refresh the stored scope set on later deliveries.

### KYC initiation (`WIDGET_KYC_INITIATION`)

Fired when a user begins identity verification. Public/widget apps only.

```json theme={null}
{
  "eventType": "WIDGET_KYC_INITIATION",
  "userId": "550e8400-e29b-41d4-a716-446655440000",
  "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "externalReferenceId": "your-reference-123"
}
```

### Transfer completed — customer to app (`WIDGET_DEPOSIT_COMPLETE`)

Fired when a user transfers funds to your application.

```json theme={null}
{
  "eventType": "WIDGET_DEPOSIT_COMPLETE",
  "userId": "550e8400-e29b-41d4-a716-446655440000",
  "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "externalReferenceId": "your-reference-123",
  "amount": 25.50
}
```

### Transfer completed — app to customer (`WIDGET_WITHDRAW_COMPLETE`)

Fired when your application transfers funds to a user.

```json theme={null}
{
  "eventType": "WIDGET_WITHDRAW_COMPLETE",
  "userId": "550e8400-e29b-41d4-a716-446655440000",
  "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "externalReferenceId": "your-reference-123",
  "amount": 15.00
}
```

### Gift card purchase (`WIDGET_PURCHASE_GIFT_CARD`)

Fired when a gift card purchase completes through the widget.

```json theme={null}
{
  "eventType": "WIDGET_PURCHASE_GIFT_CARD",
  "userId": "550e8400-e29b-41d4-a716-446655440000",
  "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "externalReferenceId": "your-reference-123",
  "amount": 25.00
}
```

**Common widget payload fields:** `userId` (Fluz user ID), `accountId` (Fluz account ID), `externalReferenceId` (your user identifier from the OAuth flow), and `amount` where applicable.

## Best practices

* **Respond fast, process later.** Return `200` immediately and handle the event asynchronously to avoid timeouts and unnecessary retries.
* **Verify every request.** Validate `X-HMAC-Signature` against the raw body using your API key before processing.
* **Deduplicate with event IDs.** Track `X-Event-ID` to handle retried/duplicate deliveries.
* **Treat enum-like fields as open strings.** New `transactionType`, `channel`, and `declineCategory` values can appear over time; branch on the values you care about and tolerate unknowns.
* **Upsert on `OAUTH_USER_LINKED`.** It can fire more than once per user; refresh the stored scope set each time rather than assuming first-link-only semantics.
* **Accept unknown fields.** Payloads may gain fields over time; ignore unrecognized ones rather than failing.
* **Monitor your endpoint.** Alert on repeated non-`2xx` responses — after 5 failed attempts an event's delivery is abandoned.

## Troubleshooting

| Symptom                                    | Likely cause                                          | Fix                                                                                                                |
| ------------------------------------------ | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Not receiving any webhooks                 | Endpoint unreachable or returning errors              | Confirm the URL is correct, publicly accessible over HTTPS, and returns `2xx`.                                     |
| Missing certain event types                | App lacks the required scopes                         | Ensure your app holds the scopes listed for that event.                                                            |
| Public app not receiving events for a user | User hasn't authorized the required scopes            | Confirm the user's OAuth grant includes the needed scopes.                                                         |
| Not receiving `OAUTH_USER_LINKED`          | Subscribed on a private app, or not subscribed at all | This event is public/OAuth only. Subscribe to it (or use catch-all) on an OAuth or widget app.                     |
| Signature verification fails               | Wrong API key, or verifying a re-serialized body      | Use your app's API key and verify against the raw request body.                                                    |
| Duplicate deliveries                       | Retry after a timeout                                 | Implement idempotency using `X-Event-ID`.                                                                          |
| Events stopped coming                      | 5 consecutive failures exhausted retries              | Fix your endpoint; new events resume automatically. Contact support to replay events whose retries were exhausted. |

## Need help?

* **Technical issues:** check your endpoint logs and contact support with the `X-Event-ID`.
* **Scope questions:** see [Application Scopes](/fluz-dashboard/application-scopes) and [Decline Codes](/features/decline-codes).
