Comealong

API и вебхуки

Читайте события, брони и гостей из своего кода, отмечайте гостей своим сканером и узнавайте о каждом изменении сразу.

Получить ключ API

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." } }
400invalid_request: a parameter or the body is wrong.
401unauthorized (no key) or invalid_key (unknown or revoked).
404not_found, ticket_not_found. An event of another account is simply not found.
409ticket_not_valid, wrong_day, event_cancelled: the check-in was refused.
429rate_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.confirmedA booking is confirmed: paid by card, a transfer confirmed, a free booking, a guest you added, a sale at the door.
order.cancelledA 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.refundedA booking's money went back (all of it, or the part the refund rule allows: see refund_cents).
order.partially_refundedPart of a booking's money went back and the booking stands.
ticket.checked_inA ticket arrived for the first time: at the door, by the API, or bought at the door.
event.updatedThe 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.