---
title: "Email participants of a signup, by Message Scope"
method: POST
path: "/signups/{id}/messages"
tags: ["Signups"]
---

# Email participants of a signup, by Message Scope

`POST /signups/{id}/messages`

## Request

- Base URL: `https://thesignup.app/api/v1`
- URL: `https://thesignup.app/api/v1/signups/{id}/messages`
- Auth: HTTP bearer

SENDS REAL EMAIL TO REAL PEOPLE, immediately and irreversibly. Requires the `messages:send` scope, which exists only for this — `reminders:send` does not grant it, and neither does anything else. Recipients are resolved SERVER-SIDE from `scope` (+ `scopeId`): the request carries no address list and no participant ids, and any unrecognized field is rejected with 422 rather than ignored. One email per person, deduplicated by address, so somebody holding three items hears from you once. Metered per organization per UTC day in RECIPIENTS, not in calls, with a ceiling of 1000. A send that would cross it is refused whole (429 `daily_ceiling_exceeded`) and nothing is delivered — it is never trimmed to fit. Every send writes an audit-log entry (`signup.messaged`) naming the credential, the scope, the recipient count and the subject; read it back at `GET /v1/audit_log`. Preview a scope before committing to it by reading `/signups/{id}/slot_coverage` and `/signups/{id}/participants`; this endpoint has no dry-run mode. The account that owns the credential must have a verified email address (returns 403 otherwise).

## Path parameters

- `id` string, uuid, required

## Request body

- SendMessageRequest
  - `scope` 'all' | 'day' | 'shift' | 'person' | 'roster' | 'gaps', required: WHO to message, resolved server-side against the signup's roster. `all` everyone attending; `day` one date; `shift` one Occurrence of a repeating slot; `person` one participant; `roster` everyone who has ever held one repeating Shift, across every date it runs; `gaps` the same people, worded as an ask to cover a short date.
  - `scopeId` string: What the scope names, when it names something: an `event_days` row id for `day`, a `time_slots` row id (one Occurrence) for `shift`, a participant registration id for `person`, `<seriesId>#<shiftIndex>` for `roster` and `gaps` (read both off `slot_coverage`). Omitted for `all`, which names itself.
  - `subject` string, required
  - `body` string, required: Plain text. `{name}`, `{slot}` and `{item}` are interpolated per recipient with that person's own name, shift and item; a token with nothing behind it renders the bare noun (`time slot`, `item`) rather than an empty string.

## Response `200`

Send summary. `sent + failed` equals `recipients`.

- SendMessageResponse
  - `recipients` integer, required: People the scope resolved to, after per-person deduplication. Equals `sent + failed`.
  - `sent` integer, required
  - `failed` integer, required: Recipients the mail transport refused. Their addresses are not returned.

## Other responses

- `401`: Unauthenticated — credentials missing, malformed, invalid, or revoked
- `403`: Forbidden — credentials valid but lack the required scope
- `404`: Not found
- `422`: Validation failed — request body / query / path is structurally wrong
- `429`: Rate limited or daily ceiling exceeded
- `500`: Internal server error

---

[API](https://skmtc.dev/thesignup/apis/thesignup-api.md) · [All operations](https://skmtc.dev/thesignup/apis/thesignup-api/llms.txt) · [OpenAPI document](https://skmtc.dev/thesignup/apis/thesignup-api/revisions/c6da7cf92172?raw)
