Payload data
The envelope
Every delivery is a JSON POST with the same envelope, whatever the event:
{
"event": "attempt.graded",
"timestamp": "2026-10-18T12:51:30+00:00",
"site": "https://your-site.example",
"data": { … }
}
| Field | Type | Meaning |
|---|---|---|
event |
string | The event name. The same as the X-NextExams-Event header |
timestamp |
string | When the event was sent, ISO 8601 with offset |
site |
string | The sending site's root address, so one receiver can serve several sites |
data |
object | The event's own fields, below |
Headers
| Header | Value |
|---|---|
Content-Type |
application/json |
X-NextExams-Event |
The event name |
X-NextExams-Signature |
sha256= followed by the hex HMAC of the body. See Verifying signatures |
X-NextExams-Delivery |
The delivery's id. The same on every retry of one delivery, so dedupe on it. Not sent on a ping |
User-Agent |
NextExams-Webhook/1.0 |
What data contains
attempt.submitted and attempt.graded
{
"attempt_id": 263,
"exam_id": 7,
"exam_title": "Safety induction",
"user_id": 412,
"guest_email": null,
"examinee_name": "Dana Kovacs",
"attempt_number": 1,
"status": "graded",
"passed": true,
"is_graded": true,
"score": 18,
"max_score": 20,
"percent": 90,
"submitted": "2026-10-18 12:51:30",
"result_url": "https://your-site.example/exams/result/263"
}
| Field | Meaning |
|---|---|
attempt_id |
The attempt. Fetch it in full with GET /attempts/{id} |
exam_id, exam_title |
The exam |
user_id |
The examinee's Joomla user id, or null for a guest |
guest_email |
A guest's email, if the exam asked for one |
examinee_name |
The user's name, or the name a guest gave |
attempt_number |
1 for their first attempt at this exam |
status |
submitted while a question waits for a person to grade it; graded once final |
passed, is_graded |
Booleans. passed means nothing until is_graded is true |
score, max_score, percent |
Numbers, or null before grading |
submitted |
UTC, as YYYY-MM-DD HH:MM:SS |
result_url |
The examinee's result page |
Note: these values are sent whatever the exam's result-release policy says. A receiver that shows them to the examinee should respect the policy itself.
certificate.issued
{
"certificate_id": 91,
"number": "2026/0042",
"attempt_id": 263,
"exam_id": 7,
"exam_title": "Safety induction",
"user_id": 412,
"guest_email": null,
"recipient_name": "Dana Kovacs",
"issued_at": "2026-10-18 12:51:31",
"expires_at": null
}
expires_at is null for a certificate that never expires. Both dates are UTC, as YYYY-MM-DD HH:MM:SS.
ping
{ "message": "NextExams webhook test ping.", "webhook_id": 3 }
Handling deliveries
- Verify the signature first, before parsing. See Verifying signatures.
- Answer quickly with a 2xx, then do the slow work in the background. A delivery that takes longer than 8 seconds counts as failed and is sent again.
- Dedupe on
X-NextExams-Delivery. A retry carries the same id. - Ignore fields you do not recognise. New ones may be added.