Muro
API reference

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 with 401 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"
  }
}
  • code is a stable, machine-readable string. Branch on this.
  • message is a human-readable explanation, safe to show to a user. Its wording may change.
  • field is present only for errors about one request field, and names that field.

Error codes

StatusCodeWhen
400INVALID_JSONThe request body isn't valid JSON.
401API_KEY_REQUIREDNo Authorization: Bearer header, or an empty key.
401API_KEY_INVALIDThe key is wrong or revoked, or was sent in the query string.
403PLAN_REQUIREDYour plan doesn't include API access.
403LINK_LIMIT_REACHEDYou're at your plan's link limit.
403PLAN_LIMIT_REACHEDYou're at your plan's tag limit, or tag_names would create tags beyond it.
403UTM_TEMPLATE_LIMIT_REACHEDYou already have 100 UTM templates.
404LINK_NOT_FOUNDNo live link with that ID in your account.
404TAG_NOT_FOUNDNo tag with that ID in your account. Also returned when tag_ids contains one.
404UTM_TEMPLATE_NOT_FOUNDNo UTM template with that ID in your account.
409SLUG_TAKENThe custom slug is already in use on muro.ink.
409EXTERNAL_ID_TAKENAnother of your live links already has that external_id.
409TAG_NAME_TAKENYou already have a tag with that name (ignoring case).
409UTM_TEMPLATE_NAME_TAKENYou already have a template with that name (ignoring case).
422VALIDATION_ERRORA field is missing, has the wrong type, or breaks a rule. field says which. Also returned when the body isn't a JSON object.
422SLUG_INVALIDThe custom slug has characters other than letters, numbers, - and _, is longer than 64 characters, or is a reserved word.
429RATE_LIMITEDOver 1,000 requests this hour. See Retry-After.
500INTERNAL_ERRORSomething 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.

ParameterDefaultNotes
page1The page to return, starting at 1.
per_page20Links 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_id or comments, are left out of responses rather than returned as null.
  • Unknown fields in a request body are ignored.

Next steps

On this page