# Webhooks — API reference

Source: https://tenergy.me/docs/api/webhooks
Last updated: 2026-09-27

Registering and testing delivery endpoints.

| Method | Path | Summary |
|---|---|---|
| `GET` | `/v1/webhooks` | List delivery endpoints |
| `POST` | `/v1/webhooks` | Register a delivery endpoint |
| `GET` | `/v1/webhooks/{webhookId}` | Read one endpoint |
| `PATCH` | `/v1/webhooks/{webhookId}` | Edit an endpoint |
| `DELETE` | `/v1/webhooks/{webhookId}` | Delete an endpoint |
| `POST` | `/v1/webhooks/{webhookId}/rotate-secret` | Issue a new signing secret |
| `POST` | `/v1/webhooks/{webhookId}/test` | Send a test delivery |
| `GET` | `/v1/webhooks/{webhookId}/deliveries` | Delivery log of one endpoint |

Generated from [`openapi.yaml`](https://tenergy.me/openapi.yaml) at build time. Base URL `https://api.tenergy.me/v1`, or `https://api-nile.tenergy.me/v1` on Nile ([Environments](https://tenergy.me/docs/environments)). Every request below is signed as in [Authentication](https://tenergy.me/docs/authentication) unless its **Auth** line says otherwise.

## List delivery endpoints

`GET /v1/webhooks` · `listWebhooks`

**Auth:** API key (HMAC).

The `secret` is never returned here.

### Responses

| Status | Meaning |
|---|---|
| `200` | OK |
| `401` | Missing, malformed or rejected credentials. |
| `500` | Something broke on our side. |

#### Response fields

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | array of object (WebhookEndpoint) | yes |  |
| `data[].id` | string | yes |  |
| `data[].url` | string (uri) | yes |  |
| `data[].role` | enum: `primary`, `backup` | yes |  |
| `data[].events` | array of enum (13 values, WebhookEventType) \| null |  | `null` means all events. |
| `data[].is_active` | boolean | yes |  |
| `data[].last_delivery_at` | string (date-time) \| null |  |  |
| `data[].last_delivery_status` | integer \| null |  |  |
| `data[].created_at` | string (date-time) | yes |  |
| `data[].updated_at` | string (date-time) |  |  |
| `max_endpoints` | integer | yes |  |
| `roles` | array of enum: `primary`, `backup` |  |  |

## Register a delivery endpoint

`POST /v1/webhooks` · `createWebhook`

**Auth:** API key (HMAC).

Returns a `secret` **shown exactly once**. It signs every delivery to this endpoint — store
it now; if you lose it, rotate rather than re-register.

At most two endpoints per account: one `primary` and one `backup`. Delivery is not fan-out
— every event goes to the primary, and moves to the backup only after the primary's retries
are exhausted. The first endpoint you create becomes primary.

URL requirements, enforced on create and on every edit: `https` only; must resolve to a
public address (loopback, RFC 1918, link-local including `169.254.169.254` and other
non-routable ranges are rejected); no credentials in the URL; at most 2048 characters.

Full event catalogue, payloads, signature scheme and retry schedule: [Webhooks](https://tenergy.me/docs/webhooks).

### Request body

JSON (`WebhookRequest`), required.

| Field | Type | Required | Description |
|---|---|---|---|
| `url` | string (uri) | yes | HTTPS, publicly resolvable, no credentials. |
| `role` | enum: `primary`, `backup` |  | Omit to take the first free role. |
| `events` | array of enum (13 values, WebhookEventType) |  | Which events to deliver here. Omit to receive all of them — recommended, since new event types then start arriving without a registration change. Route by the `event` field and ignore types you do not handle yet. |

Example request body, from the contract (illustrative values):

```json
{
  "url": "https://acme.example/hooks/tenergy",
  "role": "primary",
  "events": ["order.confirmed","order.failed","balance.credited"]
}
```

### Responses

| Status | Meaning |
|---|---|
| `201` | Created. `secret` is present only in this response. |
| `400` | Malformed request — bad JSON, unknown field, wrong type. |
| `401` | Missing, malformed or rejected credentials. |
| `409` | The request contradicts the current state of the object. |
| `422` | Syntactically valid but semantically impossible. |
| `500` | Something broke on our side. |

#### Response fields

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes |  |
| `url` | string (uri) | yes |  |
| `role` | enum: `primary`, `backup` | yes |  |
| `events` | array of enum (13 values, WebhookEventType) \| null |  | `null` means all events. |
| `is_active` | boolean | yes |  |
| `last_delivery_at` | string (date-time) \| null |  |  |
| `last_delivery_status` | integer \| null |  |  |
| `created_at` | string (date-time) | yes |  |
| `updated_at` | string (date-time) |  |  |
| `secret` | string | yes | Signing secret. Shown once, never returned by GET. |

## Read one endpoint

`GET /v1/webhooks/{webhookId}` · `getWebhook`

**Auth:** API key (HMAC).

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `webhookId` | path | string | yes |  |

### Responses

| Status | Meaning |
|---|---|
| `200` | OK |
| `401` | Missing, malformed or rejected credentials. |
| `404` | No such object, or it belongs to another account. The two are not distinguished. |
| `500` | Something broke on our side. |

#### Response fields (`WebhookEndpoint`)

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes |  |
| `url` | string (uri) | yes |  |
| `role` | enum: `primary`, `backup` | yes |  |
| `events` | array of enum (13 values, WebhookEventType) \| null |  | `null` means all events. |
| `is_active` | boolean | yes |  |
| `last_delivery_at` | string (date-time) \| null |  |  |
| `last_delivery_status` | integer \| null |  |  |
| `created_at` | string (date-time) | yes |  |
| `updated_at` | string (date-time) |  |  |

## Edit an endpoint

`PATCH /v1/webhooks/{webhookId}` · `updateWebhook`

**Auth:** API key (HMAC).

Change `url`, `events`, `is_active` and/or `role`. Sending `{"role": "primary"}` to the
backup **swaps** the two roles in one transaction, so you are never left without a primary.
A changed URL is re-validated. `is_active: false` pauses delivery without losing the
endpoint; pausing the primary does **not** promote the backup — swap the roles if that is
what you want.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `webhookId` | path | string | yes |  |

### Request body

JSON (`WebhookPatch`), required.

| Field | Type | Required | Description |
|---|---|---|---|
| `url` | string (uri) |  |  |
| `role` | enum: `primary`, `backup` |  |  |
| `events` | array of enum (13 values, WebhookEventType) \| null |  |  |
| `is_active` | boolean |  |  |

### Responses

| Status | Meaning |
|---|---|
| `200` | Updated |
| `400` | Malformed request — bad JSON, unknown field, wrong type. |
| `401` | Missing, malformed or rejected credentials. |
| `404` | No such object, or it belongs to another account. The two are not distinguished. |
| `422` | Syntactically valid but semantically impossible. |
| `500` | Something broke on our side. |

#### Response fields (`WebhookEndpoint`)

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes |  |
| `url` | string (uri) | yes |  |
| `role` | enum: `primary`, `backup` | yes |  |
| `events` | array of enum (13 values, WebhookEventType) \| null |  | `null` means all events. |
| `is_active` | boolean | yes |  |
| `last_delivery_at` | string (date-time) \| null |  |  |
| `last_delivery_status` | integer \| null |  |  |
| `created_at` | string (date-time) | yes |  |
| `updated_at` | string (date-time) |  |  |

## Delete an endpoint

`DELETE /v1/webhooks/{webhookId}` · `deleteWebhook`

**Auth:** API key (HMAC).

Hard delete; frees the role. Undelivered events for this endpoint are dropped.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `webhookId` | path | string | yes |  |

### Responses

| Status | Meaning |
|---|---|
| `204` | Deleted. No body. |
| `401` | Missing, malformed or rejected credentials. |
| `404` | No such object, or it belongs to another account. The two are not distinguished. |
| `500` | Something broke on our side. |

## Issue a new signing secret

`POST /v1/webhooks/{webhookId}/rotate-secret` · `rotateWebhookSecret`

**Auth:** API key (HMAC).

Returns a new secret **once**. It takes effect immediately for subsequent deliveries; there
is no overlap window, so deploy the new secret to your receiver before rotating, or accept
a brief period where in-flight retries fail signature verification. Each endpoint has its
own secret — rotating the primary's does not touch the backup's.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `webhookId` | path | string | yes |  |

### Responses

| Status | Meaning |
|---|---|
| `200` | Rotated |
| `401` | Missing, malformed or rejected credentials. |
| `404` | No such object, or it belongs to another account. The two are not distinguished. |
| `500` | Something broke on our side. |

#### Response fields

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes |  |
| `secret` | string | yes |  |

## Send a test delivery

`POST /v1/webhooks/{webhookId}/test` · `testWebhook`

**Auth:** API key (HMAC).

Sends a synthetic event to the endpoint and reports what your server answered. The payload
is signed exactly like a real one and carries `"test": true` at the top level, so a
receiver that ignores unknown event types, or that checks the flag, will not act on it.

Test deliveries are **not retried** and do not appear in delivery history.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `webhookId` | path | string | yes |  |

### Request body

JSON.

| Field | Type | Required | Description |
|---|---|---|---|
| `event` | enum (13 values, WebhookEventType) |  | Full catalogue with payloads in [Webhooks](https://tenergy.me/docs/webhooks). `balance.credited` for a deposit also carries `asset` (`TRX`\|`USDT`), `usdt_amount`, `rate` (SUN per USDT after spread), `rate_source`, `spread_bps`, `txid`, `sender`, `block_number`; the USDT fields are `null` for TRX. |

### Responses

| Status | Meaning |
|---|---|
| `200` | The delivery was attempted; the result describes what happened. |
| `401` | Missing, malformed or rejected credentials. |
| `404` | No such object, or it belongs to another account. The two are not distinguished. |
| `500` | Something broke on our side. |

#### Response fields

| Field | Type | Required | Description |
|---|---|---|---|
| `delivered` | boolean | yes | True when your endpoint answered 2xx. |
| `event` | enum (13 values, WebhookEventType) | yes | Full catalogue with payloads in [Webhooks](https://tenergy.me/docs/webhooks). `balance.credited` for a deposit also carries `asset` (`TRX`\|`USDT`), `usdt_amount`, `rate` (SUN per USDT after spread), `rate_source`, `spread_bps`, `txid`, `sender`, `block_number`; the USDT fields are `null` for TRX. |
| `delivery_id` | string | yes |  |
| `response_status` | integer \| null |  | HTTP status your endpoint returned; `null` on connection failure. |
| `response_body_excerpt` | string \| null |  | First 512 bytes of your response, for debugging. |
| `error` | string \| null |  | Transport-level failure description, when `delivered` is false. |
| `duration_ms` | integer |  |  |

## Delivery log of one endpoint

`GET /v1/webhooks/{webhookId}/deliveries` · `listWebhookDeliveries`

**Auth:** API key (HMAC).

Every event queued for this endpoint, newest first: which event, how many attempts, the
state, the last HTTP status your server answered and when the next retry is due. Test
deliveries are not listed. Scope `webhooks.read`, or a dashboard session with member
role `viewer`. Another account's endpoint answers `404`.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `webhookId` | path | string | yes |  |
| `limit` | query | integer |  |  |
| `cursor` | query | string |  | Opaque cursor from a previous response's `next_cursor`. |

### Responses

| Status | Meaning |
|---|---|
| `200` | OK |
| `400` | Malformed request — bad JSON, unknown field, wrong type. |
| `401` | Missing, malformed or rejected credentials. |
| `403` | Authenticated, but this key is not allowed to do it. |
| `404` | No such object, or it belongs to another account. The two are not distinguished. |
| `500` | Something broke on our side. |

#### Response fields

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | array of object (WebhookDelivery) | yes |  |
| `data[].id` | string | yes |  |
| `data[].event_id` | string | yes | The `id` of the event envelope — your dedup key, stable across retries. |
| `data[].event` | enum (13 values, WebhookEventType) | yes | Full catalogue with payloads in [Webhooks](https://tenergy.me/docs/webhooks). `balance.credited` for a deposit also carries `asset` (`TRX`\|`USDT`), `usdt_amount`, `rate` (SUN per USDT after spread), `rate_source`, `spread_bps`, `txid`, `sender`, `block_number`; the USDT fields are `null` for TRX. |
| `data[].attempt` | integer | yes | Attempts made so far. |
| `data[].state` | enum: `pending`, `delivering`, `delivered`, `failed`, `dead` | yes | `failed` is retried at `next_attempt_at`; `dead` is not retried. |
| `data[].response_status` | integer \| null | yes | HTTP status of the last attempt; `null` before one, or on a connection failure. |
| `data[].error` | string \| null | yes | Transport failure of the last attempt. |
| `data[].next_attempt_at` | string (date-time) \| null | yes |  |
| `data[].delivered_at` | string (date-time) \| null | yes |  |
| `data[].created_at` | string (date-time) | yes |  |
| `data[].updated_at` | string (date-time) | yes |  |
| `next_cursor` | string \| null | yes |  |

Example `200` response, from the contract (illustrative values — live numbers come from the API):

```json
{
  "data": [
    {
      "id": "dlv_01K5YA1B2C3D4E5F6G7H8J9K0M",
      "event_id": "evt_01K5YA1B2C3D4E5F6G7H8J9K0N",
      "event": "order.confirmed",
      "attempt": 2,
      "state": "failed",
      "response_status": 502,
      "error": null,
      "next_attempt_at": "2026-09-25T10:31:00.000Z",
      "delivered_at": null,
      "created_at": "2026-09-25T10:30:00.000Z",
      "updated_at": "2026-09-25T10:30:30.000Z"
    }
  ],
  "next_cursor": null
}
```
