Booth.Events API

Automate your events from your own tools — or let an AI agent do it for you. Available on Pro+ plans.

Authentication

Create an API key in the dashboard under Account → API Access (it is shown exactly once). Send it on every request:

curl https://api.booth.events/v1/events \
  -H "Authorization: Bearer be_live_..."

Keys are read or read-write scoped. Treat them like passwords: server-side only, revoke from the dashboard if leaked. The API sends no CORS headers, so a web page cannot call it from the browser: call it from your own server, which keeps the key.

A key acts as the account owner. Everything it does — including on events a team member or operator runs — is done with the owner's full access; team-member roles and per-event operator scope never apply to API calls. Only the account owner can create keys: a team member cannot create one for an account they belong to, so ask the owner to create it.

Versioning

The API is date-versioned. Every API key is pinned to the version that was current when the key was created and keeps that behavior for as long as it lives — new API versions never change what an existing key's requests mean. To adopt a newer version, create a new key (new keys always pin to the current version) or re-pin the existing one; pin changes take up to a minute to apply. Apps you connect from the dashboard's Connected apps are not pinned: they always use the newest version. The current version is 2026-10-16.

Override the version per request with the Booth-Events-Version header:

curl https://api.booth.events/v1/events \
  -H "Authorization: Bearer be_live_..." \
  -H "Booth-Events-Version: 2026-10-16"

Every response echoes the version that served it in the same header. MCP connectors (which cannot send custom headers) can append ?version=2026-10-16 to the connector URL. The machine-readable spec for any version is at /v1/openapi.json?version=….

A version that does not exist, in the header, on the connector URL or on /v1/openapi.json, is refused with 400 validation_failed: the message names the value, and hint lists the versions there are.

Changelog

While the API is in early access its changes keep the version date, breaking ones included; each is listed here, newest first.

Webhooks

Booth.Events POSTs JSON to your URLs when something happens: a photo reaches an event's gallery, a guest's session is shared, a printer runs low on paper, a device goes quiet. Add a URL and choose its events with create_webhook_endpoint (POST /v1/webhooks/endpoints); list_webhook_event_types lists every event with an example body, and send_test_webhook sends one to check your endpoint. The account's two original webhook URLs (the media and share webhooks, also set in the dashboard) are listed as built-in endpoints.

Every delivery carries an event name, a unique deliveryId and its own apiVersion — a date separate from the API version above, because webhook payloads change additively only: keys are added over time, never removed, renamed or re-typed, so there is nothing to pin. Endpoints must be public https and must answer without redirecting. A delivery not answered with a 2xx within 30 seconds is tried 5 more times over about an hour with the same deliveryId, so treat a deliveryId you have seen as already handled. Deliveries to the endpoints you add are signed: a Booth-Events-Signature header, t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">, keyed with the secret create_webhook_endpoint returned; recompute it over the raw body and refuse a t more than 5 minutes old. The two built-in URLs are not signed: put a long random secret in the URL and check it on arrival. media.uploaded carries the eventId and eventIdUser it belongs to; session.sent carries the galleryId, so match it to the event with that galleryId.

Events

Connect an AI agent (MCP)

The same API is exposed as a hosted MCP server at https://api.booth.events/mcp.

Claude Code

claude mcp add booth-events --transport http https://api.booth.events/mcp \
  --header "Authorization: Bearer be_live_..."

Cursor / VS Code / Windsurf (mcp.json)

{
  "mcpServers": {
    "booth-events": {
      "url": "https://api.booth.events/mcp",
      "headers": { "Authorization": "Bearer be_live_..." }
    }
  }
}

claude.ai / ChatGPT (web connectors)

Add a custom connector with the URL https://api.booth.events/mcp. A Booth.Events sign-in page opens and asks for one thing: a connection code, which you create in the dashboard under Account → API Access → Connect an app, choosing read-only or read & write access there. The code works for 15 minutes. When the app finishes connecting, Booth.Events creates an API key for it (named after the app, with the access you chose) — nothing is created until then, and repeating the connection with the same code reuses that key. The connection lasts 90 days and is listed under Connected apps in the same place; removing the last connection on a key made this way revokes the key, and revoking the key removes every connection made with it. Keys you create yourself in the dashboard are never removed by a connection.

