Webhooks
Tell your system automatically about every order, registration, cancellation or check-in.
With webhooks, Nyebar tells your system as soon as something happens, for example to add buyers to your CRM, send a welcome WhatsApp message from your own system, or update an attendance board. Your system doesn’t have to keep asking the API.
Webhooks are set up by the owner or an Admin in the Integrations menu. The last part of this article is for the developer who sets up the receiver.
Events you can send
| Event | When it’s sent |
|---|---|
Order confirmed (order.confirmed) | An order is paid (or free) and its tickets are issued. This includes free registrations you approve. |
Registration received (registration.received) | A new registration waits for your approval. |
Order cancelled (order.cancelled) | A registration is rejected, or an order expires or its payment fails. |
Ticket checked in (ticket.checked_in) | A ticket is checked in, from the dashboard, the crew link or the API. |
Adding a webhook
-
Open Integrations, then under Webhooks click Add webhook.
Your webhooks on the Integrations page. -
Fill in the Endpoint URL, the address in your system that receives deliveries. It must be https and reachable from the internet.
-
Pick the Events to send, then click Save.
Enter the URL and pick the events to send. -
Copy the secret that appears and set it up in your system to verify deliveries. You can also see the secret again on the webhook’s page.
An organizer can have up to 5 webhooks.
Watching deliveries
In the webhook list, each row has two icons:
- Deliveries (clock icon): deliveries from the last 30 days. Click one to see what was sent and how your system answered, and Send again if needed.
- Settings (gear icon): change the URL, description or events with Edit Endpoint, turn the webhook off for a while, see or rotate the Signing secret, and delete the webhook.
At the top of both pages, Send test checks that your endpoint receives deliveries correctly.
If your system doesn’t answer successfully, Nyebar retries automatically after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours. If deliveries to a webhook keep failing for over a day, the webhook is turned off automatically and the owner and Admins get a notification in the dashboard. Once the endpoint is fixed, send a test and turn it back on. Events from while it was off are still sent once it’s back on, as long as they’re less than 30 days old.
For developers
Delivery format
Nyebar sends a POST with JSON to your URL, with these headers:
| Header | Value |
|---|---|
Nyebar-Event-Id | The event ID, for example evt_1042. The same for every retry. |
Nyebar-Event-Type | The event type, for example order.confirmed. |
Nyebar-Signature | The signature, t=<unix time>,v1=<hex>. |
{
"id": "evt_1042",
"type": "order.confirmed",
"created_at": "2026-10-11T14:36:46Z",
"data": {
"event": { "id": 134, "title": "Nusantara City Run 10K", "slug": "nusantara-city-run-10k" },
"order": {
"id": 1230,
"code": "ORDRHMRQERV",
"status": "confirmed",
"buyer_name": "Andi Nasution",
"buyer_email": "andi@example.com",
"ticket_subtotal": 200000,
"addon_subtotal": 0,
"discount_amount": 0,
"platform_fee_amount": 0,
"total_price": 200000,
"created_at": "2026-10-11T14:30:02Z"
},
"tickets": [
{
"id": 1583,
"code": "NYE8KRXGKU8",
"holder_name": "Andi Nasution",
"holder_email": "andi@example.com",
"ticket_type": { "id": 235, "name": "Reguler" },
"bib_number": null,
"checked_in_at": null,
"created_at": "2026-10-11T14:36:46Z"
}
]
}
}
The data of each event:
order.confirmed:event,orderand the issuedtickets.registration.received:eventandorder(statusawaiting_approval).order.cancelled:event,orderandreason:rejected,expired(the approval or payment deadline passed),payment_failedorpayment_expired.ticket.checked_in:event,ticketandsource:dashboard,crew_link,apiorsupport.
order and ticket have the same shape as in the Organizer API.
Answering deliveries
Answer with a 2xx status within 10 seconds. If your system needs longer, store the delivery first, answer 200, and process it afterwards. Any other status, a timeout or a redirect counts as a failure and is retried.
A delivery can arrive more than once (for example on a retry), and the order isn’t guaranteed. Use the event id to skip deliveries you’ve already processed, and created_at for ordering.
Verifying the signature
Compute the HMAC-SHA256 of <t>.<raw body> with the webhook’s secret and compare it with the v1 value. Also reject deliveries whose t is more than 5 minutes from now.
// Node.js (Express): use the raw body, not the result of JSON.parse
const crypto = require("crypto");
app.post("/webhooks/nyebar", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("Nyebar-Signature") || "";
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = crypto
.createHmac("sha256", process.env.NYEBAR_WEBHOOK_SECRET)
.update(`${parts.t}.${req.body}`)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
if (!fresh || !parts.v1 || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))) {
return res.sendStatus(400);
}
const event = JSON.parse(req.body);
// ... handle the event ...
res.sendStatus(200);
});
<?php
// PHP
$body = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_NYEBAR_SIGNATURE'] ?? ''), $parts);
$expected = hash_hmac('sha256', ($parts['t'] ?? '') . '.' . $body, getenv('NYEBAR_WEBHOOK_SECRET'));
if (abs(time() - (int) ($parts['t'] ?? 0)) > 300 || !hash_equals($expected, $parts['v1'] ?? '')) {
http_response_code(400);
exit;
}
$event = json_decode($body, true);
// ... handle the event ...
http_response_code(200);
# Python (Flask)
import hashlib, hmac, os, time
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/webhooks/nyebar")
def nyebar_webhook():
parts = dict(p.split("=", 1) for p in request.headers.get("Nyebar-Signature", "").split(",") if "=" in p)
body = request.get_data()
expected = hmac.new(os.environ["NYEBAR_WEBHOOK_SECRET"].encode(), parts.get("t", "").encode() + b"." + body, hashlib.sha256).hexdigest()
if abs(time.time() - int(parts.get("t", 0))) > 300 or not hmac.compare_digest(expected, parts.get("v1", "")):
abort(400)
event = request.get_json()
# ... handle the event ...
return "", 200
If you rotate the secret with Rotate secret, the old secret stops working right away. Update the secret in your system right after.
Was this article helpful?
Thanks for your feedback!
Thanks. We’ll work on improving this article.
Still need help?
The Nyebar team can help over WhatsApp or email. Other ways to reach us