API keys and scopes
Issue a key that can do exactly one thing, send it as a bearer token, and read the one error shape.
An API key lets your own software read or write your workspace through https://<your-workspace>.monody.app/api/v1/integrations/*. The owner issues it under Account → API keys, picks what it may do, and the key is shown once. Send it on every request:
Authorization: Bearer monody_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxScopes are the vocabulary of what a key may do. Pick the least the integration needs — a landing page that posts leads needs leads:write and nothing else.
leads:read— list leads and read their status (GET /api/v1/integrations/leads, paged withcursorandlimit, filtered withstatus)leads:write— create a lead (POST /api/v1/integrations/leads)members:read,bookings:read,webhooks:manage— reserved: they are in the vocabulary so a key can be issued for them today, and the form says no endpoint reads them yet*— everything, present and future; for a tool you trust with all of it
A scope outside this list cannot be issued, and an old key carrying free text grants nothing for it.
Creating a lead with a key. POST /api/v1/integrations/leads with a JSON body: firstName, lastName (up to 120 characters each), email (stored lower-cased), phone (as typed — normalised to E.164 when it parses), source (defaults to campaign) and consent ({ marketing_email, marketing_sms } booleans; recorded as granted via the API). At least a name, an email or a phone is required. The answer is 201 with the lead: id, firstName, lastName, email, phone, source, status, score, duplicateOfId (the open lead this one matches, when it does) and createdAt. A lead.created webhook goes out for it.
Every refusal is one shape, whatever went wrong:
{ "error": { "code": "missing_scope", "message": "This key does not have the leads:read scope. Issue a key with it under Account → API keys.", "field": null } }Codes: missing_api_key and invalid_api_key (401 — no bearer, or a revoked or expired key), missing_scope (403), invalid_json and validation_failed (400 — field names the field), tenant_not_found (404). The message is written to be shown; code is what your code branches on.
Revoking. Revoke a key from its row; every request with it answers invalid_api_key from that moment. The row shows when the key was last used, so a key nothing has used in months is easy to spot and retire.