Cloomba API
Read your events, attendees, and registration answers — pull your agenda, speakers, sponsors, photos, and ticket prices — create, update and cancel events, and check guests in, over a simple REST API.
The public API is still under development. The list of endpoints will grow over time — following our roadmap and on request.
What you can do
A key belongs to one of your calendars and acts as the owner who made it. Use it to list the calendar's events, read their attendee lists with typed registration answers, and check people in at the door. It reaches only that calendar's events — never anyone else's data. Webhooks send the same data to your server as it changes.
Base URL
All endpoints are relative to a single base URL:
https://api.cloomba.com/public/v1
Authentication
Send your API key as a Bearer token on every request. Keys are created on a calendar's Settings, under Developer — open it from your calendars — where you also choose the key's scope. Events that are on none of your team calendars live on your personal calendar, which has its own Settings.
curl -H "Authorization: Bearer cloomba_sk_..." \ https://api.cloomba.com/public/v1/events
Scopes
A key's scope is fixed when it is created:
- read_publicAn event as a signed-out visitor sees it on its Cloomba page — the scope for a published website, safe to commit to a public repository. Public events only (an unlisted one opens by its slug); no join link, no venue the organizer hid, no attendee data.
- readEvery read endpoint below, for every event of the key's calendar.
- read_writeAdditionally allows every write operation — creating, updating and cancelling events, setting a cover, and checking a guest in or undoing it.
Calendar
A key belongs to the calendar it was made on and reaches the events that calendar presents, whoever organizes them, together with the calendar's memberships. It acts as the owner who made it, for as long as that person is the calendar's owner or admin. A write needs them to still be an owner, and their role on that event: editing and check-in work on every one of the calendar's events, cancelling only on events they organize. A read_write key creates events into its calendar, organized by the owner who made it. Every owner of the calendar sees, renames and revokes its keys.
Polling for changes
The list endpoints for events, attendees and memberships accept ?since=, so a sync job can fetch only what changed instead of re-reading everything. Each response carries a timestamp — store it and send it back as the next since. Your own clock never enters the picture.
curl -H "Authorization: Bearer cloomba_sk_..." \ "https://api.cloomba.com/public/v1/events/my-event/attendees?since=2026-10-01T08:00:00Z"
A delta reports registrations that were created or changed — a check-in and an approval both count. Deletions are not reported; webhooks report them.
Webhooks
A webhook sends a signed POST to your URL when something changes in its calendar's events or memberships. Set one up on a calendar's Settings: a URL and the types it receives. It reaches what a key on that calendar made by the same owner would, and receives only the types it lists.
| Type | Sent when |
|---|---|
| event.published | A draft became published — when created or on a later save. |
| event.updated | Any other saved change to an event, drafts included. |
| event.cancelled | The event was cancelled. Each guest it cancels also sends rsvp.updated. |
| event.deleted | The event was deleted. The body names it by the slug it had. |
| rsvp.created | Someone registered, in whatever status the registration starts. |
| rsvp.updated | A registration's status or payment changed, it was removed, a later ticket of the party checked in, or a check-in was undone. |
| rsvp.checked_in | The party's first ticket was checked in. |
| membership.created | Someone joined or applied to the calendar. |
| membership.updated | A membership's status, tier, renewal date or end date changed. |
Every delivery has the same envelope. data uses the shapes this API returns, built when the delivery is sent, so every attempt carries the current state:
POST https://example.com/cloomba
Content-Type: application/json
User-Agent: Cloomba-Webhooks/1
Cloomba-Signature: t=1791374401,v1=<64 hex characters>
{
"id": "0b8f3c2e-5a7d-4b1e-9c6f-2d4a8e1b7f90",
"type": "rsvp.created",
"occurred_at": "2026-10-07T12:00:00Z",
"data": {
"event": { "slug": "open-coffee", "title": "Open Coffee", "starts_at": "2026-10-09T07:00:00Z", "timezone": "Europe/Warsaw" },
"qr_token": "k3Jd9QwP2x",
"attendee": { … }
}
}- Delivery is at least once and in any order. id is the same on every retry of a change — deduplicate on it.
- Answer with any 2xx within 10 seconds. Anything else, a 3xx included, is a failure and is retried after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h — 8 attempts over about 45 hours.
- A webhook whose URL accepts nothing for 3 days is turned off, with an email to the calendar's owners; turn it back on from its page.
- A webhook receives the changes made after it was created. Changes made while it is paused or off are skipped.
- If the calendar stops being eligible for webhooks, they pause, with an email to its owners, and resume with the next change once it is eligible again.
- Send test on a webhook's page sends made-up data for one of its types, with "test": true in the body.
- A URL on hooks.slack.com receives Slack's own message instead: one line in the language of the owner who made the webhook, naming the event or calendar.
- Up to 5 webhooks per calendar. A URL starts with https:// and resolves to a public address. Deliveries do not count toward the daily request limit. Each webhook's page keeps its delivery log for 30 days.
Cloomba-Signature is t=<unix seconds>,v1=<hex HMAC-SHA256>, the HMAC computed over <t>.<raw body> with the webhook's whole signing secret (whsec_…) as the key. Verify the raw bytes before parsing the JSON, compare in constant time, and refuse a t more than 5 minutes off. In Node:
import { createHmac, timingSafeEqual } from 'node:crypto'
export const verifyCloomba = (header, rawBody, secret, toleranceSeconds = 300) => {
let t = null
const signatures = []
for (const part of header.split(',')) {
const [key, value] = part.split('=', 2).map((s) => s.trim())
if (key === 't') t = Number(value)
else if (key === 'v1' && value) signatures.push(value)
}
if (t === null || !Number.isFinite(t) || signatures.length === 0) return false
if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest()
return signatures.some((signature) => {
const given = Buffer.from(signature, 'hex')
return given.length === expected.length && timingSafeEqual(given, expected)
})
}With Express, read the body raw on that route:
app.post('/cloomba', express.raw({ type: 'application/json' }), (req, res) => {
const ok = verifyCloomba(req.get('Cloomba-Signature') ?? '', req.body.toString('utf8'), process.env.CLOOMBA_WEBHOOK_SECRET)
if (!ok) return res.sendStatus(400)
const delivery = JSON.parse(req.body)
// deduplicate on delivery.id, then act on delivery.type
res.sendStatus(200)
})Rotating the secret on the webhook's page switches it at once. A delivery that fails your check in between is retried on the schedule above, so a receiver updated within that window loses nothing.
Rate limits
- 60 requests per minute (burst) per key.
- 10,000 requests per day per calendar, across its keys.
Exceeding either returns 429 rate_limited.
Errors
Every error carries a stable snake_case code. Branch on the code, never on the human-readable message — codes are never renamed or reused.
| Code | HTTP | Meaning |
|---|---|---|
| api_key_invalid | 401 | Missing or invalid API key. |
| api_key_revoked | 401 | The key was revoked or has expired. |
| api_pro_required | 403 | The key's calendar is not eligible to write. |
| api_scope_forbidden | 403 | The key can't reach this event or calendar, or a read key attempted a write. |
| validation_failed | 400 | A parameter or body field is invalid, or unknown. |
| rsvp_not_found | 404 | No registration matches that token. |
| event_not_found | 404 | No such event. |
| calendar_not_found | 404 | No such calendar. |
| membership_not_found | 404 | No membership of that calendar has that hash. |
| slug_taken | 409 | That event URL is already in use. |
| external_id_taken | 409 | Another of your events already uses that external_id. |
| event_cancelled | 409 | The event is cancelled — it cannot be cancelled again or change status. |
| rsvp_not_checkinable | 409 | Only a confirmed registration can be checked in. |
| event_ended | 422 | The event is completed — its status cannot change. |
| event_is_external | 422 | The event links out to another site and has no guests. |
| cover_fetch_failed | 422 | The cover image could not be fetched from that URL. |
| rate_limited | 429 | Over the rate limit — slow down. |
A note on attendee data: an attendee's email and phone are the values that person consented to share for that registration — never their account contact details. Custom-question answers are typed (an options answer is an array of strings, a checkbox / terms answer is a boolean, everything else is a string). Lists are paginated with limit / offset and report a total.
Endpoints
- GET/eventsList your events. Add ?since= for only what changed.
- POST/eventsCreate an event, or upsert on your external_id (read_write).
- GET/events/{slug}One event with guest counts and questions.
- PATCH/events/{slug}Update an event (read_write).
- POST/events/{slug}/cancelCancel an event — refunds and emails every guest (read_write).
- PUT/events/{slug}/coverSet the cover from a URL or preset (read_write).
- DELETE/events/{slug}/coverRemove the cover (read_write).
- GET/events/{slug}/attendeesThe full registration list with answers. Add ?since= for a delta.
- GET/events/{slug}/attendees/{qr_token}Look up one registration.
- GET/events/{slug}/creditsSpeakers, hosts, organizers, partners, sponsors — filter by kind, e.g. ?kind=speaker,mentor.
- GET/events/{slug}/sessionsThe agenda, with speakers on each session.
- GET/events/{slug}/mediaThe event's photos and videos.
- GET/events/{slug}/ticket-typesTicket tiers, prices, seats left.
- POST/events/{slug}/check-inCheck a guest in (read_write).
- POST/events/{slug}/uncheck-inUndo a check-in (read_write).
- GET/cover-presetsThe bundled cover library.
- GET/calendars/{slug}/membershipsA calendar's members, with answers and payments. Add ?since= for a delta.
- GET/calendars/{slug}/memberships/{hash}One membership.