> For the complete documentation index, see [llms.txt](https://synthesys-2.gitbook.io/synthesys-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://synthesys-2.gitbook.io/synthesys-docs/network-overview/api-reference/webhooks-wip.md).

# Webhooks \[WIP]

Webhooks are how the Synthesys network notifies your backend about lifecycle events without you having to poll for them. When the fund administrator accepts or rejects an order, when a token transfer settles or fails on-chain, the API emits an HTTP `POST` to an endpoint you have registered. This page covers the four webhook management endpoints, the event types you can subscribe to, the recommended delivery posture, and the patterns we recommend for handling deliveries reliably.

Webhooks are associated with the distributor that registers them — they fire for events that pertain to that distributor's orders and investors. There is no global "firehose" of all platform events; each distributor sees only its own.

### Why webhooks (rather than polling)

Two reasons are worth internalising:

1. **Order acceptance is not instant.** When a distributor places a subscription or redemption, the order sits in *pending* state until the fund administrator's back-office accepts or rejects it. That can take seconds, minutes, or — for funds with daily cut-offs — hours. Polling `GET /orders/:id` to detect this transition wastes calls and introduces detection lag. A webhook fires the moment the state changes.
2. **Token transfers settle asynchronously.** Even once an order is accepted, the on-chain transfer can take additional time, and may fail. Webhooks surface settlement (`token.transfer_completed`) and failure (`token.transfer_failed`) so you can update your bookkeeping and notify the end-investor.

If your integration is small (a single order per day in testing, say) and you don't want to expose a webhook receiver yet, polling `GET /orders/:id` and `GET /transaction/holdings/...` is a valid fallback. As volume grows, webhooks become the only realistic option.

### Event types

The API emits four event types today:

| Event                      | Fires when                                                                                                                               |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `order.accepted`           | The fund administrator has accepted a pending order. Fund movement and on-chain settlement will follow                                   |
| `order.rejected`           | The fund administrator has declined the order. No further action will be taken on it                                                     |
| `token.transfer_completed` | An on-chain token transfer associated with an order has settled successfully                                                             |
| `token.transfer_failed`    | An on-chain token transfer has failed (revert, out-of-gas, or other on-chain failure). The associated order may need manual intervention |

Each event carries a payload describing the order or transfer it relates to. Your handler should treat the event type as the primary discriminator and route to the appropriate handler from there.

***

### List webhooks

Return every webhook registered for your distributor.

**`GET /webhooks`**

#### Response

Returns an array of webhook records:

json

```json
{
  "isError": false,
  "errorMsg": null,
  "data": [
    {
      "id": "wh_01HF9X2W5N8B6Q7R3T1V8Y4Z9D",
      "event_type": "order.accepted",
      "endpoint_url": "https://api.apex.example/webhooks/synthesys",
      "createdAt": 1761807929692,
      "createdBy": "apex-ops"
    },
    {
      "id": "wh_01HF9X2WAB3D4F5G6H7J8K9L0M",
      "event_type": "token.transfer_completed",
      "endpoint_url": "https://api.apex.example/webhooks/synthesys",
      "createdAt": 1761807929700,
      "createdBy": "apex-ops"
    }
  ]
}
```

#### Examples

**curl:**

bash

```bash
curl -s $BASE/webhooks | jq .
```

**Node.js:**

javascript

```javascript
const webhooks = await call('GET', '/webhooks');
console.log(`${webhooks.length} webhook(s) registered`);
```

**Python:**

python

```python
webhooks = call("GET", "/webhooks")
print(f"{len(webhooks)} webhook(s) registered")
```

***

### Register webhook

Register a new endpoint to receive notifications for a single event type. To receive multiple event types, register the same `endpoint_url` once per event — webhooks are scoped to one event each.

**`POST /webhooks/register`**

#### Request body

| Field          | Type   | Required | Description                                                                                    |
| -------------- | ------ | -------- | ---------------------------------------------------------------------------------------------- |
| `event_type`   | string | Yes      | One of `order.accepted`, `order.rejected`, `token.transfer_completed`, `token.transfer_failed` |
| `endpoint_url` | string | Yes      | Fully qualified HTTPS URL that will receive `POST` deliveries. Plain HTTP is rejected          |

#### Response

json

```json
{
  "isError": false,
  "errorMsg": null,
  "data": {
    "id": "wh_01HF9X2W5N8B6Q7R3T1V8Y4Z9D",
    "event_type": "order.accepted",
    "endpoint_url": "https://api.apex.example/webhooks/synthesys",
    "createdAt": 1761807929692,
    "createdBy": "apex-ops"
  }
}
```

#### Examples

**curl:**

bash

```bash
curl -sX POST $BASE/webhooks/register \
  -H 'Content-Type: application/json' \
  -H 'x-actor: apex-ops' \
  -d '{
    "event_type": "order.accepted",
    "endpoint_url": "https://api.apex.example/webhooks/synthesys"
  }'
```

**Node.js:**

javascript

```javascript
async function registerWebhook(eventType, endpointUrl) {
  return call('POST', '/webhooks/register', {
    event_type: eventType,
    endpoint_url: endpointUrl,
  });
}

// Register all four event types at once
const events = ['order.accepted', 'order.rejected', 'token.transfer_completed', 'token.transfer_failed'];
for (const e of events) {
  await registerWebhook(e, 'https://api.apex.example/webhooks/synthesys');
}
```

**Python:**

python

```python
def register_webhook(event_type, endpoint_url):
    return call("POST", "/webhooks/register", {
        "event_type": event_type,
        "endpoint_url": endpoint_url,
    })

events = ["order.accepted", "order.rejected", "token.transfer_completed", "token.transfer_failed"]
for e in events:
    register_webhook(e, "https://api.apex.example/webhooks/synthesys")
```

***

### Check webhook registration

Look up which event types are registered for a given URL under your distributor. Useful for idempotent registration scripts that re-run safely on deploy.

**`GET /webhooks/check`**

#### Request body

| Field          | Type   | Required | Description              |
| -------------- | ------ | -------- | ------------------------ |
| `endpoint_url` | string | Yes      | The webhook URL to check |

#### Response

json

```json
{
  "isError": false,
  "errorMsg": null,
  "data": {
    "exists": true,
    "event_types": ["order.accepted", "order.rejected"]
  }
}
```

If no webhook for this URL is registered for your distributor, `exists` is `false` and `event_types` is an empty array.

#### Examples

**curl:**

bash

```bash
curl -sX GET $BASE/webhooks/check \
  -H 'Content-Type: application/json' \
  -d '{"endpoint_url":"https://api.apex.example/webhooks/synthesys"}'
```

**Node.js:**

javascript

```javascript
async function checkWebhook(endpointUrl) {
  const res = await fetch(`${BASE}/webhooks/check`, {
    method: 'GET',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ endpoint_url: endpointUrl }),
  });
  const payload = await res.json();
  if (payload.isError) throw new Error(payload.errorMsg);
  return payload.data;
}
```

**Python:**

python

```python
import requests

def check_webhook(endpoint_url):
    r = requests.request(
        "GET",
        f"{BASE}/webhooks/check",
        json={"endpoint_url": endpoint_url},
        timeout=10,
    )
    payload = r.json()
    if payload["isError"]:
        raise RuntimeError(payload["errorMsg"])
    return payload["data"]
```

***

### Test webhook

Trigger a synthetic event delivery to a registered webhook. The endpoint receives a `POST` with a payload shaped like the real event, marked as a test. Use this to verify your receiver is wired up correctly without waiting for a real event.

**`POST /webhooks/test`**

#### Request body

| Field        | Type   | Required | Description                                             |
| ------------ | ------ | -------- | ------------------------------------------------------- |
| `event_type` | string | Yes      | The event type to simulate. Must match the registration |
| `webhook_id` | string | Yes      | The webhook ID returned from `POST /webhooks/register`  |

#### Response

json

```json
{
  "isError": false,
  "errorMsg": null,
  "data": {
    "success": true,
    "message": "Test webhook sent successfully"
  }
}
```

#### Examples

**curl:**

bash

```bash
curl -sX POST $BASE/webhooks/test \
  -H 'Content-Type: application/json' \
  -d '{
    "event_type": "order.accepted",
    "webhook_id": "wh_01HF9X2W5N8B6Q7R3T1V8Y4Z9D"
  }'
```

**Node.js:**

javascript

```javascript
await call('POST', '/webhooks/test', {
  event_type: 'order.accepted',
  webhook_id: 'wh_01HF9X2W5N8B6Q7R3T1V8Y4Z9D',
});
```

**Python:**

python

```python
call("POST", "/webhooks/test", {
    "event_type": "order.accepted",
    "webhook_id": "wh_01HF9X2W5N8B6Q7R3T1V8Y4Z9D",
})
```

***

### Building a webhook receiver

Your endpoint should accept `POST` requests with a JSON body, respond quickly with a `2xx` status, and do the actual work asynchronously. A minimal receiver in each language:

**Node.js (Express):**

javascript

```javascript
import express from 'express';

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

app.post('/webhooks/synthesys', async (req, res) => {
  const { event_type, data } = req.body;

  // Acknowledge immediately
  res.status(200).json({ received: true });

  // Do the work asynchronously
  queueMicrotask(() => handleEvent(event_type, data).catch(console.error));
});

async function handleEvent(eventType, data) {
  switch (eventType) {
    case 'order.accepted':         await onOrderAccepted(data); break;
    case 'order.rejected':         await onOrderRejected(data); break;
    case 'token.transfer_completed': await onTransferCompleted(data); break;
    case 'token.transfer_failed':    await onTransferFailed(data); break;
    default: console.warn('Unknown event type:', eventType);
  }
}

app.listen(3000);
```

**Python (FastAPI):**

python

```python
from fastapi import FastAPI, BackgroundTasks
from pydantic import BaseModel

app = FastAPI()

class WebhookPayload(BaseModel):
    event_type: str
    data: dict

@app.post("/webhooks/synthesys")
async def receive(payload: WebhookPayload, bg: BackgroundTasks):
    bg.add_task(handle_event, payload.event_type, payload.data)
    return {"received": True}

async def handle_event(event_type: str, data: dict):
    handlers = {
        "order.accepted":             on_order_accepted,
        "order.rejected":             on_order_rejected,
        "token.transfer_completed":   on_transfer_completed,
        "token.transfer_failed":      on_transfer_failed,
    }
    handler = handlers.get(event_type)
    if handler:
        await handler(data)
```

### Patterns

* **Acknowledge fast, process async.** Return `200` as soon as you've persisted the raw payload. Do downstream work (database updates, email notifications, on-chain reads) on a background worker. A slow handler is the most common cause of dropped deliveries.
* **Be idempotent.** The same event may be delivered more than once (e.g. when a previous delivery timed out from the sender's perspective but actually succeeded). Use the order ID or transfer hash in the payload as an idempotency key — if you've already processed it, return `200` without re-processing.
* **Register on deploy, idempotently.** Use `GET /webhooks/check` in your deploy script to confirm registrations are in place. If they're not, call `POST /webhooks/register`. This makes your environment self-healing — a freshly provisioned environment ends up with the right webhooks without manual setup.
* **Log every delivery.** Capture the event type, the order or transfer ID, and the result of your handler. Webhook bugs are notoriously hard to debug after the fact without a trail.
* **Use `POST /webhooks/test` in CI.** A smoke test that registers a webhook against a test receiver, fires the test event, and asserts the receiver got hit catches a whole class of misconfiguration bugs before they bite production.

### Common errors

* **`400` "endpoint\_url must be HTTPS"** — registration rejected plain HTTP. Use HTTPS even for staging environments.
* **`400` "event\_type must be one of …"** — typo in the event type. Copy the value from the event types table.
* **`404` "webhook\_id not found"** — `POST /webhooks/test` was called with an ID that doesn't exist or doesn't belong to your distributor.
* **A registered webhook never fires** — either no events are being generated yet (typical in a fresh environment), or your endpoint URL is unreachable from the API's network egress. Use `POST /webhooks/test` to distinguish.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## 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, and the optional `goal` query parameter:

```
GET https://synthesys-2.gitbook.io/synthesys-docs/network-overview/api-reference/webhooks-wip.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
