# Webhooks

### Configuration

Set your webhook URL in the Zynta dashboard under **Settings → Webhooks.**

### Event types

| Event                 | Trigger                                  |
| --------------------- | ---------------------------------------- |
| `transfer.pending`    | Transfer created, awaiting deposit       |
| `transfer.processing` | Deposit received, conversion in progress |
| `transfer.completed`  | Funds delivered to recipient             |
| `transfer.failed`     | Transfer could not be completed          |
| `transfer.expired`    | Deposit window expired before completion |
| `compliance.approved` | KYC/KYB verification approved            |
| `compliance.rejected` | KYC/KYB verification rejected            |
| `quote.expired`       | A quote expired without execution        |

### Payload format

All webhook payloads follow a consistent structure:

```json
{
  "id": "evt_abc123def456",
  "type": "transfer.completed",
  "created_at": "2026-03-24T14:03:22Z",
  "data": {
    "transfer_id": "txn_7km3nf...",
    "status": "completed",
    "source_amount": 1000.00,
    "destination_amount": 1685420.00,
    "completed_at": "2026-03-24T14:03:22Z"
  }
}
```

### Verification

Every webhook includes an `X-Zynta-Signature` header containing an HMAC-SHA256 signature of the raw request body, signed with your webhook secret.

Always verify the signature before processing:

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

function verifyWebhook(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// In your webhook handler
app.post('/webhooks/zynta', (req, res) => {
  const signature = req.headers['x-zynta-signature'];
  const isValid = verifyWebhook(
    JSON.stringify(req.body),
    signature,
    process.env.WEBHOOK_SECRET
  );

  if (!isValid) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Process the event
  const { type, data } = req.body;
  switch (type) {
    case 'transfer.completed':
      // Handle completed transfer
      break;
    case 'compliance.approved':
      // Handle KYC approval
      break;
  }

  res.status(200).json({ received: true });
});
```

### Retry policy

If your endpoint returns a non-2xx status code or times out, Zynta retries with exponential backoff:

| Attempt | Delay      |
| ------- | ---------- |
| 1       | Immediate  |
| 2       | 1 minute   |
| 3       | 5 minutes  |
| 4       | 30 minutes |
| 5       | 2 hours    |

After 5 failed attempts, the delivery is marked as failed. You can view delivery logs in the [Dashboard](/dashboard/dashboard-overview.md).

### Best practices <a href="#best-practices" id="best-practices"></a>

* **Return 200 quickly** — process webhook events asynchronously. Acknowledge receipt with a `200` response and handle the business logic in a background job.
* **Handle duplicates** — webhooks may be delivered more than once. Use the `id` field to deduplicate.
* **Verify signatures** — always validate the `X-Zynta-Signature` header before trusting the payload.
* **Use HTTPS** — webhook URLs must use HTTPS in production.

### Incoming provider webhooks <a href="#incoming-provider-webhooks" id="incoming-provider-webhooks"></a>

These are internal endpoints where Zynta receives callbacks from upstream providers. Partners do not call these directly.

| Endpoint                           | Purpose                                 |
| ---------------------------------- | --------------------------------------- |
| `POST /api/v1/webhooks/compliance` | KYC decision events (approved/rejected) |
| `POST /api/v1/webhooks/transfers`  | Transfer status updates from upstream   |
| `POST /api/v1/webhooks/busha`      | Liquidity provider callbacks            |


---

# 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.zynta.com/api-reference/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.
