background
Simpli Logo

Simpli API Documentation

Technical reference for public organizer and collection endpoints and outbound webhooks.

Public API Reference

Webhooks

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.

Delivery

POSThttps://your-domain.com/webhooks/simpli
Event type
registration.created
Content type
application/json
Timeout
10 seconds per attempt
Retries
Up to 6 attempts with exponential backoff (1m, 5m, 30m, 2h, 6h, 24h)

A 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.

Request Headers

NameDescription
X-Simpli-EventThe event type. Currently always registration.created.
X-Simpli-DeliveryUnique ID of this delivery. Retries of the same delivery reuse the same ID, so store it to dedupe.
X-Simpli-TimestampUnix epoch time in milliseconds when the request was signed. Reject requests older than a few minutes to protect against replays.
X-Simpli-Signaturesha256=<hex>, the HMAC-SHA256 of "{timestamp}.{rawBody}" computed with your endpoint's signing secret.

Payload Example

{
  "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
    }
  ]
}

Payload Properties

PathTypeDescription
idstringUnique ID of this webhook event. Stable across retries.
type"registration.created"Event type. Currently always registration.created.
versionnumberPayload schema version. Currently 1.
createdAtnumberWhen the payload was generated, in milliseconds.
testboolean | undefinedPresent and true only on sample payloads sent with Send Test from the dashboard. Verify them but do not act on them.
event.idstringEvent ID.
event.namestringEvent name.
event.customURLstringPublic slug of the event. The event page is https://simpli.events/e/{customURL}.
order.idstringOrder ID. One order can hold several attendees.
order.status"Paid" | "Processing" | "Failed" | nullOrder status. Paid for completed orders, including free ones.
order.amountPaidnumberTotal the buyer paid for the order, in dollars. 0 for free orders.
order.userNamestringName of the buyer.
order.userEmailstringEmail of the buyer.
order.boughtAtnumber | nullWhen the order was completed, in milliseconds.
attendeesarray<Attendee>One entry per ticket in the order.
attendees[].idstringAttendee ID. Also the value encoded in the attendee's ticket QR code.
attendees[].namestringFull name of the attendee.
attendees[].firstNamestring | nullFirst name of the attendee when collected separately.
attendees[].lastNamestring | nullLast name of the attendee when collected separately.
attendees[].emailstringEmail of the attendee.
attendees[].ticketIdstringID of the ticket type the attendee holds.
attendees[].ticketNamestringName of the ticket type at the time of purchase.
attendees[].checkedInbooleanWhether the attendee has been checked in. Always false on registration.created.
attendees[].customAnswersobject | nullAnswers to the ticket's custom questions keyed by question ID, or null when the ticket has no questions.
attendees[].customAnswers[questionId].questionobjectThe question as it was asked, with its id, title, type, required flag and options.
attendees[].customAnswers[questionId].textAnswerstring | undefinedAnswer for text, textarea, number, email, date and select questions.
attendees[].customAnswers[questionId].checkAnswerboolean | undefinedAnswer for checkbox questions.
attendees[].customAnswers[questionId].selectedOptionsstring[] | undefinedSelected options for checkbox_group questions.

Typed Payload Schema

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;
  }>;
};

Verifying the Signature

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.

  1. 1

    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.

  2. 2

    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.

  3. 3

    Compare in constant time

    Compare "sha256=" plus your hex digest against the X-Simpli-Signature header with a constant-time comparison.

  4. 4

    Reject stale timestamps

    Reject the request if X-Simpli-Timestamp is more than about five minutes away from your current time.

Node.js Example

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();
  }
);

Test Deliveries

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.

Setup

  1. 1

    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.

  2. 2

    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.

  3. 3

    Send a test delivery

    Use Send Test to confirm your receiver accepts the request and verifies the signature.

  4. 4

    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.

Notes

  • Timestamps are numbers in milliseconds.
  • order.amountPaid is in dollars, not cents.
  • Deliveries are at-least-once. Dedupe on the top-level id or the X-Simpli-Delivery header.
  • Endpoint URLs must use https. Respond within 10 seconds and do your processing afterwards.
  • An endpoint scoped to all events receives registrations for every event you organize, including events created later.
  • New optional fields may be added without changing version. Ignore fields you do not recognize.