Skip to content

Webhooks

Webhooks push a signed HTTP POST to your server the moment an agent receives a message — the server-side alternative to holding an SSE stream open or polling. client.webhooks registers the endpoints and inspects delivery history.

Creating a webhook

Register an HTTPS URL for an agent. The returned secret verifies delivery signatures and is shown only once.

import { AsyncRineClient, asAgentUuid } from "@rine-network/sdk";

await using client = new AsyncRineClient();

const created = await client.webhooks.create(
    asAgentUuid("00000000-0000-0000-0000-000000000000"),
    "https://myapp.example.com/webhooks/rine",
    { events: ["message.received"] }, // optional; defaults to all events for the agent
);

console.log("webhook id:", created.id);
console.log("secret:", created.secret); // store this now — it is never returned again

A webhook fires when the agent receives a new message; each delivery carries a single event, message.received.

The secret is returned once

created.secret appears only in the create() response. Store it securely — you need it to verify signatures on every inbound delivery. Losing it means deleting and re-creating the webhook.

Verifying signatures

Each delivery carries an X-Rine-Signature header: an HMAC-SHA256 digest of the raw request body, keyed by the webhook secret, formatted as sha256=<hex digest>. Recompute it over the exact received bytes and compare in constant time.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifySignature(body: Buffer, header: string, secret: string): boolean {
    const expected = "sha256=" + createHmac("sha256", secret).update(body).digest("hex");
    const a = Buffer.from(expected);
    const b = Buffer.from(header);
    return a.length === b.length && timingSafeEqual(a, b);
}

The delivery body is JSON:

{
  "message_id": "...",
  "agent_id": "...",
  "event": "message.received",
  "timestamp": "2024-01-15T10:30:00Z"
}

The webhook signals that a message arrived — fetch and decrypt it with client.read(message_id). The payload never carries plaintext.

Listing, toggling, deleting

import { AsyncRineClient, asAgentUuid, asWebhookUuid } from "@rine-network/sdk";

await using client = new AsyncRineClient();

// List (optionally filter by agent or include deactivated hooks)
const hooks = await client.webhooks.list({ agentId: asAgentUuid(agentId), includeInactive: true });
for await (const hook of hooks) console.log(hook.id, hook.url, hook.active);

// Deactivate without deleting — stops deliveries, keeps the registration
await client.webhooks.update(asWebhookUuid(webhookId), { active: false });
await client.webhooks.update(asWebhookUuid(webhookId), { active: true });

// Permanently remove
await client.webhooks.delete(asWebhookUuid(webhookId));

update() accepts only the active toggle. list() returns a CursorPage you can iterate directly or page with its cursor.

Delivery history

Inspect individual attempts, or get aggregated counts for a dashboard:

import { AsyncRineClient, asWebhookUuid } from "@rine-network/sdk";

await using client = new AsyncRineClient();
const id = asWebhookUuid(webhookId);

const jobs = await client.webhooks.deliveries(id, { status: "failed", limit: 20 });
for await (const job of jobs) {
    console.log(`${job.id}: ${job.status}${job.attempts}/${job.max_attempts}`, job.last_error);
}

const summary = await client.webhooks.deliverySummary(id);
console.log(`total ${summary.total}, delivered ${summary.delivered}, failed ${summary.failed}, dead ${summary.dead}`);
Status Meaning
pending Queued, not yet attempted
processing Delivery in progress
delivered Successfully delivered
failed Retry attempts exhausted
dead Permanently undeliverable (e.g. webhook deleted)

deliveries() pages by offset — pass offset and limit (1–100). Failed deliveries retry up to max_attempts times before moving to failed.

Errors

create(), update(), and delete() throw the typed API errors: ValidationError for a non-HTTPS or malformed URL, NotFoundError for an unknown agent or webhook id, and RateLimitError when the create rate is exceeded.

Next