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. That field
is the part to branch on: it is stable, while the human-readable messages beside it are
not: we improve their wording, and they follow the language of the account.
{
"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 |
| 429 | rate_limit |
The key made more requests this minute than its plan allows | Yes unchanged, once the minute turns over |
| 500 | (no error field) |
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 too long, or the body was not sent at all. The messages array says which, in the same words the dashboard would use.
What to do: Fix the request. Retrying it unchanged will fail identically.
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.
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.
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.
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.
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. This is the one error worth retrying automatically, with a different alias.
429
rate_limit
The key made more requests this minute than its plan allows
The budget is per key and resets every minute. limit repeats the ceiling, and the same figure is on every response in the X-RateLimit-Limit header.
What to do: Wait for the minute to turn over and retry the same request. Nothing was created, so a retry is safe.
500
(no error field)
Something failed on our side
Rare, and never your request's fault. Note the shape is the older one used across the rest of our API, carrying message and errors with no error field, so a client that switches on error must have a fallback branch.
What to do: Retry once after a short pause. If it persists, the state of the link is genuinely unknown: list your links to see whether the write landed before contacting support.
Handling them in one place
Four of these need different responses from your code, and the difference matters: retrying
a validation failure in a loop will never succeed, while not retrying a
rate_limit throws away work that would have gone through a minute later.
# 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)
# The window is a fixed minute. Wait for it, 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.