Authentication, limits and errors
How to authenticate API requests, manage your key, stay within the rate limit, read errors and page through results.
Every request to the Muro API carries your API key in a header. This page covers the key itself, the hourly rate limit, the error format and codes, and pagination.
Authenticate with a Bearer header
Send your key in the Authorization header on every request:
curl https://app.muroanalytics.com/api/v1/links \
-H "Authorization: Bearer muro_lnk_xxxxxxxxxxxx"- Keys go in the header only. A key in the query string (
?api_key=,?apikey=or?key=) is refused with401 API_KEY_INVALID, so it doesn't end up in logs or browser history. - A missing header returns
401 API_KEY_REQUIRED. - A wrong or revoked key returns
401 API_KEY_INVALID. - If your account's plan doesn't include API access, every request returns
403 PLAN_REQUIRED.
Your key has full access to your account's links, tags and UTM templates. Use it only from servers and scripts you control, never in browser code or a public repository.
Your API key
Manage your key in Settings → API Keys. API access needs the Team plan or a free trial.
- One key per account. There are no scopes: the key can do everything the API allows for your account.
- Shown once. When you click Generate API Key, the full key appears a single time. Afterwards Muro shows only its first characters (like
muro_lnk_a1B2…), when it was created, and when it was last used, or never used. - Revoke: click Revoke key and confirm. The key stops working immediately, and anything using it fails until you generate a new one.
- Replace (rotate): revoke the current key, then click Generate API Key again and update your integrations with the new key.
If your account moves to a plan without API access, requests return 403 PLAN_REQUIRED. In some cases the key is also revoked. After you upgrade, generate a new key if the old one returns 401 API_KEY_INVALID.
Rate limit
Each key can make 1,000 requests per hour. The hour is a fixed window that resets at the top of each hour, and every authenticated request counts, including ones that return an error.
Over the limit, requests return 429 with the code RATE_LIMITED and a Retry-After header: the number of seconds until the window resets.
HTTP/1.1 429 Too Many Requests
Retry-After: 1260
Content-Type: application/json
{ "error": { "code": "RATE_LIMITED", "message": "API rate limit exceeded. Try again later." } }If you hit it, wait for Retry-After seconds before retrying. For bulk jobs, spread requests out, and use upsert instead of listing links to check whether one exists.
Errors
Every error uses the same envelope:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Title is required.",
"field": "title"
}
}codeis a stable, machine-readable string. Branch on this.messageis a human-readable explanation, safe to show to a user. Its wording may change.fieldis present only for errors about one request field, and names that field.
Error codes
| Status | Code | When |
|---|---|---|
400 | INVALID_JSON | The request body isn't valid JSON. |
401 | API_KEY_REQUIRED | No Authorization: Bearer header, or an empty key. |
401 | API_KEY_INVALID | The key is wrong or revoked, or was sent in the query string. |
403 | PLAN_REQUIRED | Your plan doesn't include API access. |
403 | LINK_LIMIT_REACHED | You're at your plan's link limit. |
403 | PLAN_LIMIT_REACHED | You're at your plan's tag limit, or tag_names would create tags beyond it. |
403 | UTM_TEMPLATE_LIMIT_REACHED | You already have 100 UTM templates. |
404 | LINK_NOT_FOUND | No live link with that ID in your account. |
404 | TAG_NOT_FOUND | No tag with that ID in your account. Also returned when tag_ids contains one. |
404 | UTM_TEMPLATE_NOT_FOUND | No UTM template with that ID in your account. |
409 | SLUG_TAKEN | The custom slug is already in use on muro.ink. |
409 | EXTERNAL_ID_TAKEN | Another of your live links already has that external_id. |
409 | TAG_NAME_TAKEN | You already have a tag with that name (ignoring case). |
409 | UTM_TEMPLATE_NAME_TAKEN | You already have a template with that name (ignoring case). |
422 | VALIDATION_ERROR | A field is missing, has the wrong type, or breaks a rule. field says which. Also returned when the body isn't a JSON object. |
422 | SLUG_INVALID | The custom slug has characters other than letters, numbers, - and _, is longer than 64 characters, or is a reserved word. |
429 | RATE_LIMITED | Over 1,000 requests this hour. See Retry-After. |
500 | INTERNAL_ERROR | Something went wrong on Muro's side. Retry later. |
A resource that belongs to another account returns the same 404 as one that doesn't exist.
Successful responses use 200 OK, 201 Created for new resources, and 204 No Content (with no body) for deletes.
Pagination
GET /links is paginated. The other list endpoints (GET /tags, GET /utm-templates) return everything in one response.
| Parameter | Default | Notes |
|---|---|---|
page | 1 | The page to return, starting at 1. |
per_page | 20 | Links per page, from 1 to 100. Larger values are capped at 100. |
The response includes page, per_page and total (all links matching your filter). Keep requesting the next page until page × per_page reaches total:
const headers = { Authorization: `Bearer ${process.env.MURO_API_KEY}` };
const all = [];
for (let page = 1; ; page++) {
const res = await fetch(
`https://app.muroanalytics.com/api/v1/links?page=${page}&per_page=100`,
{ headers },
);
const data = await res.json();
all.push(...data.links);
if (page * data.per_page >= data.total) break;
}Links are returned newest first.
Conventions
- IDs have a prefix:
lnk_for links,tag_for tags,utmt_for UTM templates. - Timestamps are Unix time in milliseconds.
- Optional link fields that aren't set, like
external_idorcomments, are left out of responses rather than returned asnull. - Unknown fields in a request body are ignored.