API for agents

Everything in ExcuseMe can be driven without the UI: surveys, email check-ins, triggered sends, results. If you are a human, this page is for you. If you are an agent, fetch the raw reference instead:

curl https://excuseme.app/llms.txt

Or connect the MCP server: EXCUSEME_API_KEY=<key> npx excuseme-mcp. API keys are minted in the dashboard under Settings, then API keys.

The full reference

Rendered from /llms.txt, the same file agents read.

# ExcuseMe

> ExcuseMe is a drop-in feedback widget (emoji, star, or thumbs) you embed on any
> website with one script tag. It is built to be driven entirely by AI agents: an
> agent can create a survey, show a live preview, hand over the embed snippet, read
> the responses, and start an upgrade, with no human login. The owner confirms once
> by email before any real responses are stored.

There are two equally supported ways to use ExcuseMe. Pick whichever fits your agent.
Neither is a fallback for the other.

## Option A: the HTTP API (works with plain curl or fetch)

Base URL: `https://app.excuseme.app/api/v1/manage`. Auth: `Authorization: Bearer <apiKey>`.
Creating an account needs no key. Send `Idempotency-Key` on creates to make retries safe.

Quickstart:

```bash
# 1. Create an account and get an API key (no key needed). Save the apiKey.
curl -sX POST https://app.excuseme.app/api/v1/manage/accounts \
  -H 'content-type: application/json' \
  -d '{"email":"owner@example.com"}'
# -> {"accountId":"...","apiKey":"exm_live_...","defaultWebsiteId":"..."}

KEY=exm_live_...

# 2. Create a survey. Returns a preview URL and the embed snippet.
curl -sX POST https://app.excuseme.app/api/v1/manage/surveys \
  -H "authorization: Bearer $KEY" -H 'content-type: application/json' \
  -d '{"type":"emojis","question":"How do you like the new dashboard?"}'
# -> {"surveyId":"...","status":"pending_confirmation","previewUrl":"https://app.excuseme.app/preview/...","embedSnippet":"<script src=\"https://hey.excuseme.app/excuseme.js?id=...\" defer></script>"}

# 3. Read results (summarize them yourself).
curl -s https://app.excuseme.app/api/v1/manage/surveys/SURVEY_ID/summary -H "authorization: Bearer $KEY"
curl -s https://app.excuseme.app/api/v1/manage/surveys/SURVEY_ID/responses -H "authorization: Bearer $KEY"
```

Endpoints: `POST /accounts`, `GET|POST /websites`, `GET|POST /surveys`,
`GET|PATCH|DELETE /surveys/{id}`, `GET /surveys/{id}/responses`,
`GET /surveys/{id}/summary`, `POST /surveys/{id}/upgrade` (returns a Stripe checkout URL),
`GET /billing` (plan, limits, and unlocked features).
Survey types: `emojis`, `stars`, `thumbs`. Errors are `{"error":{"code","message"}}`.

### Plan and features

`GET /billing` tells you what the account's plan includes:

```json
{
  "tier": "free",            // "free" | "business" | "enterprise"
  "businessUntil": null,     // ISO timestamp while a Business subscription runs
  "features": [],            // unlocked Business features, see below
  "activeLimit": 1,          // active surveys + check-ins the plan allows (null = unlimited)
  "activeUnpaid": 0          // how many of those slots are in use
}
```

