1. Guides
  2. Recipes
  3. Receive webhooks

Receive webhooks

Receive webhooks

Goal: get CRM events (record created / updated / deleted) pushed to your endpoint instead of polling. Time: ~10 min.

You need

  • A public HTTPS endpoint that answers with a 2xx status within 5 seconds.
  • An API key (crtk_live_…) or OAuth token (crtk_oauth_…) with the read.<object_slug> scope for the object you want to watch.

1. Register an endpoint

Create a subscription with your credential. Pick the object and the events you care about:

bash
curl -X POST https://app.corteksa.info/api/v1/webhooks/subscriptions \  -H "X-Api-Key: crtk_live_7f3c…" \  -H "Content-Type: application/json" \  -d '{    "object_slug": "contacts",    "events": ["record.created", "record.updated"],    "target_url": "https://hooks.example.com/corteksa"  }'

A dedicated workspace uses its own subdomain instead of app. The response carries data.signing_secret. It is returned only once, so store it now.

2. Verify the signature

Every delivery carries an X-Corteksa-Signature header of the form t=<unix seconds>,v1=<hex>. v1 is the lowercase hex HMAC-SHA256 of the string <t>.<raw body>: the timestamp, a ., then the exact request body bytes. The key is your whole signing secret, including its whsec_ prefix.

Recompute it over the raw body, before any JSON parsing, and compare in constant time. Reject a t more than 5 minutes from your clock to stop replays. Each retry is signed again with a new t, so this window does not reject retries.

ts
import express from 'express';
import { createHmac, timingSafeEqual } from 'crypto';

const TOLERANCE_SECONDS = 300;

function verify(rawBody: Buffer, header: string, secret: string): boolean {
  const parts = new Map(
    header.split(',').map((part) => {
      const i = part.indexOf('=');
      return [part.slice(0, i), part.slice(i + 1)];
    }),
  );
  const t = Number(parts.get('t'));
  if (!Number.isInteger(t)) return false;
  if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false;

  const expected = createHmac('sha256', secret)
    .update(`${t}.`)
    .update(rawBody)
    .digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.get('v1') ?? '');
  return a.length === b.length && timingSafeEqual(a, b);
}

const app = express();

// express.raw keeps req.body as the exact bytes that were signed.
app.post('/corteksa', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.header('X-Corteksa-Signature') ?? '';
  if (!verify(req.body, header, SIGNING_SECRET)) {
    return res.status(401).end();
  }
  enqueue(JSON.parse(req.body.toString('utf8'))); // do the work async
  res.status(200).end(); // ack within 5 seconds
});

3. Respond fast, work async

Return 200 immediately and process in the background. A delivery is tried up to 3 times: a timeout (5 seconds), a connection failure, a 5xx, a 408 or a 429 is retried with backoff, and any other 4xx stops the retries.

Make your handler idempotent. There is no event id that stays the same across retries: X-Corteksa-Delivery, t and the body's timestamp all change on every attempt. Treat each delivery as a signal and fetch the record by record_slug (GET /api/v1/object/data/:objectSlug/:dataSlug) before you act, so running the same delivery twice does no harm.

Notes

  • This is Corteksa → your app. Provider → Corteksa webhooks (WhatsApp and Facebook replies) are handled by Corteksa itself and need nothing from you.
  • Every event, header and body field, with a signature test vector → Webhook events.
  • Listing and deleting subscriptions → Webhooks.
Create a record< Previous

Powered by heyo-docs

On this page

You need1. Register an endpoint2. Verify the signature3. Respond fast, work asyncNotes