Payload Examples
This page shows the webhook body for each case your endpoint receives. Use these examples to build and test your handler.
Request Structure
Every notification is an HTTP POST request:
HTTP Method: POST
Content-Type: application/json
Authentication: the header of the method configured on the endpoint (Bearer, Basic Auth, API Key or Custom Header)
Request Headers Example
POST /webhooks/beedeez HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer your-secret-bearer-token
No other header is added by Beedeez: there is no source header, no signature header and no delivery identifier.
Body
The body is the certificate award record, serialized as JSON, with no envelope: no event name, no send timestamp, no delivery identifier. Identifiers are 24-character hexadecimal strings and dates are ISO 8601 strings.
Certificate Awarded
Sent under the event type certificate_completed when a learner meets a certificate rule, or when Beedeez runs a catch-up pass that creates missing awards (one request per award created).
Payload Example
{
"_id": "6712a9f4c3b5e10d2f8a4471",
"_ownerId": "5f2c1ab8d9e4470012a3c001",
"_userId": "66e1b0447a2c9d0011f4e882",
"_badgeId": "64c73f10ae5b220019d0a1c7",
"name": "Workplace Safety Certificate",
"name_i18n": [
{ "lang": "en", "value": "Workplace Safety Certificate" },
{ "lang": "fr", "value": "Certificat Sécurité au travail" }
],
"description": "Complete all workplace safety modules",
"ruleType": "certifCapsule",
"creationDate": "2026-03-11T09:42:18.204Z",
"expires": "2027-03-11T00:00:00.000Z",
"expiryReminderSent": false,
"disabled": false,
"acknowledged": false,
"successRate": 92,
"_lessonActiveIds": ["66f0d2213b8c4a0014ee7710"],
"createdAt": "2026-03-11T09:42:18.209Z",
"updatedAt": "2026-03-11T09:42:18.209Z",
"__v": 0
}
Field Descriptions
| Field | Type | Description |
|---|---|---|
_id | ObjectId | The certificate award. Stable: the expiry notification carries the same value |
_userId | ObjectId | The learner who earned the certificate (internal identifier) |
_badgeId | ObjectId | The certificate definition |
_ownerId | ObjectId | The Beedeez account |
name | String | Certificate name at the time it was earned |
ruleType | String | Certificate rule type, for example certifCapsule or certifSkill |
creationDate | ISO 8601 Date | When the learner met the certificate requirements |
expires | ISO 8601 Date | Expiry date (start of day). null or absent when the certificate has no validity duration |
expiryReminderSent | Boolean | false on the award notification, true on the expiry notification |
disabled | Boolean | true when the award is superseded by a renewal, revoked, or already expired when it was created |
acknowledged | Boolean | Whether the learner has seen the certificate in the app |
createdAt | ISO 8601 Date | When the award record was created |
updatedAt | ISO 8601 Date | When the award record was last updated |
description | String | Optional. Certificate description |
name_i18n, description_i18n | Array | Optional. Translations, as { "lang", "value" } objects |
score | Number | Optional. Present when the certificate rule produced a score |
successRate | Number | Optional. Present when the certificate rule produced a success rate |
correctAnswersRate | Number | Optional. Present when the certificate rule produced a correct-answers rate |
_lessonActiveIds | Array\<ObjectId> | Optional. The capsule attempts that validated the certificate |
lang, svgIcon, images | Various | Optional. Display data stored on the award |
The record may also contain internal fields such as __v. Ignore fields you do not recognize.
The body carries no identity data: no name, email address, login or employee number. To resolve the learner, call the public API: GET /v1/certificates/{CERTIFICATE_ID}, with the award _id as CERTIFICATE_ID, returns the same award with the learner's email, login and externalId, and the list endpoint GET /v1/certificates supports updatedSince for a pull-based sync. See the API reference.
Use Case Example
Scenario: record a certification in your HR system.
// Node.js webhook handler
app.post('/webhooks/beedeez/certificates', async (req, res) => {
const award = req.body;
// Acknowledge within 5 seconds
res.status(200).json({ received: true });
if (award.expiryReminderSent) {
return; // Expiry notification, handled elsewhere
}
// Resolve the learner through the public API
const certificate = await beedeezApi.get(`/v1/certificates/${award._id}`);
await hrSystem.updateEmployeeRecord({
externalId: certificate.user.externalId,
certification: {
name: award.name,
completedDate: award.creationDate,
expiryDate: award.expires,
awardId: award._id,
},
});
});
Certificate Expired
Beedeez never sends the certificate_expired event type. When an award passes its expiry date, the notification is sent under certificate_completed, with the same body shape and expiryReminderSent set to true.
An hourly job sends this notification once per award, together with the expiry reminder email, when all of the following hold:
- the certificate is a per-capsule certificate (
ruleTypeiscertifCapsule) - the certificate has a validity duration
- the expiry reminder is switched on for the certificate
- the award's
expiresdate is in the past and no expiry reminder has been sent for it yet
Other certificates produce no notification when they expire. The public API (GET /v1/certificates) returns expired as the status of those awards once their expiry date is reached; the API names that field expiresAt, the webhook body names it expires.
Payload Example
{
"_id": "6712a9f4c3b5e10d2f8a4471",
"_ownerId": "5f2c1ab8d9e4470012a3c001",
"_userId": "66e1b0447a2c9d0011f4e882",
"_badgeId": "64c73f10ae5b220019d0a1c7",
"name": "Workplace Safety Certificate",
"ruleType": "certifCapsule",
"creationDate": "2026-03-11T09:42:18.204Z",
"expires": "2027-03-11T00:00:00.000Z",
"expiryReminderSent": true,
"disabled": false,
"acknowledged": true,
"successRate": 92,
"_lessonActiveIds": ["66f0d2213b8c4a0014ee7710"],
"createdAt": "2026-03-11T09:42:18.209Z",
"updatedAt": "2027-03-11T08:00:02.517Z",
"__v": 0
}
Differences from the Award Notification
_idis the same as on the original award notificationexpiryReminderSentistrueexpiresis in the pastupdatedAtreflects the time the expiry was processed
Use Case Example
Scenario: start a re-certification workflow when a certificate expires.
// Node.js webhook handler
app.post('/webhooks/beedeez/certificates', async (req, res) => {
const award = req.body;
res.status(200).json({ received: true });
if (!award.expiryReminderSent) {
return; // Award notification, handled elsewhere
}
await workflowEngine.start({
workflow: 'recertification',
userId: award._userId,
certificateDefinitionId: award._badgeId,
certificateName: award.name,
expiredDate: award.expires,
});
});
Handling Both Cases
Both cases arrive on an endpoint subscribed to certificate_completed. Tell them apart with expiryReminderSent.
Example: Single Handler
app.post('/webhooks/beedeez', async (req, res) => {
const award = req.body;
res.status(200).json({ received: true });
if (award.expiryReminderSent) {
await handleCertificateExpired(award);
} else {
await handleCertificateAwarded(award);
}
});
async function handleCertificateAwarded(award) {
console.log('Certificate awarded:', award._id);
// Handle award logic...
}
async function handleCertificateExpired(award) {
console.log('Certificate expired:', award._id);
// Handle expiry logic...
}
Do not infer the case by comparing expires with the current time only: an award can be created with an expires date already in the past (it then has disabled: true), and expiry notifications are processed hourly, not at the exact expiry time.
Responses
Return a 2xx status to acknowledge the request. The response body is ignored.
Success Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"received": true
}
Error Responses
Any status outside 200-299, or no response within 5 seconds, counts as a failure:
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"error": "Unauthorized"
}
Beedeez does not retry failed deliveries. Whatever status you return, a notification that fails is not sent again. Acknowledge as soon as the request is authenticated and stored, then process it, and reconcile periodically through GET /v1/certificates with updatedSince to recover anything you missed.
Idempotency Handling
The same notification can occasionally be delivered more than once. The expiry notification reuses the _id of the award, so deduplicate on the pair _id + expiryReminderSent, not on _id alone.
Example: Idempotent Handler
const processedKeys = new Set(); // In production, use a database
app.post('/webhooks/beedeez', async (req, res) => {
const award = req.body;
const key = `${award._id}:${award.expiryReminderSent ? 'expired' : 'awarded'}`;
if (processedKeys.has(key)) {
return res.status(200).json({ received: true, duplicate: true });
}
processedKeys.add(key);
res.status(200).json({ received: true });
await processAward(award);
});
Database-Based Idempotency
app.post('/webhooks/beedeez', async (req, res) => {
const award = req.body;
const key = `${award._id}:${award.expiryReminderSent ? 'expired' : 'awarded'}`;
// Unique index on `key`: a duplicate insert fails
const inserted = await db.webhookEvents
.insertOne({ key, receivedAt: new Date(), payload: award, processed: false })
.then(() => true)
.catch((error) => {
if (error.code === 11000) return false; // Duplicate key
throw error;
});
res.status(200).json({ received: true, duplicate: !inserted });
if (inserted) {
await processAward(award);
await db.webhookEvents.updateOne({ key }, { $set: { processed: true } });
}
});
Storing the payload before processing lets you replay it on your side if processing fails, since Beedeez will not send it again.
Testing Webhooks
Manual Testing with curl
# Award notification
curl -X POST https://api.example.com/webhooks/beedeez \
-H "Authorization: Bearer your-token" \
-H "Content-Type: application/json" \
-d '{
"_id": "6712a9f4c3b5e10d2f8a4471",
"_ownerId": "5f2c1ab8d9e4470012a3c001",
"_userId": "66e1b0447a2c9d0011f4e882",
"_badgeId": "64c73f10ae5b220019d0a1c7",
"name": "Test Certificate",
"ruleType": "certifCapsule",
"creationDate": "2026-03-11T09:42:18.204Z",
"expires": "2027-03-11T00:00:00.000Z",
"expiryReminderSent": false,
"disabled": false,
"acknowledged": false,
"_lessonActiveIds": ["66f0d2213b8c4a0014ee7710"],
"createdAt": "2026-03-11T09:42:18.209Z",
"updatedAt": "2026-03-11T09:42:18.209Z"
}'
Send the same body with "expiryReminderSent": true to test the expiry case.
Testing with Postman
- Create a new
POSTrequest - Set the URL to your webhook endpoint
- Add the authentication header you configured (Bearer token, API key, etc.)
- Set
Content-Typetoapplication/json - Paste an example payload in the body
- Send the request and verify the response
To test end to end, trigger a real certificate award on a test account: the back office has no test button.
Complete Integration Example
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
const EXPECTED_AUTH = `Bearer ${process.env.BEEDEEZ_WEBHOOK_TOKEN}`;
function safeEqual(a, b) {
const bufA = Buffer.from(a || '');
const bufB = Buffer.from(b || '');
return bufA.length === bufB.length && crypto.timingSafeEqual(bufA, bufB);
}
// Middleware: authentication (the only proof of origin; requests are not signed)
function authenticateWebhook(req, res, next) {
if (!safeEqual(req.headers.authorization, EXPECTED_AUTH)) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
}
// Middleware: payload validation
function validatePayload(req, res, next) {
const award = req.body;
if (!award._id || !award._userId || !award._badgeId || !award._ownerId) {
return res.status(400).json({ error: 'Invalid payload' });
}
next();
}
app.post(
'/webhooks/beedeez',
authenticateWebhook,
validatePayload,
async (req, res) => {
const award = req.body;
const isExpiry = award.expiryReminderSent === true;
const key = `${award._id}:${isExpiry ? 'expired' : 'awarded'}`;
// Store first (unique index on `key`), then acknowledge within 5 seconds
const isNew = await storeIfNew(key, award);
res.status(200).json({ received: true, duplicate: !isNew });
if (!isNew) return;
try {
if (isExpiry) {
await handleCertificateExpired(award);
} else {
await handleCertificateAwarded(award);
}
await markProcessed(key);
} catch (error) {
// Beedeez will not resend: keep the stored payload for a local replay
console.error('Error processing webhook', key, error);
}
},
);
async function handleCertificateAwarded(award) {
// Resolve the learner (email, login, externalId) through GET /v1/certificates/{id}
// then update your HR system
}
async function handleCertificateExpired(award) {
// Mark the certification as expired and start a re-certification workflow
}
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Webhook server listening on port ${PORT}`);
});
Next Steps
- Review the Configuration Guide to set up your webhook
- Learn about Authentication to secure your endpoint
- Read Event Types to understand what is emitted
- Check the Introduction for webhook concepts