Skip to content
Tickply Developers
Documentation menu: Webhooks
On this page

Webhooks

When something happens, Tickply sends a signed POST to the address you choose. Add the address in Settings → Technical → Integrations, under Notify another system, or with the webhooks scope, and choose its events. Up to 20 addresses per organizer.

The address must be public https, with no user name or password, and never an internal network. The whsec_ secret is shown when you create the address and when you change it; changing it stops the old one at once.

What we send

A POST with a JSON body:

{
    "id": "4b6f6f4e-1c2d-4e5f-8a9b-0c1d2e3f4a5b",
    "event": "order.paid",
    "occurred_at": "2026-11-01T12:00:00Z",
    "organization": { "id": 12, "slug": "your-organizer" },
    "data": {
        "item_id": 34,
        "origin": "checkout",
        "total_cents": 10000,
        "currency": "BRL",
        "subject": { "type": "order", "id": 56 }
    }
}

data holds ids and simple values, never card data or personal data: use the ids to read the rest from the API. id is the event: one event sent to two addresses has the same id.

Header Content
X-Tickply-Event The event name
X-Tickply-Delivery The delivery id, the same on every attempt: use it to skip repeats
X-Tickply-Timestamp Seconds since the epoch
X-Tickply-Signature t=<timestamp>,v1=<HMAC-SHA256 of "<timestamp>.<body>" with the secret>
User-Agent Tickply-Webhooks/1.0

Verifying the signature

Use the raw body, before parsing it, compare in constant time and reject anything older than 300 seconds.

Node:

import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(secret, rawBody, header) {
    const [, t, v1] = header.match(/^t=(\d+),v1=([a-f0-9]{64})$/) ?? [];
    if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
    const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
    return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

Python:

import hashlib, hmac, re, time

def verify(secret: str, raw_body: bytes, header: str) -> bool:
    match = re.fullmatch(r"t=(\d+),v1=([a-f0-9]{64})", header)
    if not match or abs(time.time() - int(match[1])) > 300:
        return False
    expected = hmac.new(secret.encode(), match[1].encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, match[2])

PHP:

function verify(string $secret, string $rawBody, string $header): bool
{
    if (preg_match('/^t=(\d+),v1=([a-f0-9]{64})$/', $header, $match) !== 1 || abs(time() - (int) $match[1]) > 300) {
        return false;
    }

    return hash_equals(hash_hmac('sha256', $match[1].'.'.$rawBody, $secret), $match[2]);
}

Replying and retries

Answer any 2xx within 10 seconds (5 to connect), and do the slow work afterwards. Anything else is a failure, a redirect included: we do not follow it.

We try again after 1, 5, 30, 120, 360 and 720 minutes: up to 7 sends in all, with the same delivery id. The history, in Integrations or at GET /webhook-endpoints/{webhook}/deliveries, shows the reply of the last attempt, and any delivery can be sent once more by hand, with the same id.

After 20 failures in a row the address is paused (each attempt counts, automatic and manual retries included; any success starts the count again), and the owner is told. Nothing new is sent to a paused address. Resume it after fixing it, and send again by hand what you missed.

A webhook failure never undoes the sale, check-in or certificate that caused it. Deliveries are kept for 90 days.

Events

Choose the events of each address. GET /webhook-events lists them all, with available. Every body carries subject in data, plus the fields of its event.

  • order.paid

    subject order

    A purchase was confirmed: at checkout, at the box office or as a complimentary order. origin says which.

    item_id origin total_cents currency

  • order.canceled

    subject order

    An order was canceled: a complimentary one by the team, or a paid one by a refund or a fraud review.

    item_id origin

  • order.expired

    subject order

    An order expired without payment.

    item_id

  • participant.registered

    subject participant

    A ticket or spot got a person: at purchase, in a complimentary order, or when a ticket bought "to be defined" got a name.

    item_id order_id ticket_id offer_id

  • participant.transferred

    subject participant

    A ticket went to another person. The new ticket replaces the previous one.

    item_id order_id ticket_id previous_ticket_id offer_id

  • participant.checked_in

    subject participant

    The first check-in of a ticket on a day of the event; again only after it was undone.

    item_id ticket_id check_in_id session_id method

  • course.completed

    subject enrollment

    A student completed a course.

    item_id ticket_id participant_id

  • certificate.issued

    subject certificate

    A certificate was issued, or corrected into a new version.

    item_id ticket_id type activity_id code version

  • certificate.revoked

    subject certificate

    A certificate was revoked.

    item_id ticket_id type activity_id code version

  • event.published

    subject event

    An event was published.

    slug

  • event.unpublished

    subject event

    An event was taken off sale or archived.

    slug

  • interest.registered

    subject interest

    Someone confirmed their place on the interest list of an item.

    item_id

  • submission.decided

    subject submission

    The result of a scientific submission was sent to its authors.

    item_id number decision

Arriving with upcoming features

Declared, listed with available: false, and not yet possible to choose. They start firing when the feature exists, and the changelog says when.

  • order.refunded: An order was refunded in full.
  • order.partially_refunded: Part of an order was refunded.
  • order.disputed: The buyer disputed the payment with the card issuer.
  • order.dispute_closed: A payment dispute was closed.
  • event.canceled: An event was canceled.
  • payout.paid: A payout reached the organizer.
  • seller.kyc_updated: The state of the payout account changed.