SimpleInviteStart an invite

Build on SimpleInvite.

With a key, a program can make an event, add people, send it and read who is coming, under the same rules a host works under in the app. The REST API and the MCP server do the same jobs.

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 key

How 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 on code, which is stable. message is a sentence for a person and may be reworded. A 422 adds details, one entry per field that did not pass.
Lists page by cursor
Pass limit (up to 200) and the next_cursor from 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-Match and 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-Limit and X-RateLimit-Remaining, so you can back off before you are refused, and a 429 carries Retry-After in 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}/send is 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

MethodPathWhat it doesMCP tool
GET/eventsThe host's events, newest first.list_events
POST/eventsMake 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}/invitationsThe parties on one event.list_invitations
POST/events/{id}/invitationsPut one more party on the event.add_invitation
POST/events/{id}/nudgeChase the people who have not answered.nudge
POST/events/{id}/sendSend the invitations.send_event
GET/events/{id}/summaryThe counts for one event.get_event_summary
POST/events/{id}/updateTell the guests something.send_update
GET/groupsThe host's saved lists.list_groups
POST/groupsMake 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/guestsThe host's address book.list_guests
POST/guestsAdd 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/importBring 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}/resendSend one invitation again.resend_invitation
GET/invitations/{id}/textThe host's own text for one guest.get_host_text
GET/meThe host this key acts for.whoami
GET/messagesThe host's thread for one event.list_messages
GET/webhooksThe URLs this host has registered.list_webhooks
POST/webhooksRegister a URL for event notifications.register_webhook
DELETE/webhooksRemove a registered URL.remove_webhook