# Your own payout system

Hand each payout to your own system by a signed webhook, read where it goes, and report the result.

If you already pay people from your own system (a bank's payout API, a payroll tool, an
in-app balance), a referral program can hand each payout to it. Choose **Your own system**
as the program's provider on the **Payouts** tab and pick one of your webhook endpoints.
When you press Send, BoltClaw asks your system for each payout; your system pays it and
says what happened. BoltClaw never holds the money.

## 1. The request

Each payout in a batch you send arrives as `referral.payout.requested`, signed like every
BoltClaw webhook ([Webhooks](https://boltclaw.ai/docs/webhooks)), at the **one** endpoint the program chose.
Ticking the event on any other endpoint receives nothing.

```json
{
  "event": "referral.payout.requested",
  "occurred_at": "2026-11-01T06:00:12Z",
  "program_id": "7c1b5f0e-2a7d-4f53-9a8e-0d4b3c2e1f10",
  "payout_id": "0f1e2d3c-4b5a-4978-8796-a5b4c3d2e1f0",
  "partner_id": "3a9c4e21-8b6d-4c1f-a0e2-5d7f9b1c3e48",
  "batch_id": "c2d4e6f8-1a3b-4c5d-8e9f-0a1b2c3d4e5f",
  "amount_minor": 12000,
  "currency": "USD",
  "reference": "bc-0f1e2d3c4b5a",
  "method": "paypal",
  "external_id": null
}
```

`external_id` is your own id for one of your own users (a referrer in your product), or
null for an affiliate. `method` is the kind of payout details the promoter saved, never the
details: webhook bodies sit in your delivery log, which more people may read than should
see a bank account.

## 2. Where to send it

Your system reads the destination with its own key, which holds `referrals:payouts`
(make it in Settings, Developers; an AI assistant's key can never hold it):

```bash
curl https://api.boltclaw.ai/v1/referrals/payouts/0f1e2d3c-4b5a-4978-8796-a5b4c3d2e1f0/details \
  -H "Authorization: Bearer $BOLTCLAW_PAYOUTS_KEY"
```

```json
{
  "payout_id": "0f1e2d3c-4b5a-4978-8796-a5b4c3d2e1f0",
  "amount_minor": 12000,
  "currency": "USD",
  "reference": "bc-0f1e2d3c4b5a",
  "method": "paypal",
  "details": { "email": "jane@example.com" },
  "external_id": null
}
```

It answers only while the payout waits to be paid, and `409` while the promoter's payout
details changed in the last 48 hours or they locked their payouts. Every read is recorded in
the activity log, as a payout export is.

## 3. The result

Pay it, then say what happened. The same answer again changes nothing.

```bash
curl -X POST https://api.boltclaw.ai/v1/referrals/payouts/0f1e2d3c-4b5a-4978-8796-a5b4c3d2e1f0/result \
  -H "Authorization: Bearer $BOLTCLAW_PAYOUTS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "paid", "reference": "TXN-99812"}'
```

| `status` | Also send | What happens |
|---|---|---|
| `paid` | `reference`, your own payment id (optional) | The commissions are paid; the promoter is emailed; `referral.payout.paid` |
| `failed` | `reason` | The commissions go back to Ready to pay; the promoter is emailed what to fix |
| `returned` | `reason` | The same, for a payment that came back |

`reason` is one of `invalid_destination`, `recipient_not_ready`, `insufficient_funds`,
`returned` or `other`. A payout already settled differently answers `409`.

## A worked example

A small Node handler, using the SDK's webhook check (`@boltclaw/referrals`):

```js
import { verifyWebhook } from "@boltclaw/referrals/server";

export async function POST(req) {
  const raw = await req.text();
  const event = await verifyWebhook(raw, req.headers, process.env.BOLTCLAW_WEBHOOK_SECRET);
  if (event.event !== "referral.payout.requested") return new Response(null, { status: 204 });

  const api = "https://api.boltclaw.ai/v1/referrals/payouts/" + event.payout_id;
  const auth = { Authorization: `Bearer ${process.env.BOLTCLAW_PAYOUTS_KEY}` };
  const where = await (await fetch(`${api}/details`, { headers: auth })).json();

  const paid = await myPayoutProvider.send({
    to: where.details,
    amountMinor: where.amount_minor,
    currency: where.currency,
    idempotencyKey: event.payout_id, // so a retried webhook never pays twice
  });

  await fetch(`${api}/result`, {
    method: "POST",
    headers: { ...auth, "Content-Type": "application/json" },
    body: JSON.stringify(paid.ok ? { status: "paid", reference: paid.id } : { status: "failed", reason: "other" }),
  });
  return new Response(null, { status: 204 });
}
```

Use the payout id as your own idempotency key: a webhook can arrive more than once.

## When the endpoint is off

If the chosen endpoint is switched off or deleted, nothing is queued: the payout stays sent
and waiting, the connection shows the problem, and you can mark it paid or failed by hand on
the Payouts tab once you know what happened.

<!-- https://boltclaw.ai/docs/referral-payout-webhook as markdown. Generated from openapi.yaml; API version 1.22.0. -->
<!-- Every /docs page answers as markdown: add .md to its path, or send Accept: text/markdown. -->
