View as Markdown

Webhook subscriptions

Webhook subscriptions push events to your endpoint instead of making you poll for them. A webhook subscription points an endpoint at one or more event types for a client; when the platform emits a matching event, it delivers a POST to that endpoint. Reads need platform:read or webhooks:write and the operations module on read in one of your groups; writes need webhooks:write and are for an admin or Aleta only: any other user gets 403, in a group or not. Without the module the routes answer 403. Every write takes ?dry_run=true and an Idempotency-Key header and is logged in the change history.

A subscription you create with POST is stored but nothing delivers to it: v1 has no event sender of its own, which is also why v1's own signing key is pending (see the key list). Subscriptions moved from the legacy platform (origin: legacy) are delivered by the legacy platform, as before. They are Aleta's (owner: aleta), so your people can read them but not change them.

Data model

idstring

Stable identifier for the subscription. A UUID.

client_idstring or null

The client whose events the subscription receives; null covers the whole tenant.

endpointstring

The http or https URL deliveries are POSTed to. Loopback, private, link-local and metadata addresses are refused.

event_typesarray of string

Event types the subscription covers, as given and in order, e.g. calculated_data.created: emitted when a day's holdings and performance numbers are ready. Each is one of the event types of GET /v1/events. A subscription moved from the legacy platform keeps the names it was stored with.

statusstring

active or disabled. A disabled subscription receives nothing.

disabled_at, disabled_reasontimestamp or null, string or null

When and why the subscription was disabled. PATCH with status: disabled sets disabled_at and leaves the reason as it is; status: active clears both.

originstring

legacy (moved from the legacy platform) or v1.

ownerstring

aleta (Aleta set it up for you) or tenant (your own). One Aleta set up is Aleta's to change: a PATCH or DELETE by your people, admins included, is a 403 with the text Aleta set this up for you: only Aleta can change it.

deliveryobject

The last delivery to this subscription: last_attempt_at and last_success_at (timestamp or null), last_status_code (integer or null), last_error (the latest failed attempt's message, cut to 500 characters, or null) and consecutive_failures (integer) since the last success. Never a secret.

created_at, updated_attimestamp

RFC 3339, UTC, microseconds. A subscription moved from the legacy platform has the time legacy made it.

archived_at, archived_by, archive_reasontimestamp, string, string or null

Set when a DELETE archived a subscription that had been delivered to or was moved from the legacy platform: when, the id of the user who archived it, and the ?reason= given. Null while it is live.


GET/v1/webhook-subscriptions

List webhook subscriptions

Returns the subscriptions you can see, oldest first (created_at, then id), as a cursor list. An admin sees all of them; a user sees the subscriptions of the clients their access covers in full: a grant on the client (and its sub-clients when the grant includes them) or on the whole tenant. A grant on a party, an entity, a cash account or a custody account under a client does not show the client's subscription; a subscription for the whole tenant needs a grant on the tenant. An unknown query parameter is a 400.

Query parameters

client_idstring

Only this client's subscriptions.

include_archivedboolean

Also archived subscriptions. Default false.

cursor, limitstring, integer

The page: limit defaults to 100; values above 1000 are capped at 1000. A cursor that does not decode is a 400.

Request
GET/v1/webhook-subscriptions
curl https://api.aleta.ai/v1/webhook-subscriptions \
  -H "Authorization: Bearer {access_token}"
Response
{
  "data": [
    {
      "id": "019f95f0-2d40-7b58-8f6e-9a3c1d7e5b02",
      "client_id": "019dfeb0-0511-77a7-a8be-a911ab988a0a",
      "endpoint": "https://example.com/aleta/events",
      "event_types": ["calculated_data.created"],
      "status": "active",
      "disabled_at": null,
      "disabled_reason": null,
      "origin": "v1",
      "owner": "tenant",
      "delivery": {
        "last_attempt_at": null,
        "last_success_at": null,
        "last_status_code": null,
        "last_error": null,
        "consecutive_failures": 0
      },
      "created_at": "2026-10-17T06:12:00.000000Z",
      "updated_at": "2026-10-17T06:12:00.000000Z",
      "archived_at": null,
      "archived_by": null,
      "archive_reason": null
    }
  ],
  "next_cursor": null,
  "limit": 100
}

GET/v1/webhook-subscriptions/{id}

Retrieve a webhook subscription

Returns one subscription, an archived one too. An unknown id, one you cannot see, or one that is not a UUID is a 404.

Request
GET/v1/webhook-subscriptions/{id}
curl https://api.aleta.ai/v1/webhook-subscriptions/019f95f0-2d40-7b58-8f6e-9a3c1d7e5b02 \
  -H "Authorization: Bearer {access_token}"

POST/v1/webhook-subscriptions

Create a webhook subscription

Subscribes an endpoint to events for a client. Returns 201 with the created subscription, status: active, origin: v1 and owner: tenant (aleta when Aleta creates it). A client you cannot see is a 404. Any other body field is a 400.

Body parameters

client_idstring

The client's UUID. Required.

endpointstring

The absolute http or https URL to deliver to, stored trimmed. Required. Anything else is a 400 bad_request, and so is a loopback, private, link-local or metadata address or localhost.

event_typesarray of string

Event types to subscribe to, stored as given and in order. Required. Each must be one of the event types, listed once. An empty list is a 400 with the text event_types must not be empty; a name that is no event type (a blank one included) is a 400 naming the allowed types, and a name listed twice is a 400.

Request
POST/v1/webhook-subscriptions
curl -X POST https://api.aleta.ai/v1/webhook-subscriptions \
  -H "Authorization: Bearer {access_token}" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2f0e-hook-1" \
  -d '{
    "client_id": "019dfeb0-0511-77a7-a8be-a911ab988a0a",
    "endpoint": "https://example.com/aleta/events",
    "event_types": ["calculated_data.created"]
  }'
