Webhooks
Webhooks notify your backend when Perkamo records events and effects. Use them for fulfillment, CRM sync and operational automation that should happen outside the synchronous API request.
Configure endpoints
Webhook endpoints must be public HTTPS URLs.
Create, edit, test, disable or remove endpoints in Webhooks. Each Space can have multiple active endpoints. Perkamo creates one delivery per matching active endpoint.
text
https://api.example.com/webhooks/perkamoEndpoint event scopes can be * for all events or one event key per line, such as purchase.completed or reward.redeemed. Removing an endpoint disables new deliveries and keeps existing delivery history.
Delivery payload
json
{
"id": "evt_01J...",
"type": "purchase.completed",
"created_at": "2026-06-01T10:00:00.000Z",
"data": {
"space": {
"id": "7c2fc0dd-86af-44f4-a1b0-99b602f99c0f",
"name": "Commerce Live",
"environment": "live"
},
"user_id": "customer_123",
"transaction_id": "order_1092",
"event": "purchase.completed",
"status": "applied",
"occurred_at": "2026-06-01T10:00:00.000Z",
"context": { "amount": 12900, "currency": "CZK" },
"delta": [{ "wallet": "loyalty_points", "amount": 10 }],
"effects": [],
"wallet_state": { "loyalty_points": 120 }
}
}Headers
Perkamo sends these headers:
| Header | Meaning |
|---|---|
user-agent | Perkamo-Webhook/1.0 |
x-perkamo-delivery | Unique delivery id (use as the idempotency key). |
x-perkamo-event | Event type. |
x-perkamo-space | Space ID. |
x-perkamo-attempt | Attempt number. |
x-perkamo-timestamp | Unix seconds when the delivery was signed. |
x-perkamo-signature | v1=<hex> HMAC-SHA256 signature (see below). |
Verify signatures
Every delivery is signed so your handler can confirm it genuinely came from Perkamo and has not been replayed. When you create an endpoint, the API returns a one-time signing_secret (whsec_...) — store it securely; it is never shown again.
The signature is v1= followed by the hex HMAC-SHA256 of ${timestamp}.${body}, keyed by the signing secret, where timestamp is the x-perkamo-timestamp header value and body is the exact raw request body.
Using the server SDK:
ts
import { verifyWebhookSignature } from "@perkamo/sdk";
// `rawBody` must be the exact bytes received (do not re-serialize the JSON).
const ok = await verifyWebhookSignature({
secret: process.env.PERKAMO_WEBHOOK_SECRET!,
payload: rawBody,
signatureHeader: req.headers["x-perkamo-signature"],
timestampHeader: req.headers["x-perkamo-timestamp"],
toleranceSeconds: 300, // reject deliveries older than 5 minutes
});
if (!ok) return res.status(400).end();Using the PHP SDK:
php
use Perkamo\WebhookSignature;
// $rawBody must be the exact bytes received (do not re-serialize the JSON).
$ok = WebhookSignature::verify(
secret: getenv('PERKAMO_WEBHOOK_SECRET'),
payload: $rawBody,
signatureHeader: $request->headers->get('x-perkamo-signature'),
timestampHeader: $request->headers->get('x-perkamo-timestamp'),
toleranceSeconds: 300, // reject deliveries older than 5 minutes
);
if (!$ok) {
return new Response(status: 400);
}Verifying manually (any language):
text
expected = "v1=" + hex(hmac_sha256(secret, timestamp + "." + rawBody))Compare expected to x-perkamo-signature in constant time, and reject requests whose x-perkamo-timestamp is outside your tolerance window. Keep using HTTPS endpoints only and treat the endpoint URL as non-public.
Handler requirements
- Return any
2xxstatus for success. - Respond within 5 seconds.
- Store
x-perkamo-deliveryand make processing idempotent. - Do not assume deliveries arrive exactly once.
- Alert on repeated failures before launching a campaign that depends on fulfillment.
Retries
Delivery status is pending, delivered or failed. Retry scheduling is shown as a delivery property while attempts remain. Delivery history records status code, status text, duration and last error for console debugging.
Use the Webhooks delivery history to inspect failures and resend a delivery after your handler is fixed.