API basics.
Mailan has two halves and two kinds of credentials. Submissions come in through a public endpoint that anybody may call with your form's access key. Everything that reads data back needs a secret API key. This page covers what both halves share: the base URL, the response shape, status codes and the limits.
Base URL
Every API route sits below /api. On the hosted service that is https://mailan.email/api; on your own installation it is your domain with the same path. The health check is the one exception — it lives at the root, outside the prefix, so an uptime monitor does not have to know about /api.
Access key and API key
The two are not interchangeable. The access key identifies one form and may sit in the HTML of a public page — it can only ever create a submission for that one form. The API key belongs to your account, reads your data, and must stay on a server.
| Property | Access key | API key |
|---|---|---|
| Scope | One form | The whole account |
| Secret | No — it is public by design | Yes — treat it like a password |
| Sent as | URL segment or access_key field | Authorization: Bearer header |
| Allows | Creating a submission | Reading forms and submissions |
| Where to find it | Dashboard → form → settings | Dashboard → API keys |
An API key is shown exactly once, right after you create it: only its hash is stored, so it cannot be displayed again. Keys look like mln_live_… and can be revoked at any time; a revoked key answers 401 like an unknown one.
Response shape
Everything under /api/v1 answers in the same envelope: a success flag plus the payload in data. Errors carry the flag and a message instead. The submit endpoint is deliberately flatter — it answers a browser as well as a program, and returns success and message directly.
Status codes
Read the status code, not the message: the wording is meant for humans and may change, the codes will not.
| Code | Meaning | What to do |
|---|---|---|
200 |
Accepted or read successfully. | Nothing. |
303 |
Submission stored, browser is redirected to the target. | Only for classic form posts. Send Accept: application/json to get JSON instead. |
400 |
The request body could not be read or a value was invalid. | Check content type and field names. |
401 |
Missing, unknown or revoked API key. | Check the Authorization header; create a new key if needed. |
403 |
Captcha failed, origin not allowed, or the form is blocked. | See the message; check the form's allowed domains. |
404 |
Unknown access key, or a submission that is not yours. | A foreign resource looks exactly like a missing one — that is intended. |
413 |
An upload exceeded the size limit. | Check the file before sending; the server aborts mid-stream. |
429 |
Rate limit or monthly quota exceeded. | Back off and retry later. Do not retry a submission blindly. |
500 |
Something went wrong on our side. | Retry with backoff; the submission may still have been stored. |
Rate limits and quotas
Three limits exist side by side. Two protect the submit endpoint, one the read API; on top of them sits the monthly submission quota of your plan. The numbers below are the defaults of the hosted service — on your own installation they are environment variables.
| Limit | Default | Environment variable |
|---|---|---|
| Submissions per form and minute | 20 |
RATELIMIT_SUBMIT_PER_MINUTE |
| Submissions per IP and minute | 10 |
RATELIMIT_SUBMIT_PER_IP_PER_MINUTE |
| Read API requests per key and minute | 120 |
RATELIMIT_API_PER_MINUTE |
| Monthly submissions | According to your plan | Plan setting |
Exceeding any of them answers 429. The submit limits use a sliding window, so the budget refills continuously instead of all at once on the minute.
Allowed domains
A form can be restricted to the domains it is embedded on. With the list empty every origin is accepted; as soon as it has entries, a submission from anywhere else is answered with 403. The same list also decides which redirect targets are accepted — that is what keeps the endpoint from becoming an open redirect.
Server-to-server requests send no Origin header and are therefore never blocked by this — the restriction protects against a copied form on a foreign page, not against a program.