Get a key
Make a key in the app under Settings, then Keys. It is shown once, when you make it, and only a hash is kept, so copy it then. If you lose it, make another and revoke the old one.
Send it on every request as Authorization: Bearer si_live_.... Reads need the read scope and anything that changes something needs the write scope; a request without its scope gets a 403. Keys made in Settings carry both.
curl https://simpleinvite.app/api/v1/me \
-H "Authorization: Bearer si_live_..."Make a keyHow it behaves
- Your key is the scope
- There is no account or organisation parameter. A key belongs to one host, and every list is that host's rows. An id from anywhere else returns a 404, never a 403: a 403 would tell you the row exists.
- Every failure has one shape
- Every failure is
{ error: { code, message } }. Branch oncode, which is stable.messageis a sentence for a person and may be reworded. A 422 addsdetails, one entry per field that did not pass. - Lists page by cursor
- Pass
limit(up to 200) and thenext_cursorfrom the page before. An offset re-sends rows when something is inserted mid-scan and skips rows when something is deleted. A cursor anchored on the last row cannot. - Reads carry an ETag
- Send it back as
If-None-Matchand an unchanged read answers 304 with no body. It pays most on the two heavy reads: an event's invitations, and the guest book. - Rate limits are per key
- 120 reads and 30 writes a minute. Successful responses carry
X-RateLimit-LimitandX-RateLimit-Remaining, so you can back off before you are refused, and a 429 carriesRetry-Afterin seconds. Writes have the tighter budget because each one can put a message in front of a person. - Sending is separate from changing
- Creating an event sends nothing. Adding a guest sends nothing.
POST /events/{id}/sendis the call that puts invitations on a wire, and it is idempotent: a retry after a timeout reports the first call's result rather than inviting everyone twice. - The API does not lift the rules
- Nudges are capped, quiet hours are honoured, someone who opted out is never messaged, and an answer runs through the same state machine a guest's tap does. Nothing here does something a host could not have done by hand.
Webhooks
Register a URL and we POST to it when something happens on one of your events. rsvp.changed is sent when an answer is recorded or changed, whether the guest tapped, replied or you set it. message.received is sent when a guest's text or email reply lands in an event's thread. invitation.delivered is sent once per invite, when the carrier confirms it reached the guest.
For the two message events, data is the message exactly as GET /messages returns it. Every payload is described under webhooks in openapi.json.
A host can register up to 10 webhooks, and each URL once.
The signing secret comes back once, from the call that registers the webhook. Every delivery carries a Simpleinvite-Signature header shaped t=<unix seconds>,v1=<hex>, computed as an HMAC-SHA256 of <timestamp>.<body> with that secret.
Verify the signature before you trust the body, and check the timestamp. Signing the time with the payload is what stops a captured delivery being replayed at you tomorrow. Compare in constant time.
Register a URL
curl -X POST https://simpleinvite.app/api/v1/webhooks \
-H "Authorization: Bearer si_live_..." \
-H "Content-Type: application/json" \
-d '{"url":"https://example.org/hooks/simpleinvite","events":["rsvp.changed"]}'A delivery
{
"id": "evt_m1x8k2q4b7",
"type": "rsvp.changed",
"created_at": "2026-09-11T18:04:22.117Z",
"data": {
"event_id": "evt_gamenight",
"invitation_id": "inv_kims",
"status": "accepted",
"maybe": false,
"headcount": 4,
"response_channel": "web",
"responded_at": "2026-09-11T18:04:22.101Z"
}
}Verify the signature
import { createHmac, timingSafeEqual } from "node:crypto";
const TOLERANCE_SECONDS = 300;
export function verify(secret: string, body: string, header: string): boolean {
const parts = new Map(
header.split(",").map((part) => {
const [name, value] = part.split("=");
return [name, value] as const;
}),
);
const timestamp = Number(parts.get("t"));
const presented = parts.get("v1") ?? "";
if (!Number.isFinite(timestamp) || !presented) return false;
// Reject a delivery older than the window, or one dated in the future.
const age = Math.abs(Math.floor(Date.now() / 1000) - timestamp);
if (age > TOLERANCE_SECONDS) return false;
const expected = createHmac("sha256", secret)
.update(`${timestamp}.${body}`)
.digest("hex");
if (expected.length !== presented.length) return false;
return timingSafeEqual(Buffer.from(expected), Buffer.from(presented));
}Deliveries are not a queue. We try twice, a second apart, and record what happened, so a receiver down for a minute misses that delivery. Answer 2xx as soon as you have the body, and do your work after.
MCP
The same surface speaks the Model Context Protocol at /api/mcp, over streamable HTTP, authenticated with your API key the same way. Every REST route has a tool with the same job and a snake_case name: list_events, get_event_summary, send_event, mark_answer.
A key without the write scope sees only the read tools, so an agent never carries a tool it would be refused on.
Two resources come with it. guest-book:// is your address book, up to its first 200 guests, and event://{id} is one event with its counts and the parties on it: the thing to put in front of a model before asking who is coming.
Claude Code
claude mcp add simpleinvite \
--transport http \
https://simpleinvite.app/api/mcp \
--header "Authorization: Bearer si_live_..."Claude Desktop
{
"mcpServers": {
"simpleinvite": {
"type": "http",
"url": "https://simpleinvite.app/api/mcp",
"headers": {
"Authorization": "Bearer si_live_..."
}
}
}
}A tool whose description opens SENDS REAL INVITATIONS or SENDS A REAL MESSAGE puts an email or a text in front of real guests. Read a tool's description before letting an agent call it unattended.
Reference
Generated from the same schemas the routes validate with. The machine-readable document is at /api/openapi.json. openapi.json
| Method | Path | What it does | MCP tool |
|---|---|---|---|
GET | /events | The host's events, newest first. | list_events |
POST | /events | Make an event. | create_event |
GET | /events/{id} | One event. | get_event |
PATCH | /events/{id} | Change an event. Nothing goes out. | update_event |
DELETE | /events/{id} | Call the event off. | cancel_event |
GET | /events/{id}/invitations | The parties on one event. | list_invitations |
POST | /events/{id}/invitations | Put one more party on the event. | add_invitation |
POST | /events/{id}/nudge | Chase the people who have not answered. | nudge |
POST | /events/{id}/send | Send the invitations. | send_event |
GET | /events/{id}/summary | The counts for one event. | get_event_summary |
POST | /events/{id}/update | Tell the guests something. | send_update |
GET | /groups | The host's saved lists. | list_groups |
POST | /groups | Make a saved list. | create_group |
GET | /groups/{id} | One saved list, with who is in it. | get_group |
PATCH | /groups/{id} | Rename a list, or replace who is in it. | update_group |
DELETE | /groups/{id} | Drop a saved list. | remove_group |
GET | /guests | The host's address book. | list_guests |
POST | /guests | Add a person to the address book. | add_guest |
GET | /guests/{id} | One person. | get_guest |
PATCH | /guests/{id} | Correct a number, add a household, write a note. | update_guest |
DELETE | /guests/{id} | Take a person out of the address book. | remove_guest |
POST | /guests/import | Bring a list of people in. | import_guests |
GET | /invitations/{id} | One party. | get_invitation |
PATCH | /invitations/{id} | Record an answer on someone's behalf. | mark_answer |
DELETE | /invitations/{id} | Take a party off an event. | remove_invitation |
POST | /invitations/{id}/resend | Send one invitation again. | resend_invitation |
GET | /invitations/{id}/text | The host's own text for one guest. | get_host_text |
GET | /me | The host this key acts for. | whoami |
GET | /messages | The host's thread for one event. | list_messages |
GET | /webhooks | The URLs this host has registered. | list_webhooks |
POST | /webhooks | Register a URL for event notifications. | register_webhook |
DELETE | /webhooks | Remove a registered URL. | remove_webhook |