Skip to content

Receiving platform webhooks (consumer guide)

How an external app (viite.ai first; trading next) receives outbound *.published webhooks from the platform, verifies them, and acts. Pairs with the sender: apps/api/webhook/ and the SDK client.webhooks.

On a matching domain event the platform sends an HTTP POST to your registered URL:

HeaderExampleMeaning
Content-Typeapplication/jsonBody is JSON.
X-Platform-Eventblog.publishedThe event type.
X-Platform-Event-Idevt_…Unique event id (use to dedupe).
X-Platform-Signaturesha256=<hex>HMAC-SHA256 of the raw body with your subscription secret.

Body (thin snapshot — pull the full entity from the public API if you need more):

{
"eventId": "evt_…",
"eventType": "blog.published",
"occurredAt": "2026-06-14T12:00:00.000Z",
"aggregateId": "art_…",
"data": { "id": "art_…", "channel": "blog", "userId": "", "slug": "my-post" }
}

Recompute the HMAC over the exact raw body bytes (do not re-serialize parsed JSON — key order/whitespace would differ) and compare in constant time. This is the mirror of apps/api/webhook/signing.ts.

import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyPlatformSignature(secret: string, rawBody: string, header: string): boolean {
const expected = `sha256=${createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex')}`;
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(header, 'utf8');
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);
}

Reference endpoint (verify → trigger rebuild)

Section titled “Reference endpoint (verify → trigger rebuild)”

viite.ai is a static site, so the action is “rebuild myself”: verify, then call your own deploy hook (Coolify/Netlify). Env: WEBHOOK_SIGNING_SECRET (the secret returned once at subscription creation) and DEPLOY_HOOK_URL (your host’s build hook).

import type { APIRoute } from 'astro';
import { createHmac, timingSafeEqual } from 'node:crypto';
export const prerender = false; // server route, not statically built
export const POST: APIRoute = async ({ request }) => {
const raw = await request.text();
const sig = request.headers.get('X-Platform-Signature') ?? '';
const secret = import.meta.env.WEBHOOK_SIGNING_SECRET ?? process.env.WEBHOOK_SIGNING_SECRET ?? '';
const expected = `sha256=${createHmac('sha256', secret).update(raw, 'utf8').digest('hex')}`;
const a = Buffer.from(expected), b = Buffer.from(sig);
if (a.length !== b.length || !timingSafeEqual(a, b)) {
return new Response('bad signature', { status: 401 });
}
// Only act on the events you care about.
if (request.headers.get('X-Platform-Event') === 'blog.published') {
const hook = import.meta.env.DEPLOY_HOOK_URL ?? process.env.DEPLOY_HOOK_URL;
if (hook) await fetch(hook, { method: 'POST' }); // fire the rebuild
}
return new Response('ok', { status: 200 }); // ack fast; non-2xx makes us retry
};
export async function handlePlatformWebhook(req: Request): Promise<Response> {
const raw = await req.text();
if (!verifyPlatformSignature(process.env.WEBHOOK_SIGNING_SECRET!, raw, req.headers.get('X-Platform-Signature') ?? ''))
return new Response('bad signature', { status: 401 });
if (req.headers.get('X-Platform-Event') === 'blog.published')
await fetch(process.env.DEPLOY_HOOK_URL!, { method: 'POST' });
return new Response('ok', { status: 200 });
}

Notes:

  • Respond 2xx fast. Any non-2xx (or a timeout > 10s) is treated as failure and retried by the sender (retryLimit: 5, exponential backoff). Do the rebuild fire-and-forget; don’t block the response on it.
  • Idempotency: the same event id can arrive more than once (retry after your ack was lost). Dedupe on X-Platform-Event-Id if a double rebuild matters.

The secret is generated by the platform and shown once at creation — so register first, then put the returned secret into your receiver’s env.

Terminal window
# 1. Register against prod (api key OR session). Save the `secret` from the response.
curl -sS -X POST https://platform.viite.ai/api/webhooks/subscriptions \
-H "Authorization: Bearer $PLATFORM_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"eventType":"blog.published","url":"https://viite.ai/api/webhooks/blog","description":"viite.ai rebuild"}'
# → { "data": { "id": "whsub_…", "secret": "whsec_…", ... } }
# 2. Set WEBHOOK_SIGNING_SECRET=<that secret> + DEPLOY_HOOK_URL=<your build hook> in viite.ai env; deploy.
# 3. Publish a blog post, then check deliveries:
curl -sS https://platform.viite.ai/api/webhooks/deliveries \
-H "Authorization: Bearer $PLATFORM_API_KEY" # status should be "success"

Or via the SDK:

const { secret } = await client.webhooks.create({
eventType: 'blog.published',
url: 'https://viite.ai/api/webhooks/blog',
description: 'viite.ai rebuild',
});
// store `secret` in viite.ai env — it is not retrievable again

To rotate the secret: delete the subscription and create a new one (the secret is immutable), then update the receiver env.