English
English
Appearance
English
English
Appearance
Webhooks notify your application in real time when things happen to your credentials — when one is issued, delivered, revoked, or viewed. Instead of polling the API, you register an HTTPS endpoint and badges.ninja sends it a signed POST for each event you subscribe to.
All management endpoints require authentication via the X-Api-Key header. See Authentication. Creating or deleting endpoints requires a key with write scope; listing requires read. See API Keys.
POST /webhooks| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Your HTTPS endpoint. Must start with https://. |
events | string[] | No | Event types to subscribe to. Omit or pass ["*"] to receive all events. |
curl -X POST https://api.badges.ninja/webhooks \
-H "X-Api-Key: bws_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/badges",
"events": ["credential.issued", "credential.delivered"]
}'201 Created. The signing secret is returned only once — store it now; you cannot retrieve it again.
{
"id": "e4b19ff5-063d-4799-bd75-d03641be624f",
"url": "https://example.com/hooks/badges",
"events": ["credential.issued", "credential.delivered"],
"secret": "2be6d335682c1658242fde3a523fd8a2493bab8c"
}GET /webhooksReturns your registered endpoints. Signing secrets are never included.
{
"webhooks": [
{
"id": "e4b19ff5-063d-4799-bd75-d03641be624f",
"url": "https://example.com/hooks/badges",
"events": ["credential.issued", "credential.delivered"],
"active": true,
"failureCount": 0,
"createdAt": 1787685415083,
"lastStatus": 200,
"lastDeliveryAt": 1787685480777
}
]
}DELETE /webhooks/{id}curl -X DELETE https://api.badges.ninja/webhooks/e4b19ff5-063d-4799-bd75-d03641be624f \
-H "X-Api-Key: bws_your_api_key_here"Deliveries to the endpoint stop immediately.
| Event | Fires when… |
|---|---|
credential.issued | A credential is issued to a recipient. |
credential.delivered | A credential's notification email is sent to the recipient (single or bulk). |
credential.revoked | A credential is revoked. |
credential.viewed | A recipient's public credential page is viewed for the first time. |
Every delivery is a POST with a JSON body of this shape:
{
"id": "b1c2d3e4-...",
"type": "credential.issued",
"createdAt": 1787685415083,
"data": { }
}| Field | Description |
|---|---|
id | Unique delivery id (use it to de-duplicate). |
type | The event type. |
createdAt | Event timestamp (epoch milliseconds). |
data | Event-specific payload (see below). |
data by event type credential.issued
{
"awardId": "https://api.badges.ninja/certify-badge/award/<guid>",
"badgeId": "https://api.badges.ninja/certify-badge/badge/<guid>",
"badgeName": "Advanced Certification",
"recipient": { "email": "jane@example.com", "name": "Jane Doe" },
"issuedOn": "2026-08-25",
"expires": null
}credential.delivered
{
"awardId": "https://api.badges.ninja/certify-badge/award/<guid>",
"recipient": { "email": "jane@example.com", "name": "Jane Doe" },
"badgeName": "Advanced Certification"
}credential.revoked
{ "awardId": "https://api.badges.ninja/certify-badge/award/<guid>", "reason": "Issued in error" }credential.viewed
{
"awardId": "https://api.badges.ninja/certify-badge/award/<guid>",
"recipient": { "email": "jane@example.com", "name": "Jane Doe" }
}Each delivery carries these headers:
| Header | Description |
|---|---|
X-Bws-Event | The event type (same as type in the body). |
X-Bws-Delivery | The delivery id (same as id in the body). |
X-Bws-Signature | sha256= followed by the HMAC-SHA256 of the raw request body, keyed by your endpoint secret. |
User-Agent | badges.ninja-webhooks/1 |
Always verify the signature before trusting a delivery. Compute the HMAC-SHA256 of the raw request body using your endpoint's signing secret, and compare it (in constant time) to the X-Bws-Signature header.
import crypto from "node:crypto";
function verify(rawBody, signatureHeader, secret) {
const expected = "sha256=" +
crypto.createHmac("sha256", secret).update(rawBody, "utf8").digest("hex");
const a = Buffer.from(signatureHeader);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Use the exact bytes you received — parsing and re-serializing the JSON first will change the bytes and break the signature.
2xx status.failureCount; after 20 consecutive failures an endpoint is automatically disabled (active: false) and stops receiving deliveries until you fix it and register a new endpoint.X-Bws-Delivery id (or the body id) to make your handler idempotent.You can also manage endpoints without the API — open Webhooks in the dashboard sidebar to add, list, and delete endpoints and choose which events each one receives.