Skip to content

Pagination, errors, rate limits and idempotency

List endpoints return at most 100 items per call and a cursor for the next page:

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"
  }
}
HTTPcodeMeaning
400invalid_json, invalid_query, invalid_cursor, invalid_sort, invalid_expandThe request could not be understood
401unauthenticatedMissing, invalid, revoked or expired key; error.details[0].code says which
403insufficient_scopeYour key lacks the scope in param
403email_verification_requiredVerify the account email to continue
403two_factor_requiredDashboard session needs its second factor
403write_blocked_past_duePayment overdue for 7+ days; reads work, writes are paused until the invoice is paid
403feature_not_in_planThe feature is not on your plan (param names it); details[0].upgrade_to tells you which plan has it
403plan_limit_reachedA 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)
404resource_not_foundUnknown id (or it belongs to another workspace)
409slug_taken, domain_already_exists, state_conflict, resource_in_use, idempotency_in_progressConflict with current state
422validation_failed, destination_invalid, destination_blocked, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, policy_acceptance_requiredRequest is well-formed but not acceptable; details[] lists fields
429rate_limitedSlow down; Retry-After says how long. param is burst, daily_quota or endpoint
5xxinternal_error, upstream_unavailable, service_unavailable, timeoutOur 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

PlanRequests per day (API keys)Burst (per key)
Pro50,00020 per second
Business200,00050 per second
Agency500,00050 per second
EnterpriseCustomCustom

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: 1764979200

Need 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.