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.
{
"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.
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. Of the errors on this page, this is the only one to retry automatically, and only with a different alias.
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.
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.
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.
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.