Response
{
  "id": "019f95f0-2d40-7b58-8f6e-9a3c1d7e5b02",
  "client_id": "019dfeb0-0511-77a7-a8be-a911ab988a0a",
  "endpoint": "https://example.com/aleta/events",
  "event_types": ["calculated_data.created"],
  "status": "active",
  "disabled_at": null,
  "disabled_reason": null,
  "origin": "v1",
  "owner": "tenant",
  "delivery": {
    "last_attempt_at": null,
    "last_success_at": null,
    "last_status_code": null,
    "last_error": null,
    "consecutive_failures": 0
  },
  "created_at": "2026-10-17T06:12:00.000000Z",
  "updated_at": "2026-10-17T06:12:00.000000Z",
  "archived_at": null,
  "archived_by": null,
  "archive_reason": null
}

PATCH/v1/webhook-subscriptions/{id}

Update a webhook subscription

Changes the endpoint, the event types or the status, and returns 200 with the subscription. Fields you leave out keep their value; any other field is a 400. status: active also brings an archived subscription back; on an archived subscription nothing else can change: 409 in_use whose next_action is this route with status. An unknown id, one you cannot see, or one that is not a UUID is a 404. One Aleta set up for you (owner: aleta) is Aleta's to change: your people, admins included, get 403 with the text Aleta set this up for you: only Aleta can change it. A subscription moved from the legacy platform is 409 legacy_managed until your tenant has moved; the 403 comes first.

Body parameters

endpointstring

The new URL, checked as on create.

event_typesarray of string

The new event types, replacing the old ones, checked as on create: an empty list, a name that is no event type or a name listed twice is a 400.

statusstring

active or disabled; any other value is a 400 with the text status must be one of: active, disabled.

Request
PATCH/v1/webhook-subscriptions/{id}
curl -X PATCH https://api.aleta.ai/v1/webhook-subscriptions/019f95f0-2d40-7b58-8f6e-9a3c1d7e5b02 \
  -H "Authorization: Bearer {access_token}" \
  -H "Content-Type: application/json" \
  -d '{ "status": "disabled" }'

DELETE/v1/webhook-subscriptions/{id}

Delete a webhook subscription

