> For the complete documentation index, see [llms.txt](https://docs.fuul.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.fuul.xyz/developer-guide/webhooks.md).

# Webhooks

Webhooks send real-time HTTP notifications to your server when rewards are distributed. Use them to sync payout data with your own systems — update balances, notify users, or trigger downstream workflows.

## When are webhooks triggered?

Webhooks fire when qualifying movements are created:

| Movement type      | Movement reason                         |
| ------------------ | --------------------------------------- |
| `point`            | `end_user_payout` or `affiliate_payout` |
| `onchain-currency` | `end_user_payout` or `affiliate_payout` |
| `airdrop`          | `end_user_payout` or `affiliate_payout` |

{% hint style="warning" %}
Webhooks fire for **all movement statuses**, including `rejected`. Always check the `status` field before processing — see [Payload fields](#payload-fields) below.
{% endhint %}

## Webhook payload

Each webhook delivers a `reward.earned` event as an HTTP POST with a JSON body:

```json
{
  "event_type": "reward.earned",
  "movement": {
    "type": "point",
    "reason": "end_user_payout",
    "status": "accepted",
    "status_details": null,
    "user_identifier": "0x1234...",
    "user_identifier_type": "evm_address",
    "amount": "1000",
    "currency": {
      "name": "POINT"
    },
    "conversion_name": "Trading Volume",
    "created_at": "2025-06-15T14:30:00Z"
  }
}
```

### Payload fields

| Field                           | Description                                                                                   |
| ------------------------------- | --------------------------------------------------------------------------------------------- |
| `event_type`                    | Always `reward.earned`                                                                        |
| `movement.type`                 | `point`, `onchain-currency`, or `airdrop`                                                     |
| `movement.reason`               | `end_user_payout` or `affiliate_payout`                                                       |
| `movement.status`               | `accepted`, `rejected`, or `pending`                                                          |
| `movement.status_details`       | Rejection reason (e.g., wallet screening failure). `null` when accepted                       |
| `movement.user_identifier`      | Wallet address or identifier of the recipient                                                 |
| `movement.user_identifier_type` | `evm_address`, `solana_address`, `sui_address`, `xrpl_address`, `stellar_address`, or `email` |
| `movement.amount`               | Reward amount (token amounts in smallest unit)                                                |
| `movement.currency`             | Currency details (name, address, chain ID for onchain tokens)                                 |
| `movement.conversion_name`      | Name of the conversion that triggered the payout                                              |
| `movement.created_at`           | ISO 8601 timestamp                                                                            |

## Handling webhooks

Your endpoint should receive the POST request, process the event, and return a `200` status code:

```typescript
import express from 'express';

const app = express();
app.use(express.json());

app.post('/webhooks/fuul', (req, res) => {
  const event = req.body;

  if (event.event_type === 'reward.earned') {
    const { status, status_details, type, amount, user_identifier } = event.movement;

    if (status === 'accepted') {
      // Process the reward — update your database, notify the user, etc.
      console.log(`Reward: ${amount} ${type} → ${user_identifier}`);
    } else if (status === 'rejected') {
      // Log rejected movements for investigation
      console.warn(`Rejected: ${user_identifier} — ${status_details}`);
    }
  }

  // Always return 200 quickly
  res.status(200).send('OK');
});
```

**cURL test** (simulate a webhook delivery locally):

```bash
curl -X POST http://localhost:3000/webhooks/fuul \
  -H "Content-Type: application/json" \
  -d '{
    "event_type": "reward.earned",
    "movement": {
      "type": "point",
      "reason": "end_user_payout",
      "status": "accepted",
      "status_details": null,
      "user_identifier": "0x1234...",
      "user_identifier_type": "evm_address",
      "amount": "500",
      "currency": { "name": "POINT" },
      "conversion_name": "Referral Signup",
      "created_at": "2025-06-15T14:30:00Z"
    }
  }'
```

## Retries

Failed deliveries (non-2xx responses or timeouts) are retried automatically. To avoid missed events:

* Return a `2xx` status code as quickly as possible
* If processing takes time, acknowledge the request immediately and handle the event asynchronously

## Best practices

| Practice                                | Why                                                                      |
| --------------------------------------- | ------------------------------------------------------------------------ |
| Return `200` immediately, process async | Prevents timeouts and unnecessary retries                                |
| Handle duplicates idempotently          | Retries may deliver the same event more than once                        |
| Check the `status` field                | Not all movements are `accepted` — rejected movements are also delivered |
| Log `status_details` for rejections     | Contains the reason (e.g., wallet screening failure) for debugging       |
| Use HTTPS for your endpoint             | Protects payload data in transit                                         |

## Managing webhooks

Webhooks are configured via the REST API:

| Endpoint                       | Method | Description                 | Reference                                                              |
| ------------------------------ | ------ | --------------------------- | ---------------------------------------------------------------------- |
| `/v1/webhooks`                 | POST   | Create a webhook endpoint   | [View](https://fuul.readme.io/reference/post_v1-webhooks)              |
| `/v1/webhooks`                 | GET    | List your webhook endpoints | [View](https://fuul.readme.io/reference/get_v1-webhooks)               |
| `/v1/webhooks/{id}`            | GET    | Get a specific webhook      | [View](https://fuul.readme.io/reference/get_v1-webhooks-id)            |
| `/v1/webhooks/{id}`            | PATCH  | Update a webhook endpoint   | [View](https://fuul.readme.io/reference/patch_v1-webhooks-id)          |
| `/v1/webhooks/{id}`            | DELETE | Delete a webhook endpoint   | [View](https://fuul.readme.io/reference/delete_v1-webhooks-id)         |
| `/v1/webhooks/{id}/deliveries` | GET    | View delivery history       | [View](https://fuul.readme.io/reference/get_v1-webhooks-id-deliveries) |

{% hint style="info" %}
Webhooks are currently only configurable via the REST API — webapp configuration is not yet available.
{% endhint %}
