/eventsevents:readList events
Parameters
| limit | query | integer | Page size. |
| starting_after | query | string | Cursor: the next_cursor of the previous page. |
Responses: 200 · 400 · 401 · 403 · 429
A REST API over HTTPS with JSON bodies. Every API key belongs to one organizer account and only sees that account's events, registrations, check-ins and abstracts.
https://ntl.events/api/v1OpenAPI 3.1 speccurl https://ntl.events/api/v1/events \
-H "Authorization: Bearer ntl_live_…"curl https://ntl.events/api/v1/registrations \
-H "Authorization: Bearer ntl_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: import-2027-000123" \
-d '{
"event_id": "8c3f…",
"registration_type_id": "1f0a…",
"buyer": { "name": "Ingrid Halvorsen", "email": "ingrid@example.org" },
"answers": { "organization": "Norwegian Defence Research Establishment" }
}'Send Authorization: Bearer ntl_live_… (or X-API-Key). A missing, invalid, expired or revoked key returns 401; a key without the required scope returns 403. Keys are stored as a SHA-256 hash: we cannot recover a lost key, only revoke it and create another.
| Scope | Allows |
|---|---|
| events:read | Read events and registration types |
| registrations:read | Read registrations and attendees |
| registrations:write | Create (import) and cancel registrations |
| checkins:read | Read check-ins |
| checkins:write | Record check-ins |
| abstracts:read | Read abstracts |
| abstracts:write | Decide abstracts |
| reports:read | Export reports |
Lists return { object: "list", data, has_more, next_cursor }, newest first. Pass next_cursor as starting_after to get the next page. limit goes from 1 to 100 (default 25). Cursors are stable even if new records arrive while you page.
curl "https://ntl.events/api/v1/registrations?event_id=8c3f…&limit=100&starting_after=eyJ0Ijoi…" \
-H "Authorization: Bearer ntl_live_…"Every error has the same shape. type is the stable family to program against; code is the specific case; request_id helps our support team find the request.
HTTP/1.1 409 Conflict
{
"error": {
"type": "conflict_error",
"code": "sold_out",
"message": "Not enough capacity left in this registration type.",
"request_id": "req_3f9c…"
}
}Each key allows 120 requests per minute by default. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; a 429 includes Retry-After.
Send an Idempotency-Key header on POST requests. If the network fails and you retry with the same key, you get the original response (with Idempotent-Replayed: true) instead of a duplicate registration or check-in. Reusing a key with a different body returns 422. Keys are kept for 24 hours.
Amounts and dates. Amounts are integers in minor units (cents) with their ISO 4217 currency. Dates are ISO 8601 in UTC.
Register an HTTPS endpoint and the events you want in the dashboard. Each delivery is a signed POST with the same object the API returns for that resource. Respond 2xx within 10 seconds. Failed deliveries are retried after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h — 8 attempts in total — and you can retry any delivery by hand from the dashboard.
registration.created — Registration confirmedregistration.cancelled — Registration cancelled or refundedpayment.completed — Payment completedcheckin.created — Check-in recordedabstract.decided — Abstract decidedNTL-Signature: t=<unix seconds>,v1=<hex> where v1 = HMAC-SHA256(your whsec_ secret, "<t>.<raw body>"). Compare in constant time and reject if the timestamp is more than 5 minutes away from your clock. Use the raw body, before any JSON parsing.
// Node.js
import crypto from "node:crypto";
export function verifyNtlSignature(secret, header, rawBody, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = Buffer.from(crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"), "hex");
const received = Buffer.from(parts.v1 ?? "", "hex");
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}POST /your/endpoint
NTL-Signature: t=1789200000,v1=5f2b…
NTL-Event-Type: registration.created
NTL-Event-Id: evt_9a1c…
{
"id": "evt_9a1c…",
"object": "event",
"type": "registration.created",
"created": 1789200000,
"data": { "object": { "id": "…", "object": "registration", "status": "confirmed", "attendees": [ … ] } }
}The reference is in English and is generated from the same OpenAPI document the API serves.
/eventsevents:readParameters
| limit | query | integer | Page size. |
| starting_after | query | string | Cursor: the next_cursor of the previous page. |
Responses: 200 · 400 · 401 · 403 · 429
/events/{id}events:readParameters
| id | path · required | string (uuid) | Event id. |
Responses: 200 · 401 · 403 · 404 · 429
/events/{id}/registration-typesevents:readParameters
| id | path · required | string (uuid) | Event id. |
Responses: 200 · 401 · 403 · 404 · 429
/registrationsregistrations:readParameters
| event_id | query | string (uuid) | Only this event. |
| status | query | pending | confirmed | failed | refunded | cancelled | |
| query | string (email) | Buyer email (exact, case-insensitive). | |
| limit | query | integer | Page size. |
| starting_after | query | string | Cursor: the next_cursor of the previous page. |
Responses: 200 · 400 · 401 · 403 · 429
/registrationsregistrations:writeCreates a confirmed registration without charging, e.g. to migrate registrations from another platform or add guests. Capacity is reserved atomically: it can never oversell. Paid registrations go through the NTL Events checkout (Stripe). Fires registration.created.
Parameters
| Idempotency-Key | header | string | Retry-safe key. The first response is replayed for 24 h; reusing the key with a different body returns 422. |
Body
| event_id | required | string (uuid) | |
| registration_type_id | required | string (uuid) | |
| buyer | required | object | |
| quantity | integer | ||
| attendees | array | One per ticket; defaults to the buyer. | |
| answers | object | ||
| language | en | es |
Responses: 201 · 400 · 401 · 403 · 404 · 409 · 422 · 429
/registrations/{id}registrations:readParameters
| id | path · required | string (uuid) | Registration id. |
Responses: 200 · 401 · 403 · 404 · 429
/registrations/{id}/cancelregistrations:writeVoids the attendees' tickets, releases the capacity and notifies the waitlist. Paid registrations return 409 refund_required: they are refunded from the dashboard so the money goes back through the payment processor. Fires registration.cancelled.
Parameters
| id | path · required | string (uuid) | Registration id. |
| Idempotency-Key | header | string | Retry-safe key. The first response is replayed for 24 h; reusing the key with a different body returns 422. |
Responses: 200 · 401 · 403 · 404 · 409 · 422 · 429
/check-inscheckins:readParameters
| event_id | query | string (uuid) | Only this event. |
| limit | query | integer | Page size. |
| starting_after | query | string | Cursor: the next_cursor of the previous page. |
Responses: 200 · 400 · 401 · 403 · 429
/check-inscheckins:writeBy qr_code (what a scanner reads) or attendee_id. Same rules as the NTL Events scanner: signed QR, confirmed registration, ticket for this date, and an atomic mark (two scanners cannot admit the same ticket). Fires checkin.created.
Parameters
| Idempotency-Key | header | string | Retry-safe key. The first response is replayed for 24 h; reusing the key with a different body returns 422. |
Body
| qr_code | string | Ticket QR payload. | |
| attendee_id | string (uuid) | Attendee id. |
Responses: 201 · 400 · 401 · 403 · 404 · 409 · 422 · 429
/abstractsabstracts:readParameters
| event_id | query | string (uuid) | Only this event. |
| status | query | submitted | in_review | accepted | rejected | |
| limit | query | integer | Page size. |
| starting_after | query | string | Cursor: the next_cursor of the previous page. |
Responses: 200 · 400 · 401 · 403 · 429
/abstracts/{id}abstracts:readParameters
| id | path · required | string (uuid) | Abstract id. |
Responses: 200 · 401 · 403 · 404 · 429
/abstracts/{id}/decisionabstracts:writeThe note is shown to the author. Recorded in the account's access log. Fires abstract.decided.
Parameters
| id | path · required | string (uuid) | Abstract id. |
| Idempotency-Key | header | string | Retry-safe key. The first response is replayed for 24 h; reusing the key with a different body returns 422. |
Body
| decision | required | accepted | rejected | in_review | |
| note | string | Visible to the author. |
Responses: 200 · 400 · 401 · 403 · 404 · 422 · 429
/reports/registrationsreports:readOne row per attendee with the registration data and each form answer as answer:<key>. CSV with BOM (opens in Excel), formula-safe cells.
Parameters
| event_id | query · required | string (uuid) | |
| format | query | csv | json |
Responses: 200 · 400 · 401 · 403 · 404 · 429
/reports/monthlyreports:readPer event and month: registrations, tickets by type, gross, NTL fee, processor fee, refunds, net and check-ins. Figures the processor could not provide are null with a reason.
Parameters
| month | query | string | YYYY-MM. Defaults to the current month. |
Responses: 200 · 400 · 401 · 403 · 404 · 429
| id | string (uuid) |
| object | "event" |
| slug | string |
| title | string |
| starts_at | string | null (date-time) |
| ends_at | string | null (date-time) |
| venue | string | null |
| city | string | null |
| currency | string |
| status | string |
| visibility | string | null |
| requires_approval | boolean |
| created_at | string | null (date-time) |
| id | string (uuid) |
| object | "registration_type" |
| event_id | string |
| name | string |
| description | string | null |
| price | integer |
| currency | string |
| capacity | integer |
| sold | integer |
| available | integer |
| active | boolean |
| sales_start | string | null (date-time) |
| sales_end | string | null (date-time) |
| id | string (uuid) |
| object | "attendee" |
| name | string | null |
| string | null | |
| qr_code | string |
| seat | string | null |
| checked_in | boolean |
| checked_in_at | string | null (date-time) |
| voided | boolean |
| id | string (uuid) |
| object | "registration" |
| event_id | string |
| status | pending | confirmed | failed | refunded | cancelled |
| registration_type_id | string | null |
| registration_type_name | string | null |
| buyer | object |
| quantity | integer |
| amount_subtotal | integer |
| amount_total | integer |
| currency | string |
| answers | object |
| source | checkout | api |
| created_at | string | null (date-time) |
| confirmed_at | string | null (date-time) |
| cancelled_at | string | null (date-time) |
| attendees | array |
| id | string |
| object | "check_in" |
| attendee_id | string |
| registration_id | string |
| event_id | string |
| attendee | object |
| checked_in_at | string | null (date-time) |
| source | scanner | api |
| id | string (uuid) |
| object | "abstract" |
| event_id | string |
| code | string |
| title | string |
| abstract | string |
| keywords | string | null |
| authors | array |
| contact | object |
| file_name | string | null |
| status | submitted | in_review | accepted | rejected |
| decision_note | string | null |
| decided_at | string | null (date-time) |
| submitted_at | string | null (date-time) |
| id | string |
| object | "event" |
| type | registration.created | registration.cancelled | payment.completed | checkin.created | abstract.decided | ping |
| created | integer |
| data | object |