`features` lists the unlocked Business features: `reminders` (automatic check-in reminder
emails), `branding` (the account's logo on the widget, emails, and share pages),
`integrations` (Slack and webhook notifications, CSV export), and `seats` (team members).
Business and Enterprise unlock all four. An endpoint behind a feature the plan lacks fails
with `403 {"error":{"code":"feature_locked","message":"...","feature":"<name>"}}`: do not
retry, suggest the Business upgrade instead.

### Email check-in endpoints

Check-ins are recurring email surveys. Each send is a **wave** that rotates through the
check-in's question list. Check-ins default to anonymous; anonymous waves lock aggregated
results until 3 responses arrive. `sendWeekday` is 0 (Sunday) to 6 (Saturday).

**POST /checkins**: create a check-in.

```json
// request
{
  "name": "Weekly team pulse",
  "anonymity": "anonymous",          // or "identified"
  "frequency": "weekly",             // "weekly" | "biweekly" | "monthly" | "manual"
  "sendWeekday": 1,                  // required unless frequency is "manual"
  "sendHour": 9,                     // 0-23; required unless frequency is "manual"
  "timezone": "America/New_York",    // IANA timezone; default "UTC"
  "questions": [
    { "type": "emojis", "question": "How did this week go?" }
  ],
  "recipients": [                    // optional; cap depends on plan (see below)
    { "email": "alice@example.com", "name": "Alice" }
  ]
}

// response 201
{
  "checkinId": "uuid",
  "publicId": "short-id",
  "shareUrl": "https://app.excuseme.app/s/<publicId>",
  "nextSendAt": "2024-01-08T09:00:00.000Z",   // null when frequency is "manual"
  "recipients": { "added": 1, "skipped": 0 }   // null if none supplied at create
}
```

Recipient caps per plan: Free 25, Pro-paid check-in 150, Business or Enterprise 500.
`400 recipient_limit` if the recipients list exceeds the cap (the check-in is still
created; `checkinId` is included in the error body). Send `Idempotency-Key` to make retries
safe.

**GET /checkins**: list all check-ins for the account. Returns an array of check-in objects.

**POST /checkins/{id}/recipients**: add recipients to an existing check-in.

```json
// request
{ "recipients": [{ "email": "bob@example.com", "name": "Bob" }] }

// response 200
{ "added": 1, "skipped": 0 }
```

Recipient caps: Free 25, Pro-paid check-in 150, Business or Enterprise 500.
`409 limit` if adding the batch would push the total over the cap. The error body is
`{"error":{"code":"limit","message":"...","limit":<number>}}` where `limit` is the cap
that applies to this check-in.
`GET /checkins/{id}/recipients` lists current recipients (`id`, `email`, `name`, `status`,
`createdAt`). `DELETE /checkins/{id}/recipients` with body `{"recipientId":"<uuid>"}` removes
one.

**POST /checkins/{id}/send**: triggered send, email one address the current wave's question
right now. Use this for CSAT-style flows (order shipped, ticket closed, etc.).

```json
// request
{ "email": "alice@example.com", "name": "Alice" }  // name optional

// response 200
{ "sent": true, "waveId": "uuid", "recipientId": "uuid" }
```

Errors: `402 limit_reached` (check-in reached its free cap; upgrade first),
`409 recipient_blocked` (address bounced or unsubscribed; do not retry),
`409 already_responded` (recipient already answered in the current wave).

**GET /checkins/{id}/summary**: per-wave results, oldest wave first.

```json
{
  "checkinId": "uuid",
  "waves": [
    {
      "waveId": "uuid",
      "question": "How did this week go?",
      "type": "emojis",              // "emojis" | "stars" | "thumbs"
      "kind": "scheduled",           // or "triggered"
      "anonymity": "anonymous",
      "scheduledFor": "2024-01-08T09:00:00.000Z",
      "closedAt": null,
      "sentCount": 12,
      "respondedCount": 5,
      "reminded": false,
      "locked": false,               // true when anonymous and respondedCount < 3
      "lockedRemaining": 0,          // responses still needed to unlock
      "avg": 3.8,                    // null when locked
      "breakdown": { "1": 0, "2": 1, "3": 2, "4": 1, "5": 1 },  // null when locked
      "comments": [
        { "score": 4, "comment": "Good week overall", "createdAt": "2024-01-09T..." }
      ],                             // null when locked
      "sources": { "email": 4, "link": 1 }
    }
  ]
}
```

### Webhooks (Business)

Business and Enterprise accounts can configure a Slack webhook and a signed generic
webhook in the dashboard under Settings, then Integrations. Every confirmed response
POSTs a JSON event to the generic URL:

```json
{
  "event": "checkin.response",        // or "survey.response"
  "entity": { "type": "checkin", "id": "uuid", "name": "Weekly team pulse" },
  "response": { "score": 4, "comment": "Good week overall", "createdAt": "2024-01-09T..." },
  "recipient": { "email": "alice@example.com", "name": "Alice" },  // identified check-ins only
  "accountId": "uuid"
}
```

Verify every delivery before trusting it. Each request carries two headers:
`X-ExcuseMe-Timestamp` (unix seconds) and `X-ExcuseMe-Signature`. The signature is
`sha256=` followed by hex `HMAC_SHA256("<timestamp>.<rawBody>", secret)`, where
`<rawBody>` is the exact request body bytes and `secret` is the webhook secret shown
in the dashboard. Recompute it, compare in constant time, and reject any timestamp
older than 5 minutes (replay protection).

Delivery is at-least-once with up to 3 attempts: a failed attempt retries after 30
seconds, then after 5 minutes, then the event is dropped. Anonymity holds for webhooks
too: events from anonymous check-in waves never include `recipient`, and they are held
back until the wave reaches its 3-response unlock, then flushed together. Events
snapshot the response as it was stored: a comment the respondent adds later does not
re-dispatch or update an already-sent event.

## Option B: the MCP server

`npx excuseme-mcp` exposes the same operations as MCP tools (`excuseme_create_survey`,
`excuseme_preview`, `excuseme_get_summary`, ...). Set `EXCUSEME_API_KEY` in its env.
See the package README for client config.

## How consent works

A new survey previews immediately but stores zero real responses until the owner clicks
the confirm link emailed at account or website creation. Show the human the `previewUrl`
in the same turn; they confirm once and collection starts.

## Pricing

Free: 1 active survey, 100 responses per survey. Paid: 1.99 USD or EUR per survey per
month for unlimited responses. Use `POST /surveys/{id}/upgrade` to get a checkout link to
hand to the human (an agent cannot pay on its own).