12345678{ "error": { "code": 40001, "constant": "INVALID_URL", "message": "The destination URL is malformed or missing a valid protocol.", "requestId": "req_aB3cD4eF5g" } }
requestId field can be included in support tickets to help diagnose specific failures.| HTTP Status | Code | Constant | Description & Resolution |
400 Bad Request | 40001 | INVALID_URL | The destination URL is malformed or lacks a valid protocol (http:// or https://). Validate the URL format before submitting. |
400 Bad Request | 40002 | INVALID_SLUG | The slug contains invalid characters or exceeds 2,048 bytes. Slugs must match /^[a-z0-9]+(?:-[a-z0-9]+)*$/i. |
400 Bad Request | 40003 | RESERVED_SLUG | The requested slug is reserved for system use (e.g. api, auth, dashboard, docs). Choose a different slug. |
400 Bad Request | 40004 | INVALID_EXPIRY | The expiration timestamp is in the past or uses an invalid ISO 8601 format. |
401 Unauthorized | 40101 | UNAUTHORIZED | Missing or invalid Authorization: Bearer <token> header. Verify your API key is current and correctly formatted. |
403 Forbidden | 40301 | FORBIDDEN | The API key does not have write permission. Use a Full Access key for create/update/delete operations. |
403 Forbidden | 40302 | QUOTA_EXCEEDED | The organization has exceeded its plan's link limit or custom domain allocation. Upgrade or wait for billing cycle renewal. |
404 Not Found | 40401 | LINK_NOT_FOUND | The requested slug does not exist on the specified domain. |
404 Not Found | 40402 | DOMAIN_NOT_FOUND | The specified custom domain is not registered in this workspace. |
409 Conflict | 40901 | SLUG_EXISTS | A short link with this slug already exists on the chosen domain. Use PATCH /links/:slug to update it, or choose a different slug. |
429 Too Many Requests | 42901 | RATE_LIMITED | Request velocity exceeds the plan's rate limit window. Implement exponential backoff with jitter. |
| HTTP Status | Code | Constant | Description & Resolution |
500 Internal Server Error | 50001 | INTERNAL_ERROR | An unexpected edge error occurred. Retry with exponential backoff. If persistent, check status.shaf.app. |
503 Service Unavailable | 50301 | EDGE_UNAVAILABLE | The edge worker encountered a transient fault. Retry after a short delay. |
429 RATE_LIMITED response, the response includes a Retry-After header indicating how many seconds to wait:12345HTTP/2 429 retry-after: 15 x-ratelimit-limit: 120 x-ratelimit-remaining: 0 x-ratelimit-reset: 1727390415
123456789101112131415async function requestWithRetry(fn: () => Promise<Response>, maxRetries = 3): Promise<Response> { for (let attempt = 0; attempt <= maxRetries; attempt++) { const response = await fn() if (response.status === 429) { const retryAfter = Number(response.headers.get('retry-after') ?? '5') const jitter = Math.random() * 1000 await new Promise(resolve => setTimeout(resolve, (retryAfter * 1000) + jitter)) continue } return response } throw new Error('Max retries exceeded') }
https://). URLs without protocols (e.g. example.com/path) are rejected. Also check for invisible Unicode characters that can be inadvertently pasted from some text editors or CMS systems.cat or shell substitution.PATCH /v1/links/{slug} with only the fields you wish to change. The POST /v1/links endpoint is for creating new links only. See the Links API Reference for the update schema.