---
title: "API reference (REST API v1)"
description: "REST API v1: authentication, scopes, conventions, cursor-paginated lists, batch create, async imports with pollable jobs, trigger events, and object reference. The API behind Zapier, Make, and n8n."
canonical: "https://agendaforge.app/docs/api"
last-updated: 2026-10-17
---
# API reference (REST API v1)
> REST API v1: authentication, scopes, conventions, cursor-paginated lists, batch create, async imports with pollable jobs, trigger events, and object reference. The API behind Zapier, Make, and n8n.
Canonical page: https://agendaforge.app/docs/api
Last reviewed or updated: 2026-10-17
Content type: page

Last updated: October 17, 2026
Breadcrumb: Home > Developers > API reference
Base URL: https://api.agendaforge.app/api/v1
Machine-readable spec: https://agendaforge.app/openapi.json (OpenAPI 3.1)

The HTTP API that powers the Zapier, Make.com, and n8n integrations. Server-to-server: every request is authenticated with an API key, sends and receives JSON over HTTPS, and is scoped to a single AgendaForge organization.

Authentication: open Settings > API Tokens (or Event Settings > Integrations > Zapier), click Generate key, name it, and copy it. Keys begin with afk_live_ and are shown once; they are stored hashed and can never be displayed again. Send the key as "Authorization: Bearer afk_live_..." or "X-API-Key: afk_live_...". Keys are organization-wide and shared across Zapier, Make.com, and n8n; revoking one disconnects all of them. Only an organization Owner or Admin can create or revoke a key. A missing, invalid, or revoked key returns 401 UNAUTHENTICATED.

Scopes: every key carries named scopes chosen at creation. public_api (on by default) grants the REST API v1 surface; mcp:read and mcp:write grant the AgendaForge MCP server for AI clients (reads, and approval-gated writes), and tickets:import (shown as Import tickets, never on by default) grants POST /imports/tickets. These endpoints require public_api, except POST /imports/tickets, which requires tickets:import; a key without the required scope returns 403 FORBIDDEN. Pick the smallest scope set the integration needs.

