API и вебхуки
Читайте события, брони и гостей из своего кода, отмечайте гостей своим сканером и узнавайте о каждом изменении сразу.
Получить ключ API- Authentication
- Errors and limits
- Pagination
- Events
- Orders
- Attendees
- Check-in
- Webhooks
- Verifying signatures
- Retries
- Excel exports
Authentication
Create a key under API and webhooks in your Comealong account (Profile → API and webhooks). The key is shown once; we keep only a hash of it, so a lost key is revoked and replaced, never recovered. Send it on every request:
curl https://comealong.co.nz/api/v1/events \
-H "Authorization: Bearer ca_…"A key acts for the account that made it: it reads the events that account owns and the events it co-organises (not events where it only helps at the door), and it can check guests in. It sees what you see in your guest list and CSV report: names, emails, phone numbers and answers your guests gave. Keep it on a server, never in a web page or an app. The API sends no CORS headers, so browsers cannot call it.
All money is in cents with a currency (always NZD for now). Times are ISO 8601 in UTC. GET https://comealong.co.nz/api/v1/me tells you which account and key you are using.
Errors and limits
Every error has the same shape and an HTTP status that matches it:
{ "error": { "code": "not_found", "message": "No event with this id for this key." } }| 400 | invalid_request: a parameter or the body is wrong. |
|---|---|
| 401 | unauthorized (no key) or invalid_key (unknown or revoked). |
| 404 | not_found, ticket_not_found. An event of another account is simply not found. |
| 409 | ticket_not_valid, wrong_day, event_cancelled: the check-in was refused. |
| 429 | rate_limited: 600 requests a minute per key (and 1,200 per address). Wait for Retry-After seconds. |
Pagination
Lists answer { "object": "list", "data": [...], "has_more": true, "next_cursor": "…" }. Pass cursor=next_cursor for the next page and limit (1–100, default 50) for its size. Orders and attendees come oldest change first and take updated_since (ISO time or Unix seconds), so a sync asks for what changed since its last run. Start that run a few minutes before the last one ended: a booking saved at that moment may carry a slightly earlier time.
Events
GET /api/v1/events
Your events, latest start first. status=active or cancelled to filter.
GET /api/v1/events/:id
One event with its ticket types and counts: sold (confirmed), awaiting (booked, payment not yet confirmed) and checked_in, per type and in total. Add-ons count what was sold and take no place.
{ "id": "2b9e…", "object": "event", "slug": "quiz-night", "url": "https://comealong.co.nz/quiz-night", "title": "Quiz night",
"status": "active", "paused": false, "starts_at": "…", "ends_at": null, "sales_close_at": "…", "place": "…",
"capacity": 120, "currency": "NZD", "created_at": "…", "updated_at": "…",
"ticket_types": [{ "id": "…", "name": "General", "price_cents": 2000, "currency": "NZD", "quantity": null, "addon": false,
"sold": 84, "awaiting": 3, "checked_in": 0 }],
"counts": { "sold": 84, "awaiting": 3, "checked_in": 0 } }Orders
GET /api/v1/events/:id/orders
Bookings of the event, each with its tickets and add-ons. Filters: updated_since, status (comma-separated: awaiting, confirmed, released (the hold ran out unpaid), cancelled, refunded).
{
"id": "6f1c…", "object": "order", "event_id": "2b9e…", "reference": "TX-7KQ2",
"status": "confirmed", "method": "card",
"name": "Anna Petrova", "email": "[email protected]", "phone": "",
"currency": "NZD", "subtotal_cents": 4000, "discount_cents": 0, "donation_cents": 0,
"fee_cents": 180, "card_fee_cents": 0, "total_cents": 4180, "refund_cents": null,
"promo_code": null,
"answers": [{ "question": "Dietary needs", "answer": "Vegetarian" }],
"created_at": "2026-11-03T05:12:09.481Z", "confirmed_at": "2026-11-03T05:13:40.002Z", "updated_at": "2026-11-03T05:13:40.002Z",
"tickets": [{ "id": "…", "object": "ticket", "code": "q8…", "type_id": "…", "type_name": "General",
"holder_name": "Anna Petrova", "checked_in": false, "checked_in_at": null, "checked_in_by": null, … }],
"addons": [{ "type_id": "…", "name": "Parking", "qty": 1, "price_cents": 500 }]
}Attendees
GET /api/v1/events/:id/attendees
One row per ticket of a confirmed booking, as on the door's guest list: the ticket (with its code, the text in its QR), whether and when it was checked in and by whom, the booking's reference, buyer and contact, and the answers. status=all includes tickets of bookings that are not confirmed; checked_in=true|false and updated_since filter.
Check-in
POST /api/v1/events/:id/check-ins
curl -X POST https://comealong.co.nz/api/v1/events/2b9e…/check-ins \
-H "Authorization: Bearer ca_…" -H "Content-Type: application/json" \
-d '{"code": "q8…"}'The door's rules apply: only a ticket of a confirmed booking lets someone in, a festival ticket only on its days, and the first arrival keeps its time. The tick is recorded under the key's name, so the report shows which scanner let each guest in. The answer is { "object": "check_in", "already_checked_in": false, "ticket": {...} }; scanning again is harmless and says already_checked_in: true.
Webhooks
Add an endpoint under API and webhooks and pick what it should hear about. We POST JSON to it within seconds of each change:
order.confirmed | A booking is confirmed: paid by card, a transfer confirmed, a free booking, a guest you added, a sale at the door. |
|---|---|
order.cancelled | A booking is cancelled: by the guest, by you, or because the event was cancelled. A paid transfer booking waiting for its refund is cancelled until the money is sent. |
order.refunded | A booking's money went back (all of it, or the part the refund rule allows: see refund_cents). |
order.partially_refunded | Part of a booking's money went back and the booking stands. |
ticket.checked_in | A ticket arrived for the first time: at the door, by the API, or bought at the door. |
event.updated | The event was saved, paused or resumed, went ahead, or was cancelled. |
Every delivery has the same envelope; data holds an order, a ticket (with its booking) or an event, in the shapes above. "Send test" on the endpoint's page sends webhook.test.
POST /your/endpoint
Content-Type: application/json
Comealong-Signature: t=1762146820,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Comealong-Event-Id: evt_3qK…
Comealong-Event-Type: order.confirmed
{ "id": "evt_3qK…", "type": "order.confirmed", "created": 1762146820, "data": { "order": { … } } }Endpoints must be https:// addresses on the public internet: we refuse names that resolve to private, loopback or link-local addresses, and we connect to the address we checked. Redirects are not followed; answer within 10 seconds with any 2xx status.
Verifying signatures
v1 is the hex HMAC-SHA256 of <t>.<raw body> with your endpoint's signing secret (whsec_…, shown once when you add the endpoint). t is when that try was sent, in Unix seconds. Compare in constant time and refuse old timestamps. In Node:
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
const secret = process.env.COMEALONG_WEBHOOK_SECRET; // whsec_..., shown once when you added the endpoint
const app = express();
// The signature is over the raw body, so read it as bytes, not as parsed JSON.
app.post("/comealong", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("Comealong-Signature") ?? "";
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
const t = Number(parts.t);
const expected = createHmac("sha256", secret).update(`${parts.t}.${req.body}`).digest("hex");
const given = Buffer.from(parts.v1 ?? "", "hex");
const fresh = Math.abs(Date.now() / 1000 - t) < 300; // refuse replays older than five minutes
if (!fresh || given.length !== 32 || !timingSafeEqual(given, Buffer.from(expected, "hex"))) return res.sendStatus(400);
const event = JSON.parse(req.body.toString("utf8"));
// The same event can arrive twice (a retry after a lost answer): keep event.id and skip ones you have seen.
if (event.type === "order.confirmed") console.log("New booking", event.data.order.reference);
res.sendStatus(200); // answer quickly; do slow work after answering
});
app.listen(3000);Retries
A delivery that gets no 2xx answer is tried again after 1 min, 5 min, 30 min, 2 h, 5 h, 10 h, 10 h: 8 tries over about 28 hours. Each try is signed again with a new t; the event's id stays the same, so use it to ignore a repeat. Deliveries are not guaranteed to arrive in order: compare updated_at, or read the current state from the API.
If every try of a delivery fails and nothing reached the endpoint in that time, the endpoint is turned off and we email you. Fix your server and turn it on again on its page; the delivery log there shows each delivery, its last status and error, for 30 days.
Excel exports
No code needed for a spreadsheet: every CSV download in your account (the event report, followers, your audience) also comes as an Excel file (.xlsx), with numbers as numbers and times in New Zealand time.