Skip to main content
Version: Latest

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​

FieldTypeDescription
_idObjectIdThe certificate award. Stable: the expiry notification carries the same value
_userIdObjectIdThe learner who earned the certificate (internal identifier)
_badgeIdObjectIdThe certificate definition
_ownerIdObjectIdThe Beedeez account
nameStringCertificate name at the time it was earned
ruleTypeStringCertificate rule type, for example certifCapsule or certifSkill
creationDateISO 8601 DateWhen the learner met the certificate requirements
expiresISO 8601 DateExpiry date (start of day). null or absent when the certificate has no validity duration
expiryReminderSentBooleanfalse on the award notification, true on the expiry notification
disabledBooleantrue when the award is superseded by a renewal, revoked, or already expired when it was created
acknowledgedBooleanWhether the learner has seen the certificate in the app
createdAtISO 8601 DateWhen the award record was created
updatedAtISO 8601 DateWhen the award record was last updated
descriptionStringOptional. Certificate description
name_i18n, description_i18nArrayOptional. Translations, as { "lang", "value" } objects
scoreNumberOptional. Present when the certificate rule produced a score
successRateNumberOptional. Present when the certificate rule produced a success rate
correctAnswersRateNumberOptional. Present when the certificate rule produced a correct-answers rate
_lessonActiveIdsArray\<ObjectId>Optional. The capsule attempts that validated the certificate
lang, svgIcon, imagesVariousOptional. 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 (ruleType is certifCapsule)
  • the certificate has a validity duration
  • the expiry reminder is switched on for the certificate
  • the award's expires date 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​

  • _id is the same as on the original award notification
  • expiryReminderSent is true
  • expires is in the past
  • updatedAt reflects 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"
}
danger

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​

  1. Create a new POST request
  2. Set the URL to your webhook endpoint
  3. Add the authentication header you configured (Bearer token, API key, etc.)
  4. Set Content-Type to application/json
  5. Paste an example payload in the body
  6. 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​

Stuck on an integration?
Write to the developer team with your request ID and we'll get back to you quickly.