Conventions: JSON bodies (send Content-Type: application/json on writes); ISO-8601 UTC timestamps; opaque, case-sensitive string IDs; successful responses return HTTP 200, except POST /imports/contacts, which returns 202 Accepted with a job to poll, and POST /imports/tickets, which does the same for more than 100 tickets. Every error, 4xx and 5xx alike, is JSON with a machine-readable code: { "error": { "code": "...", "message": "..." } }. Codes: 401 UNAUTHENTICATED (key missing, malformed, invalid, or revoked), 402 PAYMENT_REQUIRED (the event is not activated yet; returned by POST /imports/tickets), 403 FORBIDDEN (the key lacks the scope the endpoint needs: public_api, or tickets:import for ticket imports), 400 VALIDATION_ERROR (a required field is missing or invalid, an unknown event was supplied, or a referenced record is not owned by the key's organization), 404 NOT_FOUND (no such endpoint; the response lists the valid ones), 409 CONFLICT (the Idempotency-Key was already used for a different endpoint, or the request cannot run in the current state, for example a ticket import is already running for the event), 429 RATE_LIMITED (the rate window is exhausted; wait Retry-After seconds), 500 INTERNAL (unexpected server failure; retry with exponential backoff).

Pagination: the list endpoints (GET /contacts, GET /sessions, GET /sponsors) are cursor-based. Request a page with ?limit= (1-100, default 25), then follow nextCursor via ?cursor= until hasMore is false. Each page is { data, hasMore, nextCursor }, newest first. Cursors are opaque continuation tokens; never construct one (a malformed cursor returns 400 VALIDATION_ERROR).

Rate limits: 120 requests per minute per API key (fixed window), 30 per minute per IP for unauthenticated requests. Every response carries RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset (seconds until the window resets), and RateLimit-Policy headers so clients can self-throttle; exceeding a window returns 429 RATE_LIMITED with a Retry-After header.

Versioning and deprecation: the API is versioned in the URL path (/api/v1). Additive changes (new endpoints, new optional fields, new webhook events) are never breaking; breaking changes ship only as a new version path. Deprecations, if any, are announced in the changelog at least 180 days before removal and signaled with Deprecation and Sunset headers. Nothing is deprecated today.

Endpoints:
- GET /me: validate the key; returns the organization.
- POST /hooks: subscribe a target URL to a trigger event (REST Hook). Re-subscribing the same URL and event is a no-op.
- DELETE /hooks/:id: remove a webhook subscription.
- GET /triggers/:event/sample: pull a sample record for field mapping.
- GET /contacts: cursor-paginated list of the organization's contacts, newest first.
- GET /sessions and GET /sponsors: cursor-paginated lists of one owned event's sessions or sponsors, newest first. eventId is required (missing = 400 VALIDATION_ERROR; an unknown event or one owned by another organization = 403 FORBIDDEN).
- POST /contacts: Create Contact (firstName, lastName, email required; type, phone, company, jobTitle optional).
- POST /contacts/batch: synchronous batch create of 1-100 contacts. Items take the POST /contacts fields and succeed or fail independently; the 200 response pairs every input index with either status "created" plus the contact or status "error" plus a typed { code, message } error, alongside a summary of total, created, and failed. Each created contact fires contact.added exactly like a single create. Supports Idempotency-Key.
- POST /imports/contacts: async import of 1-1,000 contacts. Returns 202 Accepted with { jobId, type: "contacts_import", status: "pending", total, statusUrl } and a Location header pointing at the poll URL. Supports Idempotency-Key.
- POST /imports/tickets: add, update, or cancel 1-1,000 tickets on one event from your own ticketing system, with the same rules as a file import in the app. Requires the tickets:import scope; the event must be activated and set up for imported tickets, and one import runs per event at a time. Body: eventId (required), tickets (required, 1-1,000 rows, whole body at most 2 MB), confirmCheckedInCancels (optional boolean; without it, cancelling or refunding a checked-in ticket fails that row with checked_in). Ticket fields: reference (required, up to 128 characters; a later request with the same reference updates or cancels that ticket), name (or firstName and lastName) and ticketType (needed for a new reference; up to 50 types per request, an unknown name creates the type), email, company, title, status, table, seat, scanKey (the value inside the attendee's existing QR code, 8 to 128 characters, unique within the event), purchasedAt (a date such as 2026-11-04 or a millisecond timestamp), answers (up to 12 { label, value }). Unknown fields are ignored. A reference that already exists can carry only what changed, so a cancellation is { "reference": "...", "status": "cancelled" }. status is case-insensitive: valid, active, paid, complete, completed, confirmed, attending, issued mean valid; cancelled, canceled, void, voided mean cancelled; refunded, refund mean refunded; any other word fails the row with unknown_status. Up to 100 tickets are applied inside the request and return 200 with the job completed; 101 to 1,000 return 202 Accepted with the job pending and a statusUrl (also the Location header) to poll at GET /jobs/:id. Both return { jobId, type: "tickets_import", status, total, statusUrl, result } where result carries total, processed, created, updated, unchanged, cancelled, restored, failed, and the first 50 row errors as { index, reference, code, message } (index is zero-based in tickets). Rows fail independently; whole-request problems (body not JSON or over 2 MB, a field of the wrong type, duplicate or too-short scanKey values, more than 50 ticket types) return 400 VALIDATION_ERROR and import nothing. 402 PAYMENT_REQUIRED for an event that is not activated; 409 CONFLICT when another import is running or the event is not set up for imported tickets; each event accepts 12 ticket import requests per minute (429 with Retry-After). Supports Idempotency-Key: the same key and body return the original response, not current progress; the same key with a different body returns 409.
- GET /jobs/:id: poll an async job. Returns { jobId, type, status, createdAt, completedAt, result }; status is pending, processing, completed, or failed, completedAt is null until a terminal state, and result carries total, processed, created, failed, and the first 50 per-item errors as { index, code, message }. Jobs are retained for 7 days; unknown, expired, and cross-organization ids all answer 404. That shape is a contacts_import job; a tickets_import job returns the POST /imports/tickets response shape and needs a key with tickets:import.
- POST /sessions: Create Session (eventId, title required; type, description, duration, startTime, endTime, maxAttendees, isPublic, tags optional).
- POST /sponsors: Create Sponsor (eventId, name required; website, description, hasBooth, tierId optional).

Trigger events, 16 (accepted by POST /hooks and GET /triggers/:event/sample): contact.added, contact.added_to_event, contact.updated, contact.removed, sponsor.added, sponsor.updated, sponsor.removed, session.added, session.updated, session.removed, speaker.confirmed, speaker.declined, submission.created, submission.status_changed, review.completed, event.published. Deliveries are retried up to 3 times and carry an X-Webhook-Signature header: an HMAC-SHA256 of the raw request body keyed with the endpoint secret.

Object reference: Contact (id, type, firstName, lastName, email, phone, company, jobTitle, bio, avatarUrl, location, website, socialLinks, expertiseAreas, tags, customFields, source, status, rating, createdAt, events[]), Sponsor (id, eventId, orgId, name, tierId, tierName, website, description, logoUrl, logoLightUrl, hasBooth, assignedPortal, primaryContactId, primaryContact, customFields, submissionId, createdAt), Session (id, eventId, eventName, eventNumber, orgId, title, description, type, status, trackId, trackName, roomId, roomName, startTime, endTime, duration, maxAttendees, tags, isPublic, isTentative, parentSessionId, customFields, source, createdAt, speakers[]).

Support: hello@agendaforge.app.

---

AgendaForge is an AI-native event-management platform. Public machine-readable index: https://agendaforge.app/api/ai
