Urlicer API v1

Errors

Every way a request can fail, the body you get back, and whether retrying helps.

A failed request answers with a JSON body carrying an error field. Branch on that field, because it is stable. The messages beside it are in English, and we may reword them, so do not match on them.

The shape of an error
{
  "error": "feature",
  "key": "CustomAliases",
  "upgrade": true
}

Some errors add fields of their own: key for the limit that was hit, messages for what failed validation, alias for the code that was taken. Those are documented per error below.

All errors at a glance

Status error Means Retry?
400 validation The request body was malformed or missing No
401 unauthorized The key was missing, unknown, revoked or expired, or the workspace is inactive No
403 feature Your plan does not include something the request asked for No
403 quota A numeric allowance is used up No
404 not_found No link with that code in this workspace No
409 alias_taken That custom alias is already in use Yes with a different alias
409 ambiguous_code That code exists on more than one of your domains Yes with ?domain= naming one of the listed domains
429 rate_limit The key made more requests in the current 60-second window than its plan allows Yes unchanged, after waiting up to 60 seconds
500 server_error Something failed on our side Yes once, after checking whether the write landed

In detail

400 validation

The request body was malformed or missing

A required field is absent, a URL is not a URL, an alias is longer than 40 characters or uses characters other than letters, digits, - and _, or the body was not sent at all. It is also the answer when a destination is refused (a denylisted host, or an address that is already a short link) when the domain you named on create is not yours or not verified and active, and when a ?domain= on a single-link request names a domain that is not yours. The messages array says which, in English.

What to do: Fix the request. Retrying it unchanged will fail identically.

{
  "error": "validation",
  "messages": [
    "Wrong URL format."
  ]
}

401 unauthorized

The key was missing, unknown, revoked or expired, or the workspace is inactive

Every one of those cases answers identically, so that guessing at keys learns nothing from the response.

What to do: Check the Authorization header is present and reads "Bearer " followed by the key. If it does, the key has been revoked or has expired: issue a new one.

{"error": "unauthorized"}

403 feature

Your plan does not include something the request asked for

Custom aliases, expiry, passwords, custom domains and bulk creation are each checked separately. The key field names the one that was refused.

What to do: Either drop that field from the request, or move to a plan that includes it. Nothing was created, so there is nothing to clean up.

{
  "error": "feature",
  "key": "CustomAliases",
  "upgrade": true
}

403 quota

A numeric allowance is used up

Most often MaxLinks, the number of links the plan allows you to create per month. The allowance counts links created, so deleting links does not free it up.

What to do: Wait for the allowance to reset at the start of next month, or move up a plan. Do not retry in a loop: the answer will not change until one of those happens.

{
  "error": "quota",
  "key": "MaxLinks",
  "limit": 500,
  "current": 500,
  "upgrade": true
}

404 not_found

No link with that code in this workspace

Either the code does not exist, or it belongs to somebody else. Both answer 404, because the API never confirms that another workspace's link is real.

What to do: Check the code. Note that it is the code alone, not the whole short URL.

{"error": "not_found"}

409 alias_taken

That custom alias is already in use

Aliases are unique per domain. The same alias may already exist on the domain you are creating on, including on a link somebody else in your workspace made.

What to do: Pick a different alias, or omit it and take a generated code. Of the errors on this page, this is the only one to retry automatically, and only with a different alias.

{
  "error": "alias_taken",
  "alias": "spring-sale"
}

409 ambiguous_code

That code exists on more than one of your domains

Only on GET, PATCH and DELETE /api/v1/links/{code} without a domain query parameter. Codes are unique per domain, so the same alias can sit on ivi.to and on each of your custom domains. domains lists the domains that hold it, with null for ivi.to. Nothing was read, changed or deleted.

What to do: Send the same request again with ?domain= set to the one you meant, e.g. ?domain=go.example.com, or ?domain=ivi.to for the default domain.

{
  "error": "ambiguous_code",
  "code": "spring-sale",
  "domains": ["go.example.com", null]
}

429 rate_limit

The key made more requests in the current 60-second window than its plan allows

The budget is per key. The window starts with the key's first request and lasts 60 seconds; it is not the clock minute. limit repeats the ceiling, and the same figure is on every response in the X-RateLimit-Limit header. The body has no reset time and there is no Retry-After header.

What to do: Wait up to 60 seconds and retry the same request. Nothing was created, so a retry is safe.

{
  "error": "rate_limit",
  "limit": 30
}

500 server_error

Something failed on our side

Rare, and never your request's fault. message says what failed. An unexpected failure also carries a reference, the same value as the X-Error-Reference header; quote it to support.

What to do: Retry once after a short pause. If it persists, the response does not tell you whether the link was created, so list your links to check before contacting support.

{
  "error": "server_error",
  "message": "The link could not be created."
}

Handling them in one place

Four of these need different handling in your code. Retrying a validation failure will never succeed, while giving up on a rate_limit throws away a request that would have gone through within a minute.

# Capture the body and the status code from one call.
response=$(curl -s -w '\n%{http_code}' -X POST "https://urlicer.com/api/v1/links" \
  -H "Authorization: Bearer url_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/a-very-long-address"}')

status=$(printf '%s' "$response" | tail -n1)
body=$(printf '%s' "$response" | sed '$d')

if [ "$status" -ge 400 ]; then
  case "$(printf '%s' "$body" | jq -r '.error // "unknown"')" in
    rate_limit)
      # Wait 60 seconds for the window to end, then send the same request.
      sleep 60 && exec "$0" "$@" ;;
    alias_taken)
      # Someone already has that code. Retry with a different alias.
      echo "alias taken: $(printf '%s' "$body" | jq -r .alias)" >&2 ; exit 1 ;;
    validation)
      # Our own bug: the request was malformed. Retrying will not help.
      printf '%s' "$body" | jq -r '.messages[]' >&2 ; exit 1 ;;
    feature|quota)
      # A plan limit. A person has to decide something; alert, do not retry.
      echo "plan limit: $(printf '%s' "$body" | jq -r .key)" >&2 ; exit 1 ;;
    *)
      echo "Urlicer API error $status" >&2 ; exit 1 ;;
  esac
fi

Nothing partial is ever left behind by a rejected request. A refused create creates no link and spends no allowance. The single exception is deliberate and documented: bulk creation reports per-row results, so some rows can succeed while others fail in the same request.