tridaPlatform developers

Webhooks

The platform tells your product what happened by calling an address you registered: a webhook. Every call is signed, retried for 72 hours if you do not answer, and carries a stable event id so you can deduplicate.

Register an endpoint

const endpoint = await trida.webhookEndpoints.create({
  url: 'https://app.example.com/webhooks/trida',
  eventTypes: ['pay.intent.succeeded'],
});
endpoint.secret; // "whsec_..." : returned ONCE, store it now

What you receive

POST with a JSON body and these headers:

Header Meaning
Trida-Signature t=<unix seconds>,v1=<hmac> (two v1 during a rotation)
Trida-Event-Id the event id: the same on every attempt, deduplicate on it
Trida-Delivery-Attempt 1 for the first delivery, 2 for the first retry...
Trida-Request-Id the id of the request that produced the event, for your logs (absent if none)
User-Agent Trida-Webhooks/1
{
  "id": "6f1c2a52-4f3d-4c53-9a3e-1b8f0d6a7c11",
  "type": "pay.intent.succeeded",
  "createdAt": "2026-10-18T10:00:00.000Z",
  "data": { "...": "the event payload" }
}

Verify the signature

The signature is HMAC-SHA256 (hex) of the string "<t>.<raw body>", keyed with the endpoint secret. Always verify the raw bytes you received, never a parsed and re-serialized JSON. In TypeScript the SDK does it:

import { verifyWebhook } from '@tridafactory/sdk';

const result = verifyWebhook(rawBody, headers, [process.env.TRIDA_WEBHOOK_SECRET!]);
if (!result.valid) throw new Error(result.reason); // 'missing-signature' | 'malformed' | 'expired' | 'mismatch'

verifyWebhook accepts the two secrets of a rotation, refuses a signature older than five minutes (or that far in the future), and compares in constant time. In another language, implement exactly these steps:

  1. Read the Trida-Signature header and split it on commas into t=... and one or more v1=....
  2. Reject if |now - t| > 300 seconds.
  3. Compute HMAC-SHA256(secret, t + "." + rawBody) as lowercase hex, for each secret you know.
  4. Accept if any computed value equals any v1 (compare in constant time).

A complete, tested receiver:

import { verifyWebhook } from '@tridafactory/sdk';

/**
 * Receive a Trida webhook: verify the signature on the RAW body, then act. Framework independent: pass the raw body exactly
 * as received (never a parsed and re-serialized JSON), the request headers, and the secrets you know.
 *
 * `secrets` holds ONE secret normally, TWO during a rotation (the new one and the previous one): either signature is accepted.
 */
export interface WebhookResult {
  status: number;
  body: string;
}

export function handleTridaWebhook(
  rawBody: string,
  headers: Record<string, string | string[] | undefined>,
  secrets: string[],
  seenEventIds: Set<string>,
): WebhookResult {
  const verification = verifyWebhook(rawBody, headers, secrets);
  if (!verification.valid) {
    // 400, not 401: the sender is Trida, and a retry with the same bytes would fail the same way.
    return { status: 400, body: `invalid signature: ${verification.reason}` };
  }

  // The same event can arrive more than once (delivery is at least once): deduplicate on the event id.
  const eventId = verification.eventId;
  if (eventId !== null && seenEventIds.has(eventId)) {
    return { status: 200, body: 'already processed' };
  }
  if (eventId !== null) seenEventIds.add(eventId);

  const event = JSON.parse(rawBody) as {
    id: string;
    type: string;
    createdAt: string;
    data: unknown;
  };
  switch (event.type) {
    case 'core.webhook.test':
      console.log('Trida test event received', event.id);
      break;
    default:
      console.log('event', event.type, event.id);
  }
  // Any 2xx tells Trida the event is delivered. Anything else is retried (1 min, 5 min, 30 min, 2 h, 8 h, 24 h...).
  return { status: 200, body: 'ok' };
}

Answer fast, answer 2xx

Retries and the dead letter

A failed delivery is retried at 1 minute, 5 minutes, 30 minutes, 2 hours, 8 hours, 24 hours, then every 24 hours, until 72 hours after the event was created. After that the delivery is dead: the platform stops, and the platform operators are alerted. Each attempt is signed afresh with the current time, so a retry hours later is still valid.

Each endpoint has its own state: an event delivered to endpoint A and failing for endpoint B keeps retrying for B only.

If an endpoint keeps failing, the platform suspends calls to that host for a minute after five consecutive failures, instead of piling up requests that would each wait 10 seconds; its deliveries stay scheduled and resume.

Replay an event

An event that failed or died can be sent again without recreating it:

await trida.events.replay(eventId);

The same event id goes to the active endpoints subscribed to its type today (an endpoint created since receives it; one deleted does not), signed with the secrets of the moment. Each replay opens a new generation of deliveries with a fresh 72-hour budget; earlier deliveries keep their history.

Send yourself a test event

await trida.webhookEndpoints.test(endpointId);

A core.webhook.test event, signed and delivered like any other, to that endpoint only (whatever it subscribed to). Use it to check your receiver.

Rotating a secret

Rotate without interrupting anything:

  1. trida.webhookEndpoints.rotateSecret(id) returns a new secret. The previous one keeps signing next to it: every delivery now carries two v1 signatures.
  2. Deploy your receiver with both secrets (verifyWebhook(rawBody, headers, [newSecret, oldSecret])).
  3. trida.webhookEndpoints.retirePreviousSecret(id) ends the rotation: only the new secret signs.

Rotating twice in a row drops the oldest secret.

Delete an endpoint

trida.webhookEndpoints.delete(id) removes the endpoint, its secrets and its pending deliveries.