# Dibs API and MCP server

Create and run sign-up sheets from code or from an AI assistant. Everything here acts for the organizer who owns the API key: their sheets, their sign-ups, their messages.

- REST: `https://dibs.page/api/v1` ([OpenAPI 3.1](https://dibs.page/api/v1/openapi.json))
- MCP: `https://dibs.page/api/mcp` (Streamable HTTP, stateless)
- Public availability, no key: `GET https://dibs.page/api/v1/public/sheets/{slug}`

## Keys

Make a key in your settings, under **API keys**. You see the whole key once (`dibs_ab12cd34_...`); after that only its first part shows, so you can tell keys apart. Revoke a key there and it stops working at once. Send it on every request:

```http
Authorization: Bearer dibs_ab12cd34_...
```

Keys have scopes: `sheets:read`, `sheets:write` (new keys get both, which includes adding and removing people and sending messages). A read-only key gets `403 FORBIDDEN` on writes.

Sign-ups made with a key are recorded with source `agent`, and show as "via an assistant" to the organizer.

## Make a sheet

Plain text, one slot per line, the same way the editor reads it:

```sh
curl https://dibs.page/api/v1/sheets \
  -H "Authorization: Bearer $DIBS_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "title": "Fall potluck",
    "text": "Desserts x3\nSalads x2\nPlates & napkins",
    "timezone": "America/New_York",
    "eventStartsAt": "2026-11-14T18:00:00-05:00",
    "location": "Elm Street Community Hall"
  }'
```

```json
{
  "sheet": { "id": "01a1...", "slug": "k7m2p9qa", "url": "https://dibs.page/k7m2p9qa", "status": "open",
             "totals": { "capacity": 6, "taken": 0, "remaining": 6, "slots": 3, "openSlots": 3 }, "...": "..." },
  "slots": [ { "id": "01a1...", "label": "Desserts", "capacity": 3, "taken": 0, "remaining": 3, "full": false, "...": "..." } ],
  "stillOpen": [ { "slotId": "01a1...", "label": "Desserts", "remaining": 3 } ],
  "warnings": []
}
```

Text the parser understands:

| You type | You get |
| --- | --- |
| `Desserts x3` | one slot for 3 people |
| `Plates & napkins` | one slot for 1 person |
| `Sat 9-11am: Setup x4` | a timed shift for 4 |
| `Tue 3:00-6:00pm every 15 min` | twelve 15-minute slots |
| `Dinner every day for 3 weeks starting Nov 3` | a 21-night rota (a meal train) |
| `Dinner every Monday from Nov 2 to Nov 20` | one slot per Monday (one line per weekday) |

Anything it had to guess comes back in `warnings`. Or send structured `slots` instead of `text`: `[{ "label": "3:00pm", "startsAt": "2026-11-10T15:00:00-06:00", "endsAt": "...", "capacity": 1 }]`. With a verified email, the organizer also gets a "your sheet is live" email (send `"notify": false` to skip it).

## Endpoints

| Method and path | What it does |
| --- | --- |
| `GET /me` | Who the key belongs to. |
| `GET /sheets` | Your sheets, newest first. |
| `POST /sheets` | Create (text or slots). |
| `GET /sheets/{id}` | The sheet, every slot's counts, and `stillOpen`. |
| `PATCH /sheets/{id}` | Change details or settings; `"status": "closed"` stops sign-ups. |
| `DELETE /sheets/{id}` | Archive (hidden, nothing erased). |
| `GET /sheets/{id}/slots` | Slots with counts. |
| `POST /sheets/{id}/slots` | Add slots (text or structured). |
| `PATCH /sheets/{id}/slots/{slotId}` | Rename, re-time, lock, change capacity. |
| `DELETE /sheets/{id}/slots/{slotId}?force=true` | Delete; people in it are emailed. |
| `GET /sheets/{id}/signups` | Who's signed up. `?status=waitlist`, `?slotId=`. |
| `POST /sheets/{id}/signups` | Put someone's name down. |
| `DELETE /sheets/{id}/signups/{signupId}` | Take someone off (sign-up or waitlist id). `?notify=false` skips their email. |
| `POST /sheets/{id}/messages` | Email participants. |
| `GET /public/sheets/{slug}` | Availability anyone with the link can see. No key, no names. |

Add someone (they get a confirmation with their own change/cancel link; see "Email to other people" below for when that's refused):

```sh
curl https://dibs.page/api/v1/sheets/$SHEET/signups \
  -H "Authorization: Bearer $DIBS_KEY" -H "Content-Type: application/json" \
  -d '{ "slotId": "'$SLOT'", "name": "Sam Okafor", "email": "sam@example.com", "comment": "Paper plates" }'
```

See who's coming, then message them:

```sh
curl "https://dibs.page/api/v1/sheets/$SHEET/signups?limit=100" -H "Authorization: Bearer $DIBS_KEY"

curl https://dibs.page/api/v1/sheets/$SHEET/messages \
  -H "Authorization: Bearer $DIBS_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: parking-note-1" \
  -d '{ "subject": "Parking", "body": "The front lot is closed. Use the side door.", "to": { "kind": "everyone" } }'
```

`to` is `{ "kind": "everyone" }`, `{ "kind": "slots", "slotIds": [...] }` or `{ "kind": "waitlist" }`. One email per person, replies go to the organizer once their email is verified. Up to 10 messages per sheet per day. The answer is `202 { "recipients", "sent", "failed", "skipped", "optedOut" }`: `optedOut` people asked not to get email about this sheet and were left out; `skipped` ones had already had 10 emails from Dibs that day.

### Email to other people: confirmed email and a monthly allowance

Messages, and the confirmation for someone you add with an email, are email the organizer sends to other people. Before anything is sent or added, the API checks, in this order:

1. **The organizer has confirmed their email** (used a sign-in link once). Otherwise `403 EMAIL_UNCONFIRMED`.
2. **Sending isn't paused** after bounces or spam reports. Otherwise `403 SENDING_PAUSED`.
3. **The whole send fits this month's allowance**, counted per recipient and reset on the 1st (UTC): messages count against `messageEmailsPerMonth`, people you add against `typedAddressEmailsPerMonth` (Free 500 / 100, half in the first week after confirming; Plus 5,000 / 2,000). Otherwise `429 QUOTA_EXCEEDED` with `Retry-After` (seconds until the reset) and:

```json
{ "error": { "code": "QUOTA_EXCEEDED", "status": 429,
  "message": "You have 2 message emails left this month, and this needs 3. It resets on November 1. Plus has room for more.",
  "details": { "quota": "message", "limit": 500, "used": 498, "remaining": 2, "needed": 3, "resetsAt": "2026-11-01T00:00:00.000Z", "plan": "free", "upgradeUrl": "https://dibs.page/pricing" } } }
```

A refused message sends nothing. A refused `POST .../signups` adds nobody; send `"notify": false` to add the person without an email (that never counts). The MCP tools `message_participants` and `add_person` return the same codes as tool errors (`EMAIL_UNCONFIRMED: ...`, with the error object in `structuredContent.error`). Participants' own emails (confirmations, reminders, waitlist news) are never limited.

Lower a slot's capacity below the people already in it and you get `409 CAPACITY_CONFLICT` with `error.details.plan.suggested` (newest sign-ups first). Send them back as `moveToWaitlist`; they go to the front of the waitlist and are emailed:

```sh
curl -X PATCH https://dibs.page/api/v1/sheets/$SHEET/slots/$SLOT \
  -H "Authorization: Bearer $DIBS_KEY" -H "Content-Type: application/json" \
  -d '{ "capacity": 2, "moveToWaitlist": ["01a1..."] }'
```

## Errors

One shape everywhere, with a message you can show a person:

```json
{ "error": { "code": "FULL", "message": "\"Desserts\" is full", "status": 409, "slotId": "01a1...", "details": { "remaining": 0 } } }
```

| Status | Codes |
| --- | --- |
| 400 | `BAD_REQUEST` (not JSON, wrong content type, bad Idempotency-Key) |
| 401 | `UNAUTHORIZED` (missing, wrong or revoked key) |
| 403 | `FORBIDDEN` (scope), `NOT_ALLOWED`, `EMAIL_UNCONFIRMED` (confirm your email to message people), `SENDING_PAUSED` (email to others is paused after bounces or spam reports) |
| 404 | `NOT_FOUND` (also for sheets that aren't yours) |
| 409 | `FULL`, `CLOSED`, `LOCKED`, `LIMIT_REACHED`, `CAPACITY_CONFLICT`, `NO_RECIPIENTS`, `IDEMPOTENCY_IN_PROGRESS` |
| 422 | `INVALID` (see `error.fields`: input path to what to fix), `IDEMPOTENCY_KEY_REUSED` |
| 429 | `RATE_LIMITED` (wait `Retry-After` seconds), `QUOTA_EXCEEDED` (this month's email allowance; `details.remaining`, `details.resetsAt`, `details.upgradeUrl`) |

## Limits, retries, pages

- **Rate limits:** 120 requests a minute per key, 60 of them changes. Every response carries `RateLimit-Limit` and `RateLimit-Remaining`. The public endpoint allows 60 a minute per IP.
- **Idempotency:** send `Idempotency-Key: <any unique string>` on `POST` creates (sheets, slots, sign-ups, messages). Retrying with the same key returns the first response (with `Idempotent-Replayed: true`) instead of doing it twice. The same key with a different body is refused. Keys last 24 hours.
- **Pages:** lists take `?limit=` (1 to 100, default 50) and return `nextCursor`; pass it back as `?cursor=` until it's `null`.
- Times are ISO 8601. Each sheet has an IANA time zone for reading and writing wall-clock times.

## MCP server

`https://dibs.page/api/mcp`, Streamable HTTP, stateless (POST only, JSON responses), with the same API key as a bearer token.

| Tool | Does | Hints |
| --- | --- | --- |
| `create_sheet` | Create from plain text or slots | |
| `list_sheets` | Your sheets with how full they are | read-only |
| `get_sheet` | Slots, counts, what's still open | read-only |
| `update_sheet` | Title, times, settings | idempotent |
| `update_slots` | Add, change (capacity, lock) or remove slots | destructive |
| `list_signups` | Who's signed up, or the waitlist | read-only |
| `add_person` | Put someone's name down (emails them) | open world |
| `remove_person` | Take someone off (waitlist moves up) | destructive |
| `message_participants` | Email everyone, some slots, or the waitlist | open world |
| `close_sheet` | Stop sign-ups (or `reopen: true`) | idempotent |

`create_sheet`, `add_person` and `message_participants` take an optional `idempotencyKey`. Every tool answers with a one-paragraph summary plus the JSON, so an assistant can relay it as is.

### Claude Code

```sh
claude mcp add --transport http dibs https://dibs.page/api/mcp \
  --header "Authorization: Bearer $DIBS_KEY"
```

Then ask: "Set up a meal train for the Smiths for three weeks, dinners only, starting Nov 3."

### Claude (claude.ai and the desktop app)

Settings, Connectors, **Add custom connector**: URL `https://dibs.page/api/mcp`. Under **Request headers** (where your plan shows it) add `Authorization` with the value `Bearer dibs_...`. Without that option, connect through Claude Code for now: sign-in with OAuth is on the list (see [limits](limits.md)).

### ChatGPT and the OpenAI API

ChatGPT's connectors sign in with OAuth, which Dibs doesn't offer yet. Through the OpenAI Responses API, pass the key as the MCP tool's `authorization`:

```json
{
  "type": "mcp",
  "server_label": "dibs",
  "server_url": "https://dibs.page/api/mcp",
  "authorization": "dibs_ab12cd34_...",
  "require_approval": "always"
}
```

### Anything else

Any MCP client that can send a header works. For clients that read an `mcp.json`:

```json
{ "mcpServers": { "dibs": { "type": "http", "url": "https://dibs.page/api/mcp", "headers": { "Authorization": "Bearer dibs_ab12cd34_..." } } } }
```

Keep keys out of shared files and chats; revoke one in your settings if it leaks.
