# 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`, 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 %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.fuul.xyz/developer-guide/webhooks.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
