JamiDev

Webhooks

Signed event notifications with automatic retries.

Register endpoints in Automation → Webhooks. Each subscription gets a secret (shown once) and a list of events to receive.

Subscriptions belong to the active organization. If you create a sandbox org for testing, register your endpoint there too — events from one org never reach another org's subscriptions.

Events

EventFired when
order.createdAn order is created (payment confirmed)
order.completedThe order is finalized — for one-time products this fires together with order.created
benefit.grantedA benefit (e.g. license key) was granted for a paid order
checkout.session.expiredA session failed, was cancelled, or hit its 30-minute TTL without payment

Payload envelope

{
	"id": "665f1c2ab8d3a2f4e1a9c001",
	"type": "order.created",
	"created": "2026-07-11T09:30:00.000Z",
	"livemode": false,
	"data": {
		"orderId": "665f1c2ab8d3a2f4e1a9c111",
		"productId": "665f1c2ab8d3a2f4e1a9c222",
		"customerId": "665f1c2ab8d3a2f4e1a9c333",
		"customerEmail": "buyer@example.com",
		"customerExternalId": null,
		"amount": 10000,
		"currency": "ETB",
		"status": "paid",
		"metadata": { "campaign": "launch" },
		"paidAt": "2026-07-11T09:29:58.000Z"
	}
}

livemode is false for sandbox organizations — filter on it if one endpoint serves both environments. amount is what the buyer paid (your listed price), in ETB minor units.

benefit.granted data: grantId, benefitId, benefitType, orderId, customerId, and payload (for license_key benefits, payload.licenseKey holds the key).

Verifying signatures

Every delivery is signed with HMAC-SHA256 over "{timestamp}.{rawBody}" using your subscription secret:

X-JamiDev-Signature: t=1783695518,v1=5257a869e7...
X-JamiDev-Event: order.created

The jami-sdk does the verification (timing-safe compare + replay guard) and returns the typed event:

import { Jami, JamiSignatureError } from 'jami-sdk';

const jami = new Jami({ token: process.env.JAMI_TOKEN! });

const event = await jami.webhooks.verify({
	payload: rawBody, // the RAW request body string
	signature: req.headers['x-jamidev-signature'],
	secret: process.env.JAMI_WEBHOOK_SECRET!,
});
// event.type is 'order.created' | 'order.completed' | 'benefit.granted' | 'checkout.session.expired'

Verifying manually (any language) works the same way:

import crypto from 'node:crypto';

function verifyJamiDevSignature(rawBody: string, header: string, secret: string): boolean {
	const t = /t=(\d+)/.exec(header)?.[1];
	const v1 = /v1=([a-f0-9]+)/.exec(header)?.[1];
	if (!t || !v1) return false;

	// Reject stale timestamps to prevent replay (5 minutes is a sane window)
	if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;

	const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
	return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

Always verify against the raw request body — parsing and re-serializing JSON can change key order and break the signature.

Delivery & retries

  • Respond with any 2xx within 10 seconds to acknowledge.
  • Failed deliveries retry with backoff: 1m → 5m → 30m → 2h → 12h (5 attempts total).
  • Every delivery and its response code is visible on the Webhooks page, with a manual resend button.
  • Handle events idempotently — retries and manual resends mean you may see the same event id more than once.

On this page