Trimming the tool list (optional)

The full surface is 140 tools; agents pick tools more reliably from smaller lists. Append ?toolsets= to the MCP URL to expose only what your integration needs, e.g. https://api.booth.events/mcp?toolsets=events,templates,ai,devices,analytics (the core CRM loop). Groups: events (events, gallery settings/URL, event⇄template assignment), templates (templates, imports, scenes, uploads, attract screens, sticker sets), ai (prompts, portraits, models, credits), media (guest media, sessions, survey export, the guest CSV, the gallery download link, sends to guests), analytics (stats, client reports), account (audit log, webhooks, communication settings), devices (the account's devices and running events on them: the schedule, open/close), team, payments, cloud-storage (the operator's Dropbox / Google Drive / SmugMug: account setup, per-event destinations and media filters, upload status, backfill and retry).

Image & preview URLs — three kinds, know which you hold

Every image URL the API returns is directly fetchable by any browser with no auth — but their lifetimes differ. If you are generating an app against this API (human or AI): render image URLs promptly and re-fetch the list to refresh them; never persist them as long-lived data.

WhereKindLifetime
Public AI portrait / prompt catalog previews Stable public URLNever expires — safe to cache
Template / attract screen / sticker set thumbnailUrl Stable upload-time URL (signed fallback when absent) Stable; fallback expires in 7 days
Gallery media originalUrl/thumbnailUrl, event-stats topMedia, private catalog previews Signed URLUp to 7 days; media URLs are normally refreshed to ≥24 h when served; a legacy item without a stored path can still hand back an expired one — re-list to refresh

Uploading files

Every operation that takes a file can take it as an upload, so the file never has to be public: template imports (a PDF, PNG/JPG design images, or a ZIP of them), AI reference images, scenes, branding images, logos and attract screens. There is no multipart request; an upload is two steps:

  1. POST /v1/uploads (create_upload) with the file's purpose, filename and contentType — returns a signed uploadUrl, valid for 15 minutes, plus the headers you must send back exactly.
  2. HTTP PUT the raw bytes to uploadUrl (no auth header), then pass the returned storagePath as uploadPath to the consuming operation. For create_template_from_images, that is images[].uploadPath for each image (purpose template-image), or zipUploadPath for a ZIP (purpose template-zip).

An uploaded file is deleted two days after the upload. The purposes, what each takes and its size limit:

PurposeContent typesUp to
template-pdfapplication/pdf75 MB
template-zipapplication/zip, application/x-zip-compressed75 MB
template-imageimage/png, image/jpeg16 MB
sceneimage/png, image/jpeg8 MB
event-branding-imageimage/png, image/jpeg8 MB
account-logoimage/png, image/jpeg8 MB
ai-reference-imageimage/png, image/jpeg8 MB
ai-choice-imageimage/png, image/jpeg8 MB
template-choice-imageimage/png, image/jpeg8 MB
template-artworkimage/png, image/jpeg16 MB
ai-preview-imageimage/jpeg, image/png8 MB
ai-test-photoimage/jpeg, image/png8 MB
attract-imageimage/png, image/jpeg8 MB
attract-videovideo/mp4, video/quicktime50 MB

No PUT? Use sourceUrl instead. Every consuming operation alternatively accepts a public https URL it fetches server-side (must not redirect) — ideal when your assets already live on your own storage/CDN, and the way in for an AI agent that cannot make HTTP requests itself.

Devices

The Booth.Events app runs on iPads, iPhones and Macs, and this API calls each one a device (list_devices; status.platform says which). They run events the same way, with these differences: a Mac has no camera of its own in the app, so it takes guest captures and test photos only with a connected camera (its builtIn camera answers noBuiltInCamera), and it reports no battery; Share Station runs on iPads and Macs, not on an iPhone (set_device_schedule refuses it for one). Screen sizes in these docs are an iPad's: an iPhone's screen is smaller, so check a design on one.

