Webhooks
Platzi can notify your systems the moment one of your employees finishes a certifying evaluation, so your compliance records stay in sync without polling our API.
Webhooks are outbound: you do not call an endpoint, you expose one. Platzi
sends an HTTP POST to a URL you own every time a qualifying event happens.
The flow
- You configure it. A company administrator sets the destination URL in the Business dashboard — see Getting set up.
- An employee finishes a certifying evaluation on Platzi: a course exam or a learning-path exam.
- Platzi builds the event with the employee, what was evaluated and the outcome.
- Platzi
POSTs it to your URL, over HTTPS, with your token attached. - Your endpoint answers
2xx. If it does not, Platzi retries on a fixed schedule.
Every attempt is recorded on our side, with the status code and the first 500 characters your endpoint returned — enough to diagnose most rejections without opening a support ticket.
What produces an event
Only certifying evaluations. Practice quizzes and diagnostic assessments run through the same exam interface but grant no certification, so they never produce an event. If they did, you would be recording certifications Platzi never issued.
Getting set up
You configure this yourself, in the Business dashboard under Conexiones. No ticket, no waiting on us.
It is an administrator-only screen. Group managers do not see it, because the endpoint belongs to the company as a whole rather than to any one team.
- Enter the endpoint URL that will receive the
POST. - Save.
That is the whole setup. On that first save Platzi generates your auth token and shows it under the URL, masked. Reveal it, copy it, and teach your endpoint to require it before you point real traffic at this.
From then on the screen shows your URL, your token and a switch to pause delivery. Changes take effect on the next event — nothing to redeploy on either side.
Use the pause switch sparingly: it keeps your URL and token, but events that happen while it is off are not queued and not recorded. They are simply never sent, and there is nothing to recover afterwards.
The URL
| Requirement | Why |
|---|---|
https://, never http:// | The payload carries employee names, emails and whatever custom attributes you configured. It does not travel in the clear. |
| Reachable from the public internet | Platzi calls you from outside your network. An address that only resolves inside your VPN cannot be delivered to. |
| Valid, complete certificate chain | A self-signed or incomplete chain fails the handshake, and the event is retried until it gives up. |
Port 443 if you spell one out | Most URLs need no port at all — HTTPS already means 443. A URL with an explicit port other than 443 is rejected. |
| A final URL, not a redirect | Redirects are not followed. Give us the address that actually answers. |
Editing the URL later is safe: your token stays the same, so your endpoint keeps accepting the calls without any change on your side.
The token
Platzi generates it — you do not choose it. It is a long random string
starting with pltz_whk_, and it arrives on every request:
POST /your/endpoint HTTP/1.1
Authorization: Bearer pltz_whk_XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx
User-Agent: Platzi-Webhooks/1.0
X-Platzi-Event-Id: 9f8e7d6c-5b4a-3210-9876-543210fedcba
Content-Type: application/json
Your endpoint's job is to compare that header against the value in your
dashboard and refuse anything that does not match. Use a constant-time
comparison — hmac.compare_digest in Python, crypto.timingSafeEqual in
Node — rather than ==, so the comparison itself does not leak the token one
character at a time.
It is not a show-once secret. It stays visible in the dashboard, so losing your copy is not a lockout: open the screen, reveal it, copy it again.
Rejecting drops the event. A 401 is a 4xx, and a 4xx is never
retried — see Retries. So if your endpoint answers 401 because
your side has the wrong value stored, the certification is not queued for
later, it is gone until someone re-sends it by hand. Deploy the token on your
side before you rely on it, not after.
Changing it
The dashboard does not rotate the token, on purpose: there is no button that can invalidate a working integration by accident. To change it, ask your account manager.
Plan for a brief gap when you do. There is no window where both the old and
the new token are accepted, so any event landing between our change and your
deploy gets a 401.
Do not use the pause switch to cover that gap. A 401 is at least
recorded: the delivery exists on our side and your account manager can re-send
it once you are ready. An event that happens while delivery is paused is not
recorded at all — there is nothing to re-send, because as far as the
integration is concerned it never had a destination. Take the 401s and ask
for a re-send; do not take the silence.
If your webhook predates this screen
Whatever token you chose back then is kept. The dashboard shows it and does not replace it, so nothing about your integration changes.
The one exception is a webhook that was configured with no token at all.
The first time anyone saves from this screen, one is generated, and your
endpoint starts receiving an Authorization header where it previously
received none. Ignoring an unexpected header is harmless — but it is the
moment to start checking it.
Why you want a token
Your endpoint is on the public internet. Without a token, anything that
discovers the URL can POST a fabricated certification into your compliance
system, and you have no way to tell it apart from a real one. A URL is not a
secret: it leaks into proxy logs, load balancer logs, browser history and
error reports.
The token turns that around — reject any request whose Authorization header
does not match, and forged calls stop at your front door.
Two things to be clear about, so it is used correctly:
- Treat it like a password. Copy it from the dashboard into the same secret store you keep your other credentials in — not into a repository, a ticket or a chat thread.
- It authenticates the caller, not the body. This is a shared secret sent over TLS, not a per-request cryptographic signature. It proves the request came from someone who knows the token; it does not independently prove the payload was untampered.
Event types
event_type reads as <what was evaluated>.<outcome>.
event_type | Meaning |
|---|---|
course.certified | Passed a course exam. The certification was granted. |
course.failed | Took a course exam and did not pass. |
course.expired | Ran out of time on a course exam. |
learning_path.certified | Passed a learning-path exam. |
learning_path.failed | Took a learning-path exam and did not pass. |
learning_path.expired | Ran out of time on a learning-path exam. |
An exam the employee opened and simply walked away from does not produce an event. Only a finished attempt — passed, failed, or timed out — counts.
New event types may be added over time. Ignore the ones you do not recognise rather than failing the request.
Payload
{
"transaction_id": "9f8e7d6c-5b4a-3210-9876-543210fedcba",
"event_type": "course.certified",
"target_system": null,
"occurred_at": "2026-08-04T15:04:05+00:00",
"user": {
"id": 123456,
"email": "ana@acme.com",
"name": "Ana Díaz",
"metadata": { "rfc": "ABC123456XYZ", "numero_asesor": "7788" }
},
"company_id": 8488,
"course": {
"id": 4321,
"name": "Curso de Cumplimiento Normativo",
"learning_path_id": null
},
"result": {
"status": "approved",
"score": 9.5,
"score_scale": 10,
"approved": true,
"certification_obtained": true,
"completed_at": "2026-08-04T15:04:05+00:00"
}
}
Fields
| Field | Description |
|---|---|
transaction_id | Unique id for this delivery. Same value as the X-Platzi-Event-Id header. Use it to reconcile on your side. |
event_type | See Event types. |
target_system | null on the standard contract. Only set if Platzi built a custom payload format for a specific system of yours. |
occurred_at | When the evaluation ended. ISO-8601 with timezone. |
user.id | The employee's Platzi user id. Stable across events. |
user.email | The email the employee uses on Platzi. |
user.metadata | The custom employee attributes your company configured (for example rfc, numero_asesor). Only the fields you defined are sent, exactly as you defined them — this is usually your join key back to your own HR records. |
company_id | Your Platzi company id. Relevant if you route more than one company into the same endpoint. |
course.id / course.name | The course that was evaluated. Set on course.* events, null on learning_path.* events. |
course.learning_path_id | The learning path that was evaluated. Set on learning_path.* events, null otherwise. |
result.status | approved (passed), failed (took it and did not pass), expired (ran out of time). |
result.score | The grade obtained. Meaningless without score_scale — see below. |
result.score_scale | The maximum possible score. 10 means score is out of 10. |
result.approved | Whether the attempt was a pass. |
result.certification_obtained | Whether the employee actually earned the certification. This is the field to write into a compliance record. |
result.completed_at | When the attempt was completed. |
scoreis not a percentage. Platzi grades on a 0–10 scale, so a9.5is 95%, not 9.5%. Always readscoretogether withscore_scaleand convert explicitly. Assuming a 0–100 scale is the most common integration bug here.
Fields may be added to the payload over time. Ignore the ones you do not recognise rather than failing the request.
Headers
| Header | Description |
|---|---|
Content-Type | Always application/json. |
User-Agent | Platzi-Webhooks/1.0. |
X-Platzi-Event-Id | Stable id for this event. Identical across retries — this is what you deduplicate on. |
X-Platzi-Event-Type | Same value as event_type in the body. Lets you route without parsing the body. |
Authorization | Bearer <token> — the token shown in your dashboard. |
What your endpoint should validate
On every request, in this order:
- The token. Compare the
Authorizationheader against the token shown in your dashboard, using a constant-time comparison. Reject with401if it does not match. - Whether you have seen
X-Platzi-Event-Idbefore. Delivery is at-least-once: a network hiccup after your2xxlooks identical to a failure from our side, so the same event can arrive more than once. Deduplicate on this id and answer2xxto the repeat. event_type. Handle the types you care about; ignore unknown ones with a2xx. Returning an error for an unrecognised type only fills your retry queue.company_id, if a single endpoint serves several Platzi companies.- Which object was evaluated. On
learning_path.*eventscourse.idandcourse.namearenull— readcourse.learning_path_idinstead. Code that assumescourse.idis always present will break the first time an employee takes a learning-path exam. result.scoreagainstresult.score_scalebefore storing or comparing it to a passing threshold.occurred_at, not arrival time. Retries mean events can arrive out of order. If you keep a per-employee latest state, order byoccurred_at.
Then:
- Answer
2xxwithin 10 seconds. Acknowledge first and do your processing asynchronously. A slow endpoint is treated exactly like a broken one. - Do not rely on redirects. They are not followed. Give us the final URL.
Retries
| Your response | What happens |
|---|---|
2xx | Delivered. Done. |
5xx, timeout, connection error | Retried after 30s, 2m, 10m, 1h, 6h — 6 attempts in total. |
429, 408 | Retried, same schedule. |
Any other 4xx | Not retried. A 4xx means the request itself was rejected, and sending the same bytes again cannot fix that. |
That last row is worth internalising: if your endpoint answers 400 or 403
because of a bug, a bad deploy or an expired token, the event is dropped
after the first attempt. It is still stored on our side and can be re-sent
manually, but nothing happens automatically. When in doubt, answer 503 and
let the retry schedule work.
After the 6 attempts are exhausted the event is marked as failed and kept for manual re-sending.
When something goes wrong
Every attempt — successful or not — is logged on the Platzi side with the timestamp, the status code your endpoint returned, and a snippet of its response body. Your account manager can, on request:
- Send a test event with fictitious data to your endpoint, through the exact same path a real event takes. Use this to validate a new URL, a token change or a certificate renewal before real traffic depends on it.
- Pull the delivery log for a date range, so you can reconcile against what your side actually received.
- Re-send a failed event, once you have fixed the outage, the certificate or the URL.
Common causes, in rough order of frequency:
| Symptom | Usual cause |
|---|---|
| Nothing ever arrives | The evaluation was not a certifying one, or the configuration is not active yet. |
401 / 403 in the log | Token mismatch — your endpoint is checking against an older value than the one in the dashboard. |
| Timeouts | The endpoint processes synchronously and exceeds 10 seconds. |
| TLS errors in the log | Expired, self-signed, or incomplete certificate chain. |
| Duplicated records on your side | Missing deduplication on X-Platzi-Event-Id. |