Unsubscribes the endpoint. A subscription that was never delivered to is deleted: 204. One that was delivered to is archived and stays readable: 200 with the subscription; ?reason= is kept as its archive_reason. Any query parameter other than reason and dry_run is a 400. An archived subscription answers 200 as it is. Unknown or not visible: 404. One Aleta set up for you (owner: aleta) is Aleta's to change: your people, admins included, get 403 with the text Aleta set this up for you: only Aleta can change it. A subscription moved from the legacy platform is 409 legacy_managed until your tenant has moved, after the 403 check; after the move it is archived like a delivered one.

Request
DELETE/v1/webhook-subscriptions/{id}
curl -X DELETE https://api.aleta.ai/v1/webhook-subscriptions/019f95f0-2d40-7b58-8f6e-9a3c1d7e5b02 \
  -H "Authorization: Bearer {access_token}"

GET/v1/webhook-keys

List webhook signing keys

The public keys a webhook signature verifies against, newest first. One key signs for every tenant, and Aleta rotates it with 30 days of overlap: the active key signs now, and an older key stays on the list as retiring until retires_at. A key made by a rotation for v1's own key ring is pending: nothing signs with it, because v1 has no event sender of its own. Verify a signature against every key on the list, and fetch the list again when a signature does not verify (at least daily), since a rotation signs with the new key at once. Needs platform:read or webhooks:write, and the operations module on read in one of your groups (403 without). Where no signing key source is set up, the route answers 503. The page has no cursor: next_cursor is null and limit is the number of keys.

Response fields

kid, kty, crv, alg, use, x, yJWK

The key as a JSON Web Key: base64url coordinates without padding. alg is null only for a curve other than P-256, P-384 or P-521.

statusstring

active (signs now), pending (v1's key ring: made, nothing signs with it) or retiring.

created_at, retires_atISO 8601 timestamp or null

When the key was made, and when a retiring key leaves (null for the active and pending keys).

Request
GET/v1/webhook-keys
curl https://api.aleta.ai/v1/webhook-keys \
  -H "Authorization: Bearer {access_token}"
Response
{
  "data": [
    {
      "kid": "339c621783ded44d99033456fef772714427bf789b5a78e3aaccfd76026ae5c6",
      "kty": "EC",
      "crv": "P-256",
      "alg": "ES256",
      "use": "sig",
      "x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
      "y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0",
      "status": "active",
      "created_at": "2026-10-06T00:00:00Z",
      "retires_at": null
    }
  ],
  "next_cursor": null,
  "limit": 1
}

POST/v1/webhook-keys/rotate

Rotate the v1 signing key

Aleta only, with platform:write: everyone else, a tenant admin included, gets 403 forbidden. Makes a new signing key for v1's own key ring and answers 201 with it in active, the ring's current key, with status pending (nothing signs with the ring); the key it replaces is in retiring and keeps verifying for 30 days. The private key never leaves Aleta's key service. The key the legacy platform signs with is not part of the ring and does not change. Both appear on the list. Takes ?dry_run=true (no key is made: active is null and retiring shows the current key as it would retire) and Idempotency-Key (a replay makes no second key). A replay with another body is a 409. A rotation that cannot be stored is undone (the new key is scheduled for deletion) and answers 409. Where no signing key service is set up, the route answers 503.

Body

reasonstring

Optional. Why the key is rotated; kept with the key. Any other field is a 400.

Request
POST/v1/webhook-keys/rotate
curl -X POST https://api.aleta.ai/v1/webhook-keys/rotate \
  -H "Authorization: Bearer {access_token}" \
  -H "Idempotency-Key: rotate-2026-10" \
  -H "Content-Type: application/json" \
  -d '{"reason": "scheduled rotation"}'
Response
{
  "active": {
    "kid": "0199f2a0-5b1c-7e3d-9f40-1a2b3c4d5e6f",
    "kty": "EC",
    "crv": "P-256",
    "alg": "ES256",
    "use": "sig",
    "x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
    "y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0",
    "status": "pending",
    "created_at": "2026-10-18T09:00:00Z",
    "retires_at": null
  },
  "retiring": null
}
  • Holdings — what to fetch when a calculated_data.created event arrives.
  • Performance — likewise ready when the event fires.