API reference

One public endpoint does the collecting, and it needs no key. Everything that creates and reads — projects, endpoints, records, export — is one key-authenticated management API, which the dashboard itself is a client of.

Two hosts

HostServes
api.mailcollectly.comThe collect endpoint, and nothing else. Cookieless, permissive CORS, callable from anywhere.
mailcollectly.comDashboard, docs, and the management API. Session or API-key authenticated.

They are one application. The split exists so a public form endpoint never sits on the same origin as your signed-in session.

POST /c/:public_id

The collect surface. No authentication, by design — the protection is the pipeline, not a secret in your page source.

request
curl -i -X POST https://api.mailcollectly.com/c/lst_a495ba \
  -H "Content-Type: application/json" \
  -d '{"email":"rowan@havenlabs.io","name":"Rowan","source":"launch-post"}'

Headers

HeaderEffect
Content-Typeapplication/json, application/x-www-form-urlencoded, or multipart/form-data. Anything unrecognised is parsed as URL-encoded.
OriginChecked against the endpoint's allowed hosts when present. Absent is allowed.
RefererUsed for the host check when there's no Origin, and stored as the record's source URL.
Accepttext/html on a form-encoded post opts into the 303 redirect instead of a JSON body.

Status codes

StatuserrorWhen
200successStored, forwarded, deduplicated, or quietly discarded as spam.
303A plain form post with Accept: text/html and a valid _redirect.
400invalid_requestThe body couldn't be parsed as JSON, URL-encoded, or multipart.
401unauthorizedManagement API and MCP only — a missing or invalid key.
403domain_not_allowed · captcha_failedOrigin not on the allowed list, or the captcha refused.
404not_foundNo endpoint with that ID.
409account_over_record_limit · endpoint_paused · endpoint_limit_reachedA limit or a state, not a malformed request. Something has to change before a retry helps.
422invalid_emailFailed syntax validation, or a disposable-provider address.
429rate_limitedPer-address limit or the endpoint's daily cap. Carries retry_after.

CORS

The collect route answers OPTIONS with 204 and allows any origin, any of the three content types, and POST. Browsers can call it from anywhere; whether a submission is kept is the origin check’s business, not CORS’s.

The error contract

Every refusal — collect, management API, MCP tool result, and the dashboard’s own banner — is the same object in different costumes. Two fields are always present:

  • error — a stable machine code. Branch on this.
  • message — written for the person who hit it. Show this.

Everything else is code-specific: records_used and records_limit on a record-limit refusal, retry_after on a rate limit, reason and fix on a captcha failure. Codes are added over time; treat an unknown one as its status class and show the message. All of them are printed here.

Management API

Everything the dashboard does — projects, endpoints, records, export — over one key-authenticated API at https://mailcollectly.com/api/v1. The dashboard is a client of it rather than a privileged path around it, so anything you can do by clicking, you can do by calling.

Getting a key

Settings → API keys, in the dashboard. The key is shown once, at creation. What’s stored is a sha256 of it, so there is no “reveal” anywhere and nobody — including us — can read it back. Lost it? Mint another and revoke the old one; that takes about five seconds and is the only design under which the storage claim stays true.

auth
curl https://mailcollectly.com/api/v1/usage \
  -H "Authorization: Bearer mc_live_…"

Revoking takes effect on the next call. A signed-in dashboard session authenticates the same handlers, which is how the app itself uses them — but cookie-authenticated writes from another origin are refused, so a key is the right answer for anything that isn’t the dashboard.

Shapes

  • A single resource comes back as itself — a bare JSON object.
  • A collection comes back as { data: [...] }, plus pagination where there’s paging.
  • A refusal is the error contract above, and carries the same usage headers as a success.
  • Endpoints are addressable by their public ID (lst_9f3a2c) or their UUID. The public one is what you already have.
MethodPathWhat it does
GET/api/v1The index — every route, for whoever pastes the base URL.
GET/api/v1/usageRecords, endpoints, projects, and where they stand.
GET/api/v1/projectsEvery project, with endpoint and record counts.
POST/api/v1/projectsCreate one. Its first list is provisioned in the same transaction and comes back under provisioned.
GET/api/v1/projects/:idOne project.
PATCH/api/v1/projects/:idRename it, or set its domain.
DELETE/api/v1/projects/:idDelete it, its endpoints, and their records. There is no undo.
GET/api/v1/endpointsEvery endpoint. Filter with ?project=<uuid> and ?kind=list|form.
POST/api/v1/endpointsCreate a list or a form in a project you own.
GET/api/v1/endpoints/:idOne endpoint, including its collect URL.
PATCH/api/v1/endpoints/:idname, status, allowed_domains, allow_localhost, turnstile, daily_cap.
DELETE/api/v1/endpoints/:idDelete it and its records.
GET/api/v1/endpoints/:id/recordsNewest first. ?limit= (50, max 200) &offset= &search=.
GET/api/v1/endpoints/:id/exportCSV. No cap check, no plan check, ever.
GET/api/v1/endpoints/:id/snippetsThe same code the Get Code screen shows. ?language=html|react|fetch|curl.
DELETE/api/v1/records/:idDelete one record.
DELETE/api/v1/recordsBulk delete: {"ids":[…]}, up to 1,000. Reports how many were actually yours.

