> The full uetr.ai documentation index is at https://uetr.ai/llms.txt, and every page under https://uetr.ai/docs/api is also Markdown at its address plus `.md`.

# Add a webhook endpoint

Ask to send the account's transfer events to an HTTPS address: your own receiver, or a Slack or Microsoft Teams incoming webhook, which gets a readable message. The endpoint starts pending: it receives only test events, on a sample transfer, until the account owner confirms it on the website, and the owner is normally notified by email. The signing secret is returned once, here; deliveries carry Standard Webhooks headers signed with it. Only the account owner's API keys and connected apps can add endpoints.

`POST https://uetr.ai/api/v1/webhooks`

- MCP tool: `create_webhook_endpoint`
- Access: a key or connection with read and write access
- Only the account owner's keys and connections

## Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | string (at most 2048 characters) | Yes | The full HTTPS address. It must use a domain name and the standard port. |
| `events` | array of `status_changed`, `final_state`, `checks_ended` (at least 1 item, at most 3 items) | No | The events to send. Defaults to all of them. |

## Response

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `status` | string | Yes |  |
| `endpoint` | object | Yes |  |
| `endpoint.id` | string (uuid) | Yes |  |
| `endpoint.url` | string | Yes | The address's host and a masked path. The full address is never shown. |
| `endpoint.destination` | `json`, `slack`, `teams` | Yes | json: signed JSON. slack and teams: a readable message for a Slack or Microsoft Teams incoming webhook. |
| `endpoint.events` | array of `status_changed`, `final_state`, `checks_ended` | Yes |  |
| `endpoint.status` | `pending`, `active`, `paused` | Yes | pending: waiting for the account owner to confirm it on the website; until then it receives test events only. active: receiving events. paused: deliveries stopped after sustained failures; a test event that succeeds resumes it. |
| `endpoint.paused_at` | string or null | Yes |  |
| `endpoint.created_at` | string | Yes |  |
| `endpoint.created_by` | `website`, `api` | Yes | website: added on the website. api: added by an API key or a connected app. |
| `endpoint.api_version` | string | Yes |  |
| `signing_secret` | string | Yes | The secret that signs every delivery, starting whsec_. It is shown only now: store it in the receiver's secret settings. |
| `request` | object | Yes |  |
| `request.id` | string (uuid) | Yes |  |
| `request.kind` | `team_invite`, `record_share`, `webhook_endpoint` | Yes | team_invite: an invitation to the team. record_share: a payment record link. webhook_endpoint: a webhook endpoint that receives status changes. |
| `request.status` | `pending`, `confirmed`, `declined`, `expired` | Yes | pending: waiting for the account owner. confirmed: the owner approved it and it took effect. declined: the owner refused it. expired: nobody answered within seven days, or a later request for the same transfer with different settings replaced it. |
| `request.created_at` | string | Yes |  |
| `request.expires_at` | string | Yes |  |
| `request.decided_at` | string or null | Yes |  |
| `request.invite_email` | string or null | Yes | For a team invite: the address to invite. |
| `request.uetr` | string or null | Yes | For a record share: the transfer's UETR. |
| `request.record_url` | string or null | Yes | For a confirmed record share: the payment record link, while the record still has that link. Null otherwise. |
| `request.endpoint_url` | string or null | Yes | For a webhook endpoint: its host and a masked path. Null otherwise. |
| `detail` | string | Yes | Plain English to pass on to the person behind the request. |

## Example

```sh
curl -X POST "https://uetr.ai/api/v1/webhooks" \
  -H "Authorization: Bearer $UETR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://hooks.slack.com/services/T000/B000/XXXX","events":["status_changed"]}'
```

The answer:

```json
{
  "status": "pending_owner_confirmation",
  "request": {
    "id": "4c7e1a3b-8d2f-4b6e-a9c5-3f6b9e2d5a8c",
    "kind": "webhook_endpoint",
    "status": "pending",
    "created_at": "2026-10-14T09:35:00.000Z",
    "expires_at": "2026-10-21T09:35:00.000Z",
    "decided_at": null,
    "invite_email": null,
    "uetr": null,
    "record_url": null,
    "endpoint_url": "hooks.slack.com/services/••••"
  },
  "detail": "The account owner must confirm the webhook endpoint hooks.slack.com/services/•••• on the website before it takes effect. They have been notified by email.",
  "endpoint": {
    "id": "3b1f8c2e-6a4d-4f5e-9c7b-2d8e1a0f4b6c",
    "url": "hooks.slack.com/services/••••",
    "destination": "slack",
    "events": [
      "status_changed"
    ],
    "status": "pending",
    "paused_at": null,
    "created_at": "2026-10-14T09:35:00.000Z",
    "created_by": "api",
    "api_version": "2026-10-14"
  },
  "signing_secret": "whsec_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
}
```

## Refusals

Besides the [access refusals](/docs/api/errors#access-refusals) every call can meet, this call can answer:

- [`read_only_credential`](/docs/api/errors#read_only_credential) (403): Read-only key or connection.
- [`machine_writes_disabled`](/docs/api/errors#machine_writes_disabled) (503): Changes are switched off.
- [`request_too_large`](/docs/api/errors#request_too_large) (413): Request too large.
- [`not_permitted`](/docs/api/errors#not_permitted) (403): Not permitted.
- [`destination_not_allowed`](/docs/api/errors#destination_not_allowed) (400): Webhook address not accepted.
- [`endpoint_exists`](/docs/api/errors#endpoint_exists) (409): Webhook address already registered.
- [`endpoint_limit`](/docs/api/errors#endpoint_limit) (409): Webhook endpoint limit reached.