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",
		"sessionId": "665f1c2ab8d3a2f4e1a9c000",
		"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.

sessionId is the same id returned by createCheckout (session._id) — use it to correlate an event back to the checkout you started, without having to store the orderId mapping yourself. It appears on order.created, order.completed, benefit.granted, and checkout.session.expired.

benefit.granted data: grantId, benefitId, benefitType, orderId, sessionId, 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