Build a CRM integration

The intended loop for booking platforms (CheckCherry, HoneyBook, BoothBook, …). Set idUser to your booking id everywhere — it is the correlation key: list_events filters by it, and create_event is retry-safe with it (an existing event with the same idUser is returned with alreadyExisted instead of creating a second event, or updated with what you sent when you pass onExisting: 'update'). Keep the rest of the booking that has no field here (venue, address, end time, package) in the event's metadata: your own key/value pairs, returned on every event read and shown nowhere in Booth.Events. An event has a day but no end: it keeps working after its date (devices can still run it, guests' photos keep arriving, its gallery stays open), so a booking over several days is one event, and set_device_schedule runs a device for set hours.

  1. Create the event — create_event with name, date, templateIds (from list_templates), idUser, and metadata for the rest of the booking. When the booking changes, update_event with metadata changes just the keys you send.
  2. Brand it — The brand shows in all of these places; go through each one for a fully branded event. On every photo: the template, the frame or overlay printed and shared with each capture (step 3). On the device, while idle: an attract screen, a full-screen design shown after settings.attractScreenDelay idle seconds (update_event attractScreenId; designed in the dashboard, see list_attract_screens, or made from one picture or video with create_attract_screen_from_image / create_attract_screen_from_video). The app's home screen: update_event sets the three colors (buttons; timers and borders; icons), the captureMessage heading, the hashtag line and their brandingTextColor; behind them is the live camera or an attract screen (update_event_branding); the capture buttons can be your own images (set_event_branding_image slot button-<type>, and settings.galleryButtonOnHomeScreen adds a gallery button). On the device after a capture: a background behind the preview and share screens (slot after-capture-background) and the stickers guests can add (update_event stickerSetId). Share Station devices: an HTML banner at the top (update_gallery_settings shareStationHtml). Guest gallery: the event logo (slot logo, also used on the device, in emails and on the report) and a background image (slot gallery-background); update_gallery_settings sets its colours, which headers show, its social links, and its own content (a Message & Button, or Content Builder blocks); set_gallery_url gives it a vanity /e/ link or the account's custom domain. Guest emails and texts: update_communication_settings sets the sender name, reply-to, subject, the lines around the gallery link and the SMS text for every event, and update_event_communication_settings gives one event its own; the email also carries the event logo and a button in the gallery's primary colour. Client report: update_event_report sets its logo, accent colour, hero background and its own content (a Message & Button, or Content Builder blocks). Every event at once: update_account_branding and set_account_logo set the account-wide defaults, which are the logo and name in each gallery's top bar, the colours new events start with, and the social links a gallery shows when it has none of its own. Each image is shown differently, so read set_event_branding_image before making one: the logo is a square (shown as a circle on the device and in emails), a capture button is a transparent PNG 292 px tall, the after-capture background is shown unscaled and should be about the device's own screen size (1376×1367 px for a landscape iPad), and the gallery background is a landscape 2560×1440 px JPEG.
  3. Bring the client's design — create_template_from_pdf or create_template_from_images (PNG/JPG/ZIP): same pipeline as the dashboard's "Upload design", photo areas detected automatically; or duplicate_template from the public library. Making the design yourself (Canva, Figma, …)? Photo areas are found by looking at the pixels of the design, so a design made for import must follow these rules — a box that breaks them is not found, and no guest photo can go in it. (1) Leave every photo box EMPTY and either fully transparent (PNG) or pure white (#FFFFFF): no fill colour, tint, gradient, texture, placeholder photo, "your photo here" text, icon, or anything else over it. In Canva, draw a plain white rectangle — a frame or grid holding an image, or a coloured box, is not a photo area. (2) Draw it as a plain, upright rectangle: circles, rounded, rotated or irregular shapes are not found reliably. (3) Surround it with opaque design that is not white — a coloured or patterned background, or at least a border all round — and keep it off the corners of the page. A white box on a white page, or a transparent hole in a transparent page, merges with the page instead, and a white box that runs into a corner of the page can be taken for the page background and ignored. (4) Make it big: at least 6% of the page's area each (360×360 px or more on a 1800×1200 4×6 print) and at least 100 px on every side. (5) Keep every other large white or transparent rectangle out of the design (white panels, text boxes, logo backgrounds): it would become a photo area too. (6) Use one kind per design: all boxes transparent, or all white. Then check the import's response: photoAreaCount must equal the number of boxes you designed (an AI prompt or portrait needs exactly 1), warnings must not contain no_photo_areas_detected, and the photo-area rectangles in layers[] must sit on the boxes in the returned picture. If they do not, fix the design and import it again, or put the areas right with add_template_photo_area / update_template_photo_area / remove_template_photo_area. get_template shows the design as rectangles in template pixels (and, through MCP, the rendered picture) at any time. Then write on it: add_template_text for the names or the date (any Google Font from list_fonts, measured and centred for you), add_template_field to print a guest's answer (a new question of the template's own, or a field of an AI prompt assigned with update_template — one field, asked once, used by both), with preview / previewChoiceId choosing what the design shows. add_template_image places a picture such as the client's logo, move_template_layer puts a photo area under a frame's artwork so the guest photo shows through its hole, and update_template sets the template's look and printing (imageFilters, bwCustom, filmStrip, glamFilter, printable, printsShortestSideDoubled). Every change renders the picture again and returns it; render_template_preview renders one on demand. An event always has a template: create_event uses templateIds, or the default template when you leave it out. So the client's design can come before or after the event: replace_event_template later puts it in the default's place, in one step.
  4. Set up AI — create_ai_prompt (+ add_ai_prompt_reference_image for style; list_ai_models compares credits per image and speed; give it fields to ask each guest a question first — a name, a choice from a list, a choice between pictures — and body to place the answers in the prompt), then test_ai_prompt with useAsPreview to see what it makes and to give it the preview image guests pick it by (the device does not offer a prompt without one). Or pick portrait styles via list_ai_portraits. Attach both with update_template, and make sure the event's settings.captureTypes includes aiCustomPrompt. Check get_ai_credits before the event.
  5. Green screen — add_template_scene per background.
  6. Back up to the client's cloud storage (optional) — get_cloud_storage shows what the operator connected (Dropbox / Google Drive / SmugMug — connecting is dashboard-only); update_event_cloud_storage points the event at its own folder or album (list_cloud_storage_folders / create_cloud_storage_folder to find or make one) and picks which media types go; backfill_event_cloud_storage uploads what was captured before the settings changed.
  7. Put the event on a device — set_device_schedule is how you put an event on a device. To run it now, send one block starting now with no end: the device launches the event at once and stays locked in it (nobody at the device can leave the event or pick another) until you change, pause or clear the schedule. Setting a schedule also turns a paused one back on, so the event you send is what runs. To run it later, give the block the event's start and end (every time with Z or an offset): the device launches the event at the start and closes the booth at the end, and a block that has already started takes over that live booth at once. Until the first block starts the booth is closed and locked: a schedule whose blocks all start later ends whatever event the device is running at once. So set the schedule close to the event, or send the running event's block along with the later one. list_devices shows the account's devices and how each is run (controls). update_camera_settings changes a device's camera settings, for its own camera or a connected DSLR, as the app's camera settings screen does (list_camera_setting_choices says what each can be set to, get_camera_settings what they are, and take_test_photo takes a test photo to check them).
  8. During the event — To see what guests are taking, list_media with since returns only what was taken after a time (pass the newest createdDate you have), and through MCP includePictures returns the photos themselves as pictures you can look at: a chat app may not open the links in the result. A photo keeps the time it was taken, so one from a device that was offline can arrive later with an earlier time; set since a few minutes back and skip ids you already have. list_devices shows each device as it last reported: battery, printers with prints left, camera, and when it was last heard from (lastSeen). For alerts, the account's webhooks POST to a URL as things happen: a guest photo or share, a printer low on paper or with a problem, a camera's card or battery running low, a device's battery running low or the device going quiet, and AI credits running low. list_webhook_event_types lists every event with an example and its threshold, create_webhook_endpoint subscribes a URL to the events the operator wants (an automation in Zapier, Make or n8n that passes the alert on to Slack, a text or an email, or their own server), and send_test_webhook checks it. Webhooks go to places the operator chooses. An app that can subscribe to this server's events (ChatGPT can) is told of each one in the chat as it happens, with no webhook. To check on the alerts yourself, list_alerts keeps the same alerts at each event's default threshold, newest first; pass the newest recordedAt you have as since to get only new ones. To take a guest capture from here, start_guest_capture makes a device that nobody is using start one now, countdown and all, and get_guest_capture says what it did.
  9. After the event — get_event_stats for engagement (leaderboards come with names + preview images, and prints counts the prints per printer), then update_event_report + publish_event_report and email the returned reportUrl to your client — a white-label-ready report page, no sign-in needed. export_guest_csv gives the dashboard's guest CSV as a download link that works for one hour: it holds the guests' personal data, so hand it only to the operator. Each guest's own photo page is sessionUrl (list_sessions): send_session_share emails or texts it to them again. get_gallery_download_link returns the link that downloads the whole gallery without the passcode, and send_gallery_download_link emails it to your client (confirm the address first). If the event backs up to cloud storage, get_event_cloud_storage_status shows what reached it and why anything failed, and retry_event_cloud_storage sends the failed uploads again. The gallery stays open to guests until it expires: get_gallery_settings lifecycle has the expiry date and the days left, so remind your client before then to save what they want to keep.

Retrying safely

A write that would happen twice if repeated (one that creates, copies, adds or sends) takes an Idempotency-Key header, or an idempotencyKey argument over MCP: any string of up to 255 printable characters, unique to the call. Retry with the same key and the same input and you get the first call's result back, with Idempotent-Replayed: true, instead of the call being made again; it counts once toward the daily limits. A key is kept for 24 hours and belongs to your account and that one endpoint. A retry while the first call is still running, or the same key with different input, is refused with 409 conflict; a call that failed frees its key for the retry. create_event has its own: retry it with the same idUser.

Rate limits & errors

At least 120 requests/min per key (burst 40); expensive operations (creates, duplicates) count extra. All keys calling from the same IP address also share 300 requests/min between them, so an integration that serves several accounts from one address should plan for that total. An account also runs at most 3 heavy operations (imports, renders, fetching a file from a URL) and 10 operations in all at once, across its keys — queue the rest — makes at most 50 duplicates a day, and sends guests at most 1000 emails and 250 texts a day. Booth.Events support can lift these limits for an account that needs more. 429 responses carry Retry-After. Every response to a request with a key carries X-RateLimit-Limit (the key's requests a minute), X-RateLimit-Remaining and X-RateLimit-Reset (Unix seconds by which another request is allowed); the API runs on several servers that each count for themselves, so read them as a guide. Requests without a key (these docs, connecting an app) are limited to 50 per 10 seconds per address. A 429 from the load balancer itself, past an address's limit, has no Retry-After: wait a minute. Each endpoint in the reference below lists the errors it can answer, by status. Errors always look like:

{ "error": { "code": "validation_failed", "message": "...", "details": [...], "hint": "...", "requestId": "..." } }

details and hint appear when there is something to add: which field failed, or what to do next.

Reference

Full endpoint reference below. The machine-readable spec is at /v1/openapi.json.

Lists come in pages: pass nextCursor back as cursor until hasMore is false. A list's total counts every match across all its pages, so to answer "how many" use the list's filters and read total from the first page instead of reading every page; export_survey_responses, whose rows are answers rather than sessions, has no total. A page can hold fewer items than limit when its items are large.