A worked example

From nothing to a live endpoint with a record in it, in four calls. The first one returns the list that was provisioned with the project — nothing has to be created separately for the project to be collecting.

create, collect, read, export
# 1 · a project, and the list that came with it
curl -X POST https://mailcollectly.com/api/v1/projects \
  -H "Authorization: Bearer $MC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Squire Editor","domain":"squireeditor.com"}'
# → { "id": "…", "provisioned": [ { "public_id": "lst_9f3a2c", "url": "…" } ] }

# 2 · anyone can post to it — no key, that's the collect surface
curl -X POST https://api.mailcollectly.com/c/lst_9f3a2c \
  -H "Content-Type: application/json" \
  -d '{"email":"rowan@havenlabs.io","name":"Rowan"}'

# 3 · read it back, newest first
curl "https://mailcollectly.com/api/v1/endpoints/lst_9f3a2c/records?limit=50" \
  -H "Authorization: Bearer $MC_KEY"

# 4 · or take the whole thing as CSV
curl "https://mailcollectly.com/api/v1/endpoints/lst_9f3a2c/export" \
  -H "Authorization: Bearer $MC_KEY" -o signups.csv

Pagination

Record lists carry a pagination object saying what was actually used — limit, offset, total, has_more — so a caller never has to guess whether its limit was honoured. Ask for more than 200 and you get 200, said out loud rather than silently.

records response
{
  "data": [
    {
      "id": "9d3f…",
      "email": "rowan@havenlabs.io",
      "name": "Rowan",
      "data": {
        "source": "launch-post"
      },
      "country": "US",
      "created_at": "2026-08-02T18:11:04.220Z"
    }
  ],
  "pagination": {
    "limit": 50,
    "offset": 0,
    "total": 3842,
    "has_more": true
  }
}

Usage headers

Every authenticated response carries where you stand — successes and refusals alike, because the moment you most want to know is the moment you have just been told no.

response headers
X-Records-Used: 8412
X-Records-Limit: 10000
X-Endpoints-Used: 7
X-Endpoints-Limit: 10

MCP server

Streamable HTTP at https://mailcollectly.com/api/mcp, authenticated with the same API key. Point an agent at it and it can provision an endpoint mid-build, without you leaving the editor to click anything.

claude code
claude mcp add --transport http mail-collectly https://mailcollectly.com/api/mcp \
  --header "Authorization: Bearer $MC_KEY"

For editors configured by file, it’s the same three facts — transport, URL, header:

mcp config
{
  "mcpServers": {
    "mail-collectly": {
      "type": "http",
      "url": "https://mailcollectly.com/api/mcp",
      "headers": {
        "Authorization": "Bearer ${MAIL_COLLECTLY_KEY}"
      }
    }
  }
}

The tools

ToolWhat it does
list_projectsEvery project, with its endpoint and record counts.
list_endpointsEvery list and form, or one project's — id, name, kind, status, collect URL, record count. Where an agent starts when it didn't create the endpoint itself.
create_projectCreate a project. Its first list is provisioned with it and comes back live.
create_listAn email list in a project, addressed by id or by exact name.
create_formA form endpoint in a project. Stored submissions.
get_embed_codeThe paste-ready snippet, spam fields included.
list_recordsRecords for an endpoint, newest first, paginated.
delete_recordsDelete records by id. Frees the allowance immediately.
export_csvCSV for an endpoint, a project, or everything.
get_usageWhere the account stands against its limits.

Every tool is a wrapper over the routes above — same store, same limits, same refusals — and every result carries the usage block, so an agent can tell you your form isn’t broken, you’ve just hit the fence. A refusal arrives with its human sentence first, which is the sentence worth repeating to you verbatim rather than paraphrasing.

Addressing things by name

The session that creates an endpoint knows its id. Every session afterwards doesn’t — so get_embed_code, list_records and export_csv also take project plus endpoint_name in place of an id, and create_list / create_form take a project by name.

Endpoint names are only unique inside a project — “Signups” is the default list name, so an account with two projects has two of them — which is why the name always needs its project alongside it. A miss answers with what does exist: No endpoint called “Waitlist” in “Nightjar”. It has “Signups” and “Tour dates”. An agent corrects itself from that in one step instead of asking you.

WHAT AN AGENT CAN’T DO

There is no tool that deletes a project or an endpoint, and none that mints or reads API keys. Removing a whole audience is a decision to make in the dashboard, where the typed confirmation is; a key that can create more keys is a key worth stealing.

Endpoint IDs

IDs are prefixed by kind: lst_ for lists, frm_ for forms — lst_a495ba. They are public: they ship in your page source the way any form action does, and they are safe there because the collect surface is designed to be posted to by strangers.

NEXTBack to the quickstartThe four snippets, every field the collect API understands, and what comes back.