Simpli API Documentation
Technical reference for public organizer and collection endpoints and outbound webhooks.
Public API Reference
Simpli sends a signed POST request to your HTTPS endpoint whenever a new registration is created for one of your events. Use it to react to sign-ups from your own website, Discord bot or CRM without polling.
https://your-domain.com/webhooks/simpliregistration.createdapplication/jsonA registration is created when a free order is completed or a paid order is confirmed by Stripe. Simpli queues one delivery per matching active endpoint and a worker sends it out within about a minute. Any 2xx response marks the delivery as complete. Any other status, a connection error or a timeout counts as a failure and the delivery is retried on the schedule above until it succeeds or runs out of attempts.
| Name | Description |
|---|---|
| X-Simpli-Event | The event type. Currently always registration.created. |
| X-Simpli-Delivery | Unique ID of this delivery. Retries of the same delivery reuse the same ID, so store it to dedupe. |
| X-Simpli-Timestamp | Unix epoch time in milliseconds when the request was signed. Reject requests older than a few minutes to protect against replays. |
| X-Simpli-Signature | sha256=<hex>, the HMAC-SHA256 of "{timestamp}.{rawBody}" computed with your endpoint's signing secret. |
{
"id": "7f1c2b4e-3d6a-4c8f-9e21-5a7b3c9d0e12",
"type": "registration.created",
"version": 1,
"createdAt": 1762646400000,
"event": {
"id": "0f8fad5b-d9cb-469f-a165-70867728950e",
"name": "Spring Gala 2027",
"customURL": "spring-gala-2027"
},
"order": {
"id": "9c858901-8a57-4791-81fe-4c455b099bc9",
"status": "Paid",
"amountPaid": 50,
"userName": "Jane Doe",
"userEmail": "jane@example.com",
"boughtAt": 1762646398000
},
"attendees": [
{
"id": "3b241101-e2bb-4255-8caf-4136c566a962",
"name": "Jane Doe",
"firstName": "Jane",
"lastName": "Doe",
"email": "jane@example.com",
"ticketId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"ticketName": "General Admission",
"checkedIn": false,
"customAnswers": {
"1a2b3c4d-0000-4000-8000-000000000001": {
"question": {
"id": "1a2b3c4d-0000-4000-8000-000000000001",
"title": "Dietary restrictions",
"required": false,
"type": "text"
},
"textAnswer": "Vegetarian"
}
}
},
{
"id": "0e3f5a7c-9b1d-4e2f-8a6b-7c9d1e3f5a7c",
"name": "Sam Lee",
"firstName": "Sam",
"lastName": "Lee",
"email": "sam@example.com",
"ticketId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"ticketName": "General Admission",
"checkedIn": false,
"customAnswers": null
}
]
}| Path | Type | Description |
|---|---|---|
| id | string | Unique ID of this webhook event. Stable across retries. |
| type | "registration.created" | Event type. Currently always registration.created. |
| version | number | Payload schema version. Currently 1. |
| createdAt | number | When the payload was generated, in milliseconds. |
| test | boolean | undefined | Present and true only on sample payloads sent with Send Test from the dashboard. Verify them but do not act on them. |
| event.id | string | Event ID. |
| event.name | string | Event name. |
| event.customURL | string | Public slug of the event. The event page is https://simpli.events/e/{customURL}. |
| order.id | string | Order ID. One order can hold several attendees. |
| order.status | "Paid" | "Processing" | "Failed" | null | Order status. Paid for completed orders, including free ones. |
| order.amountPaid | number | Total the buyer paid for the order, in dollars. 0 for free orders. |
| order.userName | string | Name of the buyer. |
| order.userEmail | string | Email of the buyer. |
| order.boughtAt | number | null | When the order was completed, in milliseconds. |
| attendees | array<Attendee> | One entry per ticket in the order. |
| attendees[].id | string | Attendee ID. Also the value encoded in the attendee's ticket QR code. |
| attendees[].name | string | Full name of the attendee. |
| attendees[].firstName | string | null | First name of the attendee when collected separately. |
| attendees[].lastName | string | null | Last name of the attendee when collected separately. |
| attendees[].email | string | Email of the attendee. |
| attendees[].ticketId | string | ID of the ticket type the attendee holds. |
| attendees[].ticketName | string | Name of the ticket type at the time of purchase. |
| attendees[].checkedIn | boolean | Whether the attendee has been checked in. Always false on registration.created. |
| attendees[].customAnswers | object | null | Answers to the ticket's custom questions keyed by question ID, or null when the ticket has no questions. |
| attendees[].customAnswers[questionId].question | object | The question as it was asked, with its id, title, type, required flag and options. |
| attendees[].customAnswers[questionId].textAnswer | string | undefined | Answer for text, textarea, number, email, date and select questions. |
| attendees[].customAnswers[questionId].checkAnswer | boolean | undefined | Answer for checkbox questions. |
| attendees[].customAnswers[questionId].selectedOptions | string[] | undefined | Selected options for checkbox_group questions. |
type RegistrationCreatedPayload = {
id: string;
type: 'registration.created';
version: number;
createdAt: number;
test?: boolean;
event: {
id: string;
name: string;
customURL: string;
};
order: {
id: string;
status: 'Paid' | 'Processing' | 'Failed' | null;
amountPaid: number;
userName: string;
userEmail: string;
boughtAt: number | null;
};
attendees: Array<{
id: string;
name: string;
firstName: string | null;
lastName: string | null;
email: string;
ticketId: string;
ticketName: string;
checkedIn: boolean;
customAnswers: {
[questionId: string]: {
question: {
id: string;
title: string;
required: boolean;
type:
| 'text'
| 'number'
| 'email'
| 'date'
| 'checkbox'
| 'checkbox_group'
| 'select'
| 'textarea';
options?: string[];
};
textAnswer?: string;
checkAnswer?: boolean;
selectedOptions?: string[];
};
} | null;
}>;
};Every request is signed with the signing secret shown when you create or rotate an endpoint. Verify the signature before trusting a payload so that nobody can forge or replay registrations against your receiver.
Read the raw body
Keep the request body exactly as received. Do not parse and re-serialize it, since key order and whitespace would change the bytes.
Compute the expected signature
Concatenate the X-Simpli-Timestamp header, a period and the raw body, then compute HMAC-SHA256 with your signing secret and hex-encode it.
Compare in constant time
Compare "sha256=" plus your hex digest against the X-Simpli-Signature header with a constant-time comparison.
Reject stale timestamps
Reject the request if X-Simpli-Timestamp is more than about five minutes away from your current time.
const crypto = require('crypto');
const TOLERANCE_MS = 5 * 60 * 1000;
function verifySimpliWebhook(rawBody, headers, signingSecret) {
const timestamp = headers['x-simpli-timestamp'];
const signature = headers['x-simpli-signature'];
if (!timestamp || !signature) return false;
if (Math.abs(Date.now() - Number(timestamp)) > TOLERANCE_MS) return false;
const expected =
'sha256=' +
crypto
.createHmac('sha256', signingSecret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const a = Buffer.from(signature);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express example: keep the raw body, verify, then respond 2xx quickly
app.post(
'/webhooks/simpli',
express.raw({ type: 'application/json' }),
(req, res) => {
const rawBody = req.body.toString('utf8');
if (!verifySimpliWebhook(rawBody, req.headers, process.env.SIMPLI_SIGNING_SECRET)) {
return res.status(401).end();
}
const payload = JSON.parse(rawBody);
if (payload.test) {
return res.status(200).end();
}
// Dedupe on payload.id, then handle payload.attendees
res.status(200).end();
}
);Send Test on an endpoint in the dashboard signs and posts a sample payload immediately, without creating a real registration or waiting for the queue. Test payloads use fake data and carry "test": true at the top level. Verify them exactly like real deliveries, then ignore them.
Add an endpoint
Open Manage, then Settings, then Webhooks on any of your events and add your HTTPS URL. Choose whether the endpoint receives registrations for that event only or for all of your events.
Store the signing secret
The secret is shown once when the endpoint is created. Save it in your receiver's configuration and keep it out of client-side code.
Send a test delivery
Use Send Test to confirm your receiver accepts the request and verifies the signature.
Rotate or pause when needed
Rotate Secret issues a new secret and invalidates the old one immediately. The switch on each endpoint pauses deliveries without deleting the endpoint.