Webhooks
Integrations — Webhooks
Instead of polling, subscribe an HTTPS endpoint and Corteksa will push a
signed HTTP POST to it whenever a record changes. An app registers its own
subscriptions with its API key (crtk_live_…) — the same key it uses to read
and write records. One key can hold many subscriptions.
Scope today: the objects (records) module —
record.created,record.updated,record.deleted. More modules (messaging, email, …) are added over time; callGET /webhooks/eventsfor the live catalog.
1. Subscribe
POST /api/v1/webhooks/subscriptions
Authorization: Bearer crtk_live_…
Content-Type: application/json
{
"object_slug": "tasks",
"events": ["record.created", "record.updated"],
"target_url": "https://api.your-app.com/corteksa/webhook"
}201 response — the signing secret is shown exactly once. Store it now.
{
"message": "Webhook subscription created. Copy the signing secret now — it will not be shown again.",
"data": {
"signing_secret": "whsec_1a2b3c…",
"subscription": {
"slug": "a1b2c3d4e5f6",
"name": "Tasks → api.your-app.com",
"object_slug": "tasks",
"events": ["record.created", "record.updated"],
"target_url": "https://api.your-app.com/corteksa/webhook",
"is_active": true,
"created_at": "2026-07-13T10:00:00.000Z"
}
}
}target_urlmust be HTTPS.eventsmust be names from the catalog (GET /api/v1/webhooks/events).- The key must have read access to the object (scope
read.<object_slug>) — a webhook only ever carries data the key could already read. Otherwise403. - Subscriptions are addressed by
slug, never a numeric id.
Manage
| Method & path | Does |
|---|---|
GET /api/v1/webhooks/events | List subscribable event names |
GET /api/v1/webhooks/subscriptions | List your subscriptions |
GET /api/v1/webhooks/subscriptions/:slug | Get one |
DELETE /api/v1/webhooks/subscriptions/:slug | Delete one |
A subscription is owned by the credential that created it — you only ever see
and manage your own. Either credential type works: an API key (crtk_live_…)
or an OAuth token (crtk_oauth_…, owned by the OAuth app installation). A
human (JWT) token is rejected (403) — these endpoints are for apps.
2. Receive a delivery
Corteksa sends a POST with a JSON body and these headers:
| Header | Example | Meaning |
|---|---|---|
X-Corteksa-Event | record.updated | The event that fired |
X-Corteksa-Delivery | 4021 | Unique per delivery attempt (idempotency key) |
X-Corteksa-Signature | t=1752400000,v1=9f86d0… | HMAC — see below |
Body:
{
"event": "record_updated",
"event_type": "update",
"timestamp": "2026-07-13T10:05:00.000Z",
"object": "tasks",
"record_id": 812,
"record_slug": "tsk_9f2c",
"changed_fields": { "status": { "old": "open", "new": "done" } },
"full_record": { "slug": "tsk_9f2c", "title": "Ship it", "status": "done" }
}full_record and changed_fields are keyed by field slug. Respond 2xx
quickly; anything else (or a timeout) is retried (see §4).
3. Verify the signature
The X-Corteksa-Signature header is t=<unix-seconds>,v1=<hex>, where v1 is
HMAC-SHA256(signing_secret, "<t>.<raw-request-body>"). Recompute it over the
raw body (do not re-serialize) and compare in constant time. Reject if t
is older than, say, 5 minutes to stop replays.
import { createHmac, timingSafeEqual } from 'crypto';
function verify(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // replay guard
const expected = createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 ?? '');
return a.length === b.length && timingSafeEqual(a, b);
}4. Delivery, retries, and loops
- Retries: a failed delivery (non-
2xxor timeout) is retried with exponential backoff. Make your handler idempotent usingX-Corteksa-Delivery. - No echo of your own writes: an event caused by a write your key made is not delivered back to your subscription. So a two-way sync won't loop — your writes into Corteksa never bounce back to you. (Writes by other keys/users still notify you, as expected.)
- Disconnecting: delete the subscription, or revoke the API key — either stops deliveries.
5. Security checklist
- Treat
signing_secretlike a password; store it server-side. - Always verify
X-Corteksa-Signaturebefore trusting a payload. - Enforce the timestamp replay window.
- Only accept HTTPS callbacks.