6.1 Pagination
List endpoints return at most 100 items per call and a cursor for the next page:
curl "https://api.urlcrop.com/v1/links?limit=50&status=active&sort=-created_at" -H "Authorization: Bearer $URLCROP_API_KEY"{ "data": [ "…50 links…" ], "pagination": { "limit": 50, "has_more": true, "next_cursor": "eyJjIjoiMjAyNi0xMi0wNVQxMDoxNTozMFoiLCJpIjoibG5rXzAxSkI0In0" } }Pass cursor=<next_cursor> to continue; next_cursor is null on the last page. The Node SDK iterates for you: for await (const link of uc.links.list({ status: 'active' })) { … }.
Filters are plain query parameters (status, domain, campaign_id, tag_ids, q, created_after), sorting is sort=field or sort=-field, and expand[]=campaign embeds related objects.
6.2 Errors
Every error has the same shape:
{
"error": {
"type": "conflict_error",
"code": "slug_taken",
"message": "The slug \"diwali\" is already in use on crop.sh.",
"param": "slug",
"request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
"doc_url": "https://docs.urlcrop.com/api/errors#slug_taken"
}
}| HTTP | code | Meaning |
|---|---|---|
| 400 | invalid_json, invalid_query, invalid_cursor, invalid_sort, invalid_expand | The request could not be understood |
| 401 | unauthenticated | Missing, invalid, revoked or expired key; error.details[0].code says which |
| 403 | insufficient_scope | Your key lacks the scope in param |
| 403 | email_verification_required | Verify the account email to continue |
| 403 | two_factor_required | Dashboard session needs its second factor |
| 403 | write_blocked_past_due | Payment overdue for 7+ days; reads work, writes are paused until the invoice is paid |
| 403 | feature_not_in_plan | The feature is not on your plan (param names it); details[0].upgrade_to tells you which plan has it |
| 403 | plan_limit_reached | A quota is used up (param is the plan entitlement key: links_per_month, dynamic_qr_total, pages_total, custom_domains, seats, webhook_endpoints, events_per_month) |
| 404 | resource_not_found | Unknown id (or it belongs to another workspace) |
| 409 | slug_taken, domain_already_exists, state_conflict, resource_in_use, idempotency_in_progress | Conflict with current state |
| 422 | validation_failed, destination_invalid, destination_blocked, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, policy_acceptance_required | Request is well-formed but not acceptable; details[] lists fields |
| 429 | rate_limited | Slow down; Retry-After says how long. param is burst, daily_quota or endpoint |
| 5xx | internal_error, upstream_unavailable, service_unavailable, timeout | Our side; retry with backoff and quote request_id to support |
Always log request_id — support can trace any request with it.
6.3 Rate limits
| Plan | Requests per day (API keys) | Burst (per key) |
|---|---|---|
| Pro | 50,000 | 20 per second |
| Business | 200,000 | 50 per second |
| Agency | 500,000 | 50 per second |
| Enterprise | Custom | Custom |
The daily quota resets at 00:00 UTC; monthly quotas (links, events) reset at the start of your billing period. Every response tells you where you stand:
X-RateLimit-Limit: 50000
X-RateLimit-Remaining: 49213
X-RateLimit-Reset: 1764979200Need more in a day? Buy a 1M API calls pack (valid 12 months, consumed only after the daily quota) or upgrade. The API runs behind Cloudflare: requests must complete within 100 seconds and bodies are capped at 100 MB (our own limits are lower: 1 MB JSON, 10 MB bulk, 5 MB uploads).
6.4 Idempotency
Network hiccups happen. Send an Idempotency-Key header (any unique string, a UUID is ideal) with every POST; if you retry with the same key within 24 hours you get the original response back, marked Idempotent-Replayed: true, instead of a duplicate link or conversion. The SDKs do this for you.