Skip to content

Links

Links endpoints of the URLCrop API: 15 operations. Short links with UTM, expiry, password, social preview, safety and destination health.

GET/links

  • Scope links:read

Parameters

NameInTypeDescription
X-Workspace-IdheaderstringWorkspace to act on. Required for session requests on workspace-scoped routes; optional for API keys (must equal the key's workspace, otherwise 403 workspace_mismatch). For a session, a workspace the user is not an active member of (another tenant, a removed membership, a malformed or unknown id) answers 404 resource_not_found, never 403.
cursorquerystringOpaque cursor from pagination.next_cursor.
limitqueryintegerPage size (1–100).
include_totalquerybooleanAdds pagination.total (estimated above 10,000 rows).
sortquerystringSort field; prefix with - for descending. Allowed fields are listed per resource (x-sortable).
expand[]queryarray of stringRelated objects to embed (depth 1). Allowed values are listed per resource (x-expandable).
statusquerystringOne of: active, paused, expired, blocked, archived
domainquerystring
campaign_idquerystringComma-separated cmp_… ids; a link matches when its campaign is any of them.
tag_idsquerystringComma-separated tag_… ids (at most 50); a link matches when it has any of them. Ids of another workspace or of deleted tags match nothing.
has_qrqueryboolean
healthquerystringDestination health filter (GET /links/{id}/health states). healthy includes links still degraded internally; unhealthy is three consecutive failures; unknown is never checked or not monitored.One of: healthy, unhealthy, unknown
qquerystringSearch slug, title and destination.
created_afterquerystring
created_beforequerystring

Request example

GET /linkscURL
curl 'https://api.urlcrop.com/v1/links' \
  -H 'Authorization: Bearer uc_live_…'

Responses

  • Links

    LinkList schema and example
    FieldTypeDescription
    datarequiredarray of Link
    data[].idrequiredstring
    data[].objectrequiredstringValue: link
    data[].workspace_idrequiredstring
    data[].is_testrequiredboolean
    data[].domain_idstring | nullnull for the system domain crop.sh.
    data[].domainrequiredstring
    data[].slugrequiredstring1–64 chars of [A-Za-z0-9_-]; must not start with @.
    data[].short_urlrequiredstringTest-mode links use https://test.crop.sh/<slug>, which never redirects. For the system domain the host follows Workspace.short_link_host: while staff run the backup short domain (ops switch short_host_fallback) new responses show the backup host (for example https://go.crop.sh/diwali). Both hosts redirect the same slug.Format: uri
    data[].destination_urlrequiredstringAbsolute http(s) URL.Format: uri · max length 2048
    data[].titlestring | nullmax length 200
    data[].descriptionstring | nullmax length 1000
    data[].statusrequiredstringblocked is set by safety; expired by expires_at/click_limit; archived by delete.One of: active, paused, expired, blocked, archived
    data[].utmUtm | null
    data[].utm.sourcestringmax length 200
    data[].utm.mediumstringmax length 200
    data[].utm.campaignstringmax length 200
    data[].utm.termstringmax length 200
    data[].utm.contentstringmax length 200
    data[].utm_template_idstring | null
    data[].password_protectedrequiredbooleanThe password itself is write-only.
    data[].expires_atstring | nullFormat: date-time
    data[].expired_redirect_urlstring | nullFormat: uri
    data[].click_limitinteger | null≥ 1
    data[].ogOpenGraph | null
    data[].og.titlestringmax length 120
    data[].og.descriptionstringmax length 300
    data[].og.image_urlstring | null1200×630 recommended. Upload via POST /assets with purpose: og_image and use the returned url.Format: uri
    data[].append_click_idboolean | nullnull inherits the workspace setting.
    data[].campaign_idstring | null
    data[].tag_idsrequiredarray of string
    data[].pixel_idsarray of stringPhase 2 (Pro+).
    data[].qr_code_idstring | null
    data[].landing_kindstring | nullSet on the link of a dynamic UPI (upi_confirm) or vCard (vcard) QR code: scans open URLCrop's payee confirmation or contact page, so the destination is not a web URL, is never scanned (safety.scanned_at stays null) and is changed on the QR code (PATCH /v1/links/{id} with destination_url → 422 validation_failed, details[].code: qr_landing_link). Null for every other link.One of: upi_confirm, vcard, null
    data[].routing_rulesarray of RoutingRulePhase 2; only present with expand[]=routing_rules.
    data[].routing_rules[].idstring
    data[].routing_rules[].typerequiredstringOne of: geo, device, os, language, time, ab
    data[].routing_rules[].priorityinteger≥ 0
    data[].routing_rules[].conditionsobject
    data[].routing_rules[].destination_urlrequiredstringAbsolute http(s) URL.Format: uri · max length 2048
    data[].routing_rules[].weightintegerab only.≥ 1, ≤ 100
    data[].routing_rules[].enabledboolean
    data[].safetyrequiredobject
    data[].safety.statusrequiredstringpending and flagged show the interstitial; blocked never redirects.One of: safe, pending, flagged, blocked
    data[].safety.risk_scoreinteger | null≥ 0, ≤ 100
    data[].safety.scanned_atstring | nullFormat: date-time
    data[].healthrequiredobject
    data[].health.statusrequiredstringOne of: healthy, unhealthy, unknown
    data[].health.http_statusinteger | null
    data[].health.last_checked_atstring | nullFormat: date-time
    data[].clicksrequiredintegerLifetime clicks from rollups (refreshed within 5 s).
    data[].clicks_7dintegerList responses only. Clicks (clicks and QR scans, like clicks) in the last 7 UTC days including today, from rollup_daily; today is partial.
    data[].clicks_prev_7dintegerList responses only. Clicks in the 7 UTC days before clicks_7d's window, for the change indicator.
    data[].scansrequiredinteger
    data[].last_event_atstring | nullFormat: date-time
    data[].metadataMetadataUp to 20 keys (≤ 40 chars) with string values (≤ 500 chars). Stored verbatim.max keys 20
    data[].created_bystring | nullMember id, or API key id when created by a key.
    data[].created_atrequiredstringISO 8601 UTC with milliseconds.Format: date-time
    data[].updated_atrequiredstringISO 8601 UTC with milliseconds.Format: date-time
    data[].archived_atstring | nullFormat: date-time
    paginationrequiredPagination
    pagination.limitrequiredinteger
    pagination.has_morerequiredboolean
    pagination.next_cursorrequiredstring | nullPass as cursor to fetch the next page. null on the last page.
    pagination.totalintegerOnly with include_total=true; estimated above 10,000.
    200 response: 200 responseJSON
    {
      "data": [
        {
          "id": "lnk_01JB4N8ZK3Q9WXYZ0123456789",
          "object": "link",
          "workspace_id": "ws_01JB4M0A1B2C3D4E5F6G7H8J9K",
          "is_test": false,
          "domain_id": "string",
          "domain": "crop.sh",
          "slug": "diwali",
          "short_url": "https://crop.sh/diwali",
          "destination_url": "https://example.com",
          "title": "string",
          "description": "string",
          "status": "active",
          "utm": {
            "source": "string",
            "medium": "string",
            "campaign": "string",
            "term": "string",
            "content": "string"
          },
          "utm_template_id": "string",
          "password_protected": false,
          "expires_at": "2026-12-05T10:15:30.000Z",
          "expired_redirect_url": "https://example.com",
          "click_limit": 1,
          "og": {
            "title": "string",
            "description": "string",
            "image_url": "https://example.com"
          },
          "append_click_id": false,
          "campaign_id": "string",
          "tag_ids": [
            "string"
          ],
          "pixel_ids": [
            "string"
          ],
          "qr_code_id": "string",
          "landing_kind": "upi_confirm",
          "routing_rules": [
            {
              "id": "rr_01JB4N9A0B1C2D3E4F5G6H7J8K",
              "type": "geo",
              "priority": 0,
              "conditions": {
                "countries": null,
                "devices": null,
                "os": null,
                "languages": null,
                "time": null
              },
              "destination_url": "https://example.com",
              "weight": 1,
              "enabled": true
            }
          ],
          "safety": {
            "status": "safe",
            "risk_score": 0,
            "scanned_at": "2026-12-05T10:15:30.000Z"
          },
          "health": {
            "status": "healthy",
            "http_status": 0,
            "last_checked_at": "2026-12-05T10:15:30.000Z"
          },
          "clicks": 0,
          "clicks_7d": 0,
          "clicks_prev_7d": 0,
          "scans": 0,
          "last_event_at": "2026-12-05T10:15:30.000Z",
          "metadata": {},
          "created_by": "string",
          "created_at": "2026-12-05T10:15:30.000Z",
          "updated_at": "2026-12-05T10:15:30.000Z",
          "archived_at": "2026-12-05T10:15:30.000Z"
        }
      ],
      "pagination": {
        "limit": 0,
        "has_more": false,
        "next_cursor": "string",
        "total": 0
      }
    }
    Example generated from the schema.
  • Malformed request (invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand). Any credentialed operation can return it: a credential in the query string (api_key, access_token) is invalid_query, and a session request without X-Workspace-Id on a workspace-scoped route is missing_header.

    Error codes: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    400 response: missing headerJSON
    {
      "error": {
        "type": "invalid_request_error",
        "code": "missing_header",
        "message": "The X-Workspace-Id header is required for session requests.",
        "param": "X-Workspace-Id",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#missing_header"
      }
    }
  • Missing or invalid credentials. Always code: unauthenticated; details[0].code gives the reason (missing_credentials, invalid_api_key, api_key_revoked, api_key_expired, session_expired, publishable_key_not_allowed).

    Error codes: unauthenticated

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    401 response: unauthenticatedJSON
    {
      "error": {
        "type": "authentication_error",
        "code": "unauthenticated",
        "message": "The API key is not valid.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#unauthenticated",
        "details": [
          {
            "path": "Authorization",
            "code": "invalid_api_key",
            "message": "No key matches this secret"
          }
        ]
      }
    }
  • Permitted credential but not allowed: insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, two_factor_required (also Owner/Admin writes on Business+ after the 14-day 2FA grace, details[0].grace_ends_at), step_up_required, email_verification_required, signups_paused (sign-up while the platform setting is on), write_blocked_past_due, workspace_suspended, content_policy_hold, origin_not_allowed, or a plan limit (type: plan_limit_error — feature_not_in_plan, plan_limit_reached with param naming the plan limit key). Any credentialed operation can return it: an API key on a session-only route is insufficient_scope, a session write without an allowed Origin is origin_not_allowed, and an API key whose plan has no API access is feature_not_in_plan.

    Error codes: two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, feature_not_in_plan, plan_limit_reached

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    403 response: plan limit reachedJSON
    {
      "error": {
        "type": "plan_limit_error",
        "code": "plan_limit_reached",
        "message": "You have created 2,000 of 2,000 links this billing period. Upgrade or buy a pack to continue.",
        "param": "links_per_month",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/pricing",
        "details": [
          {
            "code": "plan_limit_reached",
            "message": "Monthly link quota reached",
            "limit": 2000,
            "used": 2000,
            "resets_at": "2026-12-01T00:00:00.000Z",
            "upgrade_to": "business"
          }
        ]
      }
    }
    403 response: feature not in planJSON
    {
      "error": {
        "type": "plan_limit_error",
        "code": "feature_not_in_plan",
        "message": "The public API is available on the Pro plan and above.",
        "param": "api_requests_per_day",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/pricing",
        "details": [
          {
            "code": "feature_not_in_plan",
            "message": "Upgrade to Pro",
            "upgrade_to": "pro"
          }
        ]
      }
    }
    403 response: email verification requiredJSON
    {
      "error": {
        "type": "permission_error",
        "code": "email_verification_required",
        "message": "Verify your email address to create links.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#email_verification_required"
      }
    }
    403 response: step up requiredJSON
    {
      "error": {
        "type": "permission_error",
        "code": "step_up_required",
        "message": "Confirm your password or 2FA code to continue.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#step_up_required"
      }
    }
  • Unknown id, or the resource belongs to another workspace (never distinguished). Also returned when a session's X-Workspace-Id names a workspace the user is not an active member of.

    Error codes: resource_not_found

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    404 response: not foundJSON
    {
      "error": {
        "type": "not_found_error",
        "code": "resource_not_found",
        "message": "No link with id lnk_01JB4N8ZK3Q9WXYZ0123456789.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#resource_not_found"
      }
    }
  • Validation failure (validation_failed, destination_invalid, destination_blocked (details[0].code is policy_violation, with the category, for content-policy blocks), slug_invalid, slug_reserved, slug_not_allowed, handle_not_allowed, title_not_allowed, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, policy_acceptance_required, …).

    Error codes: validation_failed, destination_invalid, destination_blocked, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, policy_acceptance_required

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    422 response: validation failedJSON
    {
      "error": {
        "type": "validation_error",
        "code": "validation_failed",
        "message": "The request body is invalid.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#validation_failed",
        "details": [
          {
            "path": "destination_url",
            "code": "invalid_string",
            "message": "Must be an absolute http(s) URL"
          }
        ]
      }
    }
    422 response: destination blockedJSON
    {
      "error": {
        "type": "validation_error",
        "code": "destination_blocked",
        "message": "This destination was flagged as unsafe and cannot be shortened.",
        "param": "destination_url",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#destination_blocked"
      }
    }
  • Burst limit, daily plan quota or endpoint limit exceeded.

    Error codes: rate_limited

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    429 response: rate limitedJSON
    {
      "error": {
        "type": "rate_limit_error",
        "code": "rate_limited",
        "message": "Daily API quota of 50,000 requests exhausted. Resets at 00:00 UTC.",
        "param": "daily_quota",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/rate-limits"
      }
    }

POST/links

  • Scope links:write

The destination runs the synchronous safety fast path (≤ 800 ms, no external calls). A denylist entry, feed hit or fresh malicious verdict fails with 422 destination_blocked; otherwise the link is created with safety.status: pending (or safe from a fresh verdict) and deep-scanned asynchronously, and visitors of untrusted workspaces see the interstitial until the scan completes. Accounts with an unverified email cannot create links (403 email_verification_required). Monthly quota exhausted → 403 plan_limit_reached (param: links_per_month). A custom slug needs custom_slugs (Starter+, else 403 feature_not_in_plan); without one a random 7-character code is generated on every plan. A custom slug that is taken, or held after another workspace released it on the shared domain, answers 409 slug_taken (details[0].code: held; available_after only for a 30-day hold, absent for a never-reused generated code).

Parameters

NameInTypeDescription
X-Workspace-IdheaderstringWorkspace to act on. Required for session requests on workspace-scoped routes; optional for API keys (must equal the key's workspace, otherwise 403 workspace_mismatch). For a session, a workspace the user is not an active member of (another tenant, a removed membership, a malformed or unknown id) answers 404 resource_not_found, never 403.
Idempotency-KeyheaderstringUnique string (UUID v4 recommended, ≤ 255 chars). Repeating a POST with the same key and body within 24 hours replays the original response with Idempotent-Replayed: true. One-time secrets are never cached: a replay returns them as null with Idempotent-Redacted: secret.

Request body application/json

FieldTypeDescription
destination_urlrequiredstringAbsolute http(s) URL.Format: uri · max length 2048
domainstringHostname of an active custom domain, or crop.sh (default is the workspace default domain).
slugstring1–64 chars of [A-Za-z0-9_-]; must not start with @.
titlestringmax length 200
descriptionstringmax length 1000
utmUtmUTM parameters appended to the destination. Values may contain {{slug}}, {{domain}}, {{campaign}}.
utm.sourcestringmax length 200
utm.mediumstringmax length 200
utm.campaignstringmax length 200
utm.termstringmax length 200
utm.contentstringmax length 200
utm_template_idstringStarter+. Applied before utm; explicit utm keys win.
passwordstringStarter+.min length 4, max length 64
expires_atstringISO 8601 UTC with milliseconds.Format: date-time
expired_redirect_urlstringAbsolute http(s) URL.Format: uri · max length 2048
click_limitinteger≥ 1
ogOpenGraphCustom social preview shown by WhatsApp, Slack, X, LinkedIn.
og.titlestringmax length 120
og.descriptionstringmax length 300
og.image_urlstring | null1200×630 recommended. Upload via POST /assets with purpose: og_image and use the returned url.Format: uri
append_click_idboolean | null
campaign_idstring
tag_idsarray of stringmax items 20
pixel_idsarray of stringPhase 2.
metadataMetadataUp to 20 keys (≤ 40 chars) with string values (≤ 500 chars). Stored verbatim.max keys 20
require_reachablebooleanFail with 422 destination_unreachable if a HEAD/GET pre-check does not return 2xx/3xx.
create_qr_codebooleanAlso create a dynamic url QR code with the default design for this link in the same transaction. Needs qr:write too and counts toward dynamic_qr_total (403 plan_limit_reached, nothing is created). Not allowed when the create is held for approval (422 validation_failed, param: create_qr_code).
Request body: Just a destinationJSON
{
  "destination_url": "https://brand.in/diwali-sale"
}
Request body: Custom domain, slug, UTM, expiry and previewJSON
{
  "destination_url": "https://brand.in/diwali-sale",
  "domain": "go.brand.in",
  "slug": "diwali",
  "title": "Diwali Sale 2026",
  "utm": {
    "source": "whatsapp",
    "medium": "broadcast",
    "campaign": "diwali-2026"
  },
  "expires_at": "2026-11-15T18:29:59.000Z",
  "expired_redirect_url": "https://brand.in",
  "og": {
    "title": "Up to 40% off this Diwali",
    "description": "Festive offers on the whole range.",
    "image_url": "https://api.urlcrop.com/v1/files/fil_01JB4PS7T8U9V0W1X2Y3Z4A5B6"
  },
  "campaign_id": "cmp_01JB4N0DIWALI2026XXXXXXXXX",
  "tag_ids": [
    "tag_01JB4PD4E5F6G7H8J9K0L1M2N3"
  ],
  "create_qr_code": true
}

Request example

POST /linkscURL
curl -X POST 'https://api.urlcrop.com/v1/links' \
  -H 'Authorization: Bearer uc_live_…' \
  -H 'Content-Type: application/json' \
  -d '{
  "destination_url": "https://brand.in/diwali-sale"
}'

Responses

  • Link created

    Link schema and example
    FieldTypeDescription
    idrequiredstring
    objectrequiredstringValue: link
    workspace_idrequiredstring
    is_testrequiredboolean
    domain_idstring | nullnull for the system domain crop.sh.
    domainrequiredstring
    slugrequiredstring1–64 chars of [A-Za-z0-9_-]; must not start with @.
    short_urlrequiredstringTest-mode links use https://test.crop.sh/<slug>, which never redirects. For the system domain the host follows Workspace.short_link_host: while staff run the backup short domain (ops switch short_host_fallback) new responses show the backup host (for example https://go.crop.sh/diwali). Both hosts redirect the same slug.Format: uri
    destination_urlrequiredstringAbsolute http(s) URL.Format: uri · max length 2048
    titlestring | nullmax length 200
    descriptionstring | nullmax length 1000
    statusrequiredstringblocked is set by safety; expired by expires_at/click_limit; archived by delete.One of: active, paused, expired, blocked, archived
    utmUtm | null
    utm.sourcestringmax length 200
    utm.mediumstringmax length 200
    utm.campaignstringmax length 200
    utm.termstringmax length 200
    utm.contentstringmax length 200
    utm_template_idstring | null
    password_protectedrequiredbooleanThe password itself is write-only.
    expires_atstring | nullFormat: date-time
    expired_redirect_urlstring | nullFormat: uri
    click_limitinteger | null≥ 1
    ogOpenGraph | null
    og.titlestringmax length 120
    og.descriptionstringmax length 300
    og.image_urlstring | null1200×630 recommended. Upload via POST /assets with purpose: og_image and use the returned url.Format: uri
    append_click_idboolean | nullnull inherits the workspace setting.
    campaign_idstring | null
    tag_idsrequiredarray of string
    pixel_idsarray of stringPhase 2 (Pro+).
    qr_code_idstring | null
    landing_kindstring | nullSet on the link of a dynamic UPI (upi_confirm) or vCard (vcard) QR code: scans open URLCrop's payee confirmation or contact page, so the destination is not a web URL, is never scanned (safety.scanned_at stays null) and is changed on the QR code (PATCH /v1/links/{id} with destination_url → 422 validation_failed, details[].code: qr_landing_link). Null for every other link.One of: upi_confirm, vcard, null
    routing_rulesarray of RoutingRulePhase 2; only present with expand[]=routing_rules.
    routing_rules[].idstring
    routing_rules[].typerequiredstringOne of: geo, device, os, language, time, ab
    routing_rules[].priorityinteger≥ 0
    routing_rules[].conditionsobject
    routing_rules[].conditions.countriesarray of string
    routing_rules[].conditions.devicesarray of string
    routing_rules[].conditions.osarray of string
    routing_rules[].conditions.languagesarray of string
    routing_rules[].conditions.timeobject
    routing_rules[].destination_urlrequiredstringAbsolute http(s) URL.Format: uri · max length 2048
    routing_rules[].weightintegerab only.≥ 1, ≤ 100
    routing_rules[].enabledboolean
    safetyrequiredobject
    safety.statusrequiredstringpending and flagged show the interstitial; blocked never redirects.One of: safe, pending, flagged, blocked
    safety.risk_scoreinteger | null≥ 0, ≤ 100
    safety.scanned_atstring | nullFormat: date-time
    healthrequiredobject
    health.statusrequiredstringOne of: healthy, unhealthy, unknown
    health.http_statusinteger | null
    health.last_checked_atstring | nullFormat: date-time
    clicksrequiredintegerLifetime clicks from rollups (refreshed within 5 s).
    clicks_7dintegerList responses only. Clicks (clicks and QR scans, like clicks) in the last 7 UTC days including today, from rollup_daily; today is partial.
    clicks_prev_7dintegerList responses only. Clicks in the 7 UTC days before clicks_7d's window, for the change indicator.
    scansrequiredinteger
    last_event_atstring | nullFormat: date-time
    metadataMetadataUp to 20 keys (≤ 40 chars) with string values (≤ 500 chars). Stored verbatim.max keys 20
    created_bystring | nullMember id, or API key id when created by a key.
    created_atrequiredstringISO 8601 UTC with milliseconds.Format: date-time
    updated_atrequiredstringISO 8601 UTC with milliseconds.Format: date-time
    archived_atstring | nullFormat: date-time
    201 response: createdJSON
    {
      "id": "lnk_01JB4N8ZK3Q9WXYZ0123456789",
      "object": "link",
      "workspace_id": "ws_01JB4M0A1B2C3D4E5F6G7H8J9K",
      "is_test": false,
      "domain_id": "dom_01JB4R5S6T7U8V9W0X1Y2Z3A4B",
      "domain": "go.brand.in",
      "slug": "diwali",
      "short_url": "https://go.brand.in/diwali",
      "destination_url": "https://brand.in/diwali-sale",
      "title": "Diwali Sale 2026",
      "description": null,
      "status": "active",
      "utm": {
        "source": "whatsapp",
        "medium": "broadcast",
        "campaign": "diwali-2026"
      },
      "utm_template_id": null,
      "password_protected": false,
      "expires_at": "2026-11-15T18:29:59.000Z",
      "expired_redirect_url": "https://brand.in",
      "click_limit": null,
      "og": {
        "title": "Up to 40% off this Diwali",
        "description": "Festive offers on the whole range.",
        "image_url": "https://api.urlcrop.com/v1/files/fil_01JB4PS7T8U9V0W1X2Y3Z4A5B6"
      },
      "append_click_id": null,
      "campaign_id": "cmp_01JB4N0DIWALI2026XXXXXXXXX",
      "tag_ids": [
        "tag_01JB4PD4E5F6G7H8J9K0L1M2N3"
      ],
      "pixel_ids": [],
      "qr_code_id": "qr_01JB4P0Q1R2S3T4U5V6W7X8Y9Z",
      "safety": {
        "status": "safe",
        "risk_score": 2,
        "scanned_at": "2026-12-05T10:15:30.000Z"
      },
      "health": {
        "status": "unknown",
        "http_status": null,
        "last_checked_at": null
      },
      "clicks": 0,
      "scans": 0,
      "last_event_at": null,
      "metadata": {},
      "created_by": "key_01JB4M4E5F6G7H8J9K0L1M2N3P",
      "created_at": "2026-12-05T10:15:30.000Z",
      "updated_at": "2026-12-05T10:15:30.000Z",
      "archived_at": null
    }
  • Waiting for approval: the workspace has approvals on and the caller's role needs review (Editor). Nothing is published until an Owner or Admin approves.

    ApprovalRequest schema and example
    FieldTypeDescription
    idrequiredstring
    objectrequiredstringValue: approval_request
    resource_typerequiredstringLinks at this phase; qr_code, page, campaign follow with those resources.Value: link
    resource_idrequiredstring | nullThe link (lnk_…) changed or archived; for create, null until applied, then the new link.
    actionrequiredstringOne of: create, update, delete
    payloadrequiredobjectThe proposed body (LinkCreate for create, LinkUpdate for update, {} for delete), with password replaced by password_set: true.
    statusrequiredstringpending until reviewed; applied once approved and written; approved only while the change is being applied; expired 14 days after creation without a decision.One of: pending, approved, rejected, cancelled, applied, expired
    requested_byrequiredApprovalActor
    requested_by.namerequiredstring | null
    requested_by.emailrequiredstring | nullNull when the member has left the workspace.
    reviewed_byrequiredApprovalActor | null
    reviewed_by.namerequiredstring | null
    reviewed_by.emailrequiredstring | nullNull when the member has left the workspace.
    reviewed_atrequiredstring | nullFormat: date-time
    review_noterequiredstring | nullReviewer comment; the reason when rejected.
    applied_atrequiredstring | nullFormat: date-time
    expires_atrequiredstringISO 8601 UTC with milliseconds.Format: date-time
    is_minerequiredbooleanThe signed-in user made the request.
    targetrequiredobject | nullCurrent short URL and destination of the link an update or delete targets (null for create).
    target.short_urlrequiredstring
    target.destination_urlrequiredstring
    target.statusrequiredstring
    created_atrequiredstringISO 8601 UTC with milliseconds.Format: date-time
    updated_atrequiredstringISO 8601 UTC with milliseconds.Format: date-time
    202 response: 202 responseJSON
    {
      "id": "string",
      "object": "approval_request",
      "resource_type": "link",
      "resource_id": "string",
      "action": "create",
      "payload": {},
      "status": "pending",
      "requested_by": {
        "name": "string",
        "email": "string"
      },
      "reviewed_by": {
        "name": "string",
        "email": "string"
      },
      "reviewed_at": "2026-12-05T10:15:30.000Z",
      "review_note": "string",
      "applied_at": "2026-12-05T10:15:30.000Z",
      "expires_at": "2026-12-05T10:15:30.000Z",
      "is_mine": false,
      "target": {
        "short_url": "string",
        "destination_url": "string",
        "status": "string"
      },
      "created_at": "2026-12-05T10:15:30.000Z",
      "updated_at": "2026-12-05T10:15:30.000Z"
    }
    Example generated from the schema.
  • Malformed request (invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand). Any credentialed operation can return it: a credential in the query string (api_key, access_token) is invalid_query, and a session request without X-Workspace-Id on a workspace-scoped route is missing_header.

    Error codes: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    400 response: missing headerJSON
    {
      "error": {
        "type": "invalid_request_error",
        "code": "missing_header",
        "message": "The X-Workspace-Id header is required for session requests.",
        "param": "X-Workspace-Id",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#missing_header"
      }
    }
  • Missing or invalid credentials. Always code: unauthenticated; details[0].code gives the reason (missing_credentials, invalid_api_key, api_key_revoked, api_key_expired, session_expired, publishable_key_not_allowed).

    Error codes: unauthenticated

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    401 response: unauthenticatedJSON
    {
      "error": {
        "type": "authentication_error",
        "code": "unauthenticated",
        "message": "The API key is not valid.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#unauthenticated",
        "details": [
          {
            "path": "Authorization",
            "code": "invalid_api_key",
            "message": "No key matches this secret"
          }
        ]
      }
    }
  • Permitted credential but not allowed: insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, two_factor_required (also Owner/Admin writes on Business+ after the 14-day 2FA grace, details[0].grace_ends_at), step_up_required, email_verification_required, signups_paused (sign-up while the platform setting is on), write_blocked_past_due, workspace_suspended, content_policy_hold, origin_not_allowed, or a plan limit (type: plan_limit_error — feature_not_in_plan, plan_limit_reached with param naming the plan limit key). Any credentialed operation can return it: an API key on a session-only route is insufficient_scope, a session write without an allowed Origin is origin_not_allowed, and an API key whose plan has no API access is feature_not_in_plan.

    Error codes: two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, feature_not_in_plan, plan_limit_reached

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    403 response: plan limit reachedJSON
    {
      "error": {
        "type": "plan_limit_error",
        "code": "plan_limit_reached",
        "message": "You have created 2,000 of 2,000 links this billing period. Upgrade or buy a pack to continue.",
        "param": "links_per_month",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/pricing",
        "details": [
          {
            "code": "plan_limit_reached",
            "message": "Monthly link quota reached",
            "limit": 2000,
            "used": 2000,
            "resets_at": "2026-12-01T00:00:00.000Z",
            "upgrade_to": "business"
          }
        ]
      }
    }
    403 response: feature not in planJSON
    {
      "error": {
        "type": "plan_limit_error",
        "code": "feature_not_in_plan",
        "message": "The public API is available on the Pro plan and above.",
        "param": "api_requests_per_day",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/pricing",
        "details": [
          {
            "code": "feature_not_in_plan",
            "message": "Upgrade to Pro",
            "upgrade_to": "pro"
          }
        ]
      }
    }
    403 response: email verification requiredJSON
    {
      "error": {
        "type": "permission_error",
        "code": "email_verification_required",
        "message": "Verify your email address to create links.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#email_verification_required"
      }
    }
    403 response: step up requiredJSON
    {
      "error": {
        "type": "permission_error",
        "code": "step_up_required",
        "message": "Confirm your password or 2FA code to continue.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#step_up_required"
      }
    }
  • Unknown id, or the resource belongs to another workspace (never distinguished). Also returned when a session's X-Workspace-Id names a workspace the user is not an active member of.

    Error codes: resource_not_found

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    404 response: not foundJSON
    {
      "error": {
        "type": "not_found_error",
        "code": "resource_not_found",
        "message": "No link with id lnk_01JB4N8ZK3Q9WXYZ0123456789.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#resource_not_found"
      }
    }
  • State conflict (slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use).

    Error codes: slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    409 response: slug takenJSON
    {
      "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://urlcrop.com/docs/api/errors#slug_taken"
      }
    }
  • Validation failure (validation_failed, destination_invalid, destination_blocked (details[0].code is policy_violation, with the category, for content-policy blocks), slug_invalid, slug_reserved, slug_not_allowed, handle_not_allowed, title_not_allowed, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, policy_acceptance_required, …).

    Error codes: validation_failed, destination_invalid, destination_blocked, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, policy_acceptance_required

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    422 response: validation failedJSON
    {
      "error": {
        "type": "validation_error",
        "code": "validation_failed",
        "message": "The request body is invalid.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#validation_failed",
        "details": [
          {
            "path": "destination_url",
            "code": "invalid_string",
            "message": "Must be an absolute http(s) URL"
          }
        ]
      }
    }
    422 response: destination blockedJSON
    {
      "error": {
        "type": "validation_error",
        "code": "destination_blocked",
        "message": "This destination was flagged as unsafe and cannot be shortened.",
        "param": "destination_url",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#destination_blocked"
      }
    }
  • Burst limit, daily plan quota or endpoint limit exceeded.

    Error codes: rate_limited

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    429 response: rate limitedJSON
    {
      "error": {
        "type": "rate_limit_error",
        "code": "rate_limited",
        "message": "Daily API quota of 50,000 requests exhausted. Resets at 00:00 UTC.",
        "param": "daily_quota",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/rate-limits"
      }
    }
  • Unexpected server error (internal_error, upstream_unavailable, service_unavailable, timeout).

    Error codes: invalid_json, internal_error, upstream_unavailable, service_unavailable, timeout

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    500 response: 500 responseJSON
    {
      "error": {
        "type": "invalid_request_error",
        "code": "invalid_json",
        "message": "string",
        "param": "string",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://example.com",
        "details": [
          {
            "path": "string",
            "code": "string",
            "message": "string",
            "limit": 0,
            "used": 0,
            "resets_at": "2026-12-05T10:15:30.000Z",
            "upgrade_to": "string"
          }
        ]
      }
    }
    Example generated from the schema.

POST/links/bulk

  • Scope links:write
  • pro plan and above

Pro and above (bulk_csv; below Pro 403 feature_not_in_plan). The body is validated as a whole (a malformed row fails the request with 422 validation_failed, param = links.<index>.<field>); the rows are then created by a background job and the call answers 202 with the job_ id. Each row goes through the same path as POST /v1/links: safety fast path (blocked destinations fail that row with destination_blocked) and async deep scan, the links_per_month quota (rows past the quota fail individually with plan_limit_reached; never billed as overage), custom_slugs for a slug, and the creation velocity of the workspace's trust tier (the job pauses with a notice and resumes). Progress and per-row errors: GET /v1/jobs/{id}; every row's result as CSV: GET /v1/jobs/{id}/results. Body ≤ 10 MB. Send an Idempotency-Key: a retried request returns the same job.

Parameters

NameInTypeDescription
X-Workspace-IdheaderstringWorkspace to act on. Required for session requests on workspace-scoped routes; optional for API keys (must equal the key's workspace, otherwise 403 workspace_mismatch). For a session, a workspace the user is not an active member of (another tenant, a removed membership, a malformed or unknown id) answers 404 resource_not_found, never 403.
Idempotency-KeyheaderstringUnique string (UUID v4 recommended, ≤ 255 chars). Repeating a POST with the same key and body within 24 hours replays the original response with Idempotent-Replayed: true. One-time secrets are never cached: a replay returns them as null with Idempotent-Redacted: secret.

Request body application/json

FieldTypeDescription
linksrequiredarray of LinkCreatemin items 1, max items 1000
links[].destination_urlrequiredstringAbsolute http(s) URL.Format: uri · max length 2048
links[].domainstringHostname of an active custom domain, or crop.sh (default is the workspace default domain).
links[].slugstring1–64 chars of [A-Za-z0-9_-]; must not start with @.
links[].titlestringmax length 200
links[].descriptionstringmax length 1000
links[].utmUtmUTM parameters appended to the destination. Values may contain {{slug}}, {{domain}}, {{campaign}}.
links[].utm.sourcestringmax length 200
links[].utm.mediumstringmax length 200
links[].utm.campaignstringmax length 200
links[].utm.termstringmax length 200
links[].utm.contentstringmax length 200
links[].utm_template_idstringStarter+. Applied before utm; explicit utm keys win.
links[].passwordstringStarter+.min length 4, max length 64
links[].expires_atstringISO 8601 UTC with milliseconds.Format: date-time
links[].expired_redirect_urlstringAbsolute http(s) URL.Format: uri · max length 2048
links[].click_limitinteger≥ 1
links[].ogOpenGraphCustom social preview shown by WhatsApp, Slack, X, LinkedIn.
links[].og.titlestringmax length 120
links[].og.descriptionstringmax length 300
links[].og.image_urlstring | null1200×630 recommended. Upload via POST /assets with purpose: og_image and use the returned url.Format: uri
links[].append_click_idboolean | null
links[].campaign_idstring
links[].tag_idsarray of stringmax items 20
links[].pixel_idsarray of stringPhase 2.
links[].metadataMetadataUp to 20 keys (≤ 40 chars) with string values (≤ 500 chars). Stored verbatim.max keys 20
links[].require_reachablebooleanFail with 422 destination_unreachable if a HEAD/GET pre-check does not return 2xx/3xx.
links[].create_qr_codebooleanAlso create a dynamic url QR code with the default design for this link in the same transaction. Needs qr:write too and counts toward dynamic_qr_total (403 plan_limit_reached, nothing is created). Not allowed when the create is held for approval (422 validation_failed, param: create_qr_code).
Request body: twoJSON
{
  "links": [
    {
      "destination_url": "https://brand.in/diwali-sale",
      "title": "Diwali Sale 2026"
    },
    {
      "destination_url": "https://brand.in/new-arrivals",
      "slug": "new-arrivals"
    }
  ]
}

Request example

POST /links/bulkcURL
curl -X POST 'https://api.urlcrop.com/v1/links/bulk' \
  -H 'Authorization: Bearer uc_live_…' \
  -H 'Content-Type: application/json' \
  -d '{
  "links": [
    {
      "destination_url": "https://brand.in/diwali-sale",
      "title": "Diwali Sale 2026"
    },
    {
      "destination_url": "https://brand.in/new-arrivals",
      "slug": "new-arrivals"
    }
  ]
}'

Responses

  • Job accepted

    JobAccepted schema and example
    FieldTypeDescription
    job_idrequiredstring
    statusrequiredstringOne of: queued, processing, paused, completed, failed
    totalrequiredinteger
    202 response: acceptedJSON
    {
      "job_id": "job_01JB4PR6S7T8U9V0W1X2Y3Z4A5",
      "status": "queued",
      "total": 2
    }
  • Malformed request (invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand). Any credentialed operation can return it: a credential in the query string (api_key, access_token) is invalid_query, and a session request without X-Workspace-Id on a workspace-scoped route is missing_header.

    Error codes: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    400 response: missing headerJSON
    {
      "error": {
        "type": "invalid_request_error",
        "code": "missing_header",
        "message": "The X-Workspace-Id header is required for session requests.",
        "param": "X-Workspace-Id",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#missing_header"
      }
    }
  • Missing or invalid credentials. Always code: unauthenticated; details[0].code gives the reason (missing_credentials, invalid_api_key, api_key_revoked, api_key_expired, session_expired, publishable_key_not_allowed).

    Error codes: unauthenticated

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    401 response: unauthenticatedJSON
    {
      "error": {
        "type": "authentication_error",
        "code": "unauthenticated",
        "message": "The API key is not valid.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#unauthenticated",
        "details": [
          {
            "path": "Authorization",
            "code": "invalid_api_key",
            "message": "No key matches this secret"
          }
        ]
      }
    }
  • Permitted credential but not allowed: insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, two_factor_required (also Owner/Admin writes on Business+ after the 14-day 2FA grace, details[0].grace_ends_at), step_up_required, email_verification_required, signups_paused (sign-up while the platform setting is on), write_blocked_past_due, workspace_suspended, content_policy_hold, origin_not_allowed, or a plan limit (type: plan_limit_error — feature_not_in_plan, plan_limit_reached with param naming the plan limit key). Any credentialed operation can return it: an API key on a session-only route is insufficient_scope, a session write without an allowed Origin is origin_not_allowed, and an API key whose plan has no API access is feature_not_in_plan.

    Error codes: two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, feature_not_in_plan, plan_limit_reached

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    403 response: plan limit reachedJSON
    {
      "error": {
        "type": "plan_limit_error",
        "code": "plan_limit_reached",
        "message": "You have created 2,000 of 2,000 links this billing period. Upgrade or buy a pack to continue.",
        "param": "links_per_month",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/pricing",
        "details": [
          {
            "code": "plan_limit_reached",
            "message": "Monthly link quota reached",
            "limit": 2000,
            "used": 2000,
            "resets_at": "2026-12-01T00:00:00.000Z",
            "upgrade_to": "business"
          }
        ]
      }
    }
    403 response: feature not in planJSON
    {
      "error": {
        "type": "plan_limit_error",
        "code": "feature_not_in_plan",
        "message": "The public API is available on the Pro plan and above.",
        "param": "api_requests_per_day",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/pricing",
        "details": [
          {
            "code": "feature_not_in_plan",
            "message": "Upgrade to Pro",
            "upgrade_to": "pro"
          }
        ]
      }
    }
    403 response: email verification requiredJSON
    {
      "error": {
        "type": "permission_error",
        "code": "email_verification_required",
        "message": "Verify your email address to create links.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#email_verification_required"
      }
    }
    403 response: step up requiredJSON
    {
      "error": {
        "type": "permission_error",
        "code": "step_up_required",
        "message": "Confirm your password or 2FA code to continue.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#step_up_required"
      }
    }
  • Unknown id, or the resource belongs to another workspace (never distinguished). Also returned when a session's X-Workspace-Id names a workspace the user is not an active member of.

    Error codes: resource_not_found

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    404 response: not foundJSON
    {
      "error": {
        "type": "not_found_error",
        "code": "resource_not_found",
        "message": "No link with id lnk_01JB4N8ZK3Q9WXYZ0123456789.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#resource_not_found"
      }
    }
  • State conflict (slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use).

    Error codes: slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    409 response: slug takenJSON
    {
      "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://urlcrop.com/docs/api/errors#slug_taken"
      }
    }
  • Body over the route's limit (payload_too_large; 10 MB on bulk create and CSV import).

    Error codes: invalid_json, payload_too_large

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    413 response: 413 responseJSON
    {
      "error": {
        "type": "invalid_request_error",
        "code": "invalid_json",
        "message": "string",
        "param": "string",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://example.com",
        "details": [
          {
            "path": "string",
            "code": "string",
            "message": "string",
            "limit": 0,
            "used": 0,
            "resets_at": "2026-12-05T10:15:30.000Z",
            "upgrade_to": "string"
          }
        ]
      }
    }
    Example generated from the schema.
  • Validation failure (validation_failed, destination_invalid, destination_blocked (details[0].code is policy_violation, with the category, for content-policy blocks), slug_invalid, slug_reserved, slug_not_allowed, handle_not_allowed, title_not_allowed, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, policy_acceptance_required, …).

    Error codes: validation_failed, destination_invalid, destination_blocked, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, policy_acceptance_required

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    422 response: validation failedJSON
    {
      "error": {
        "type": "validation_error",
        "code": "validation_failed",
        "message": "The request body is invalid.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#validation_failed",
        "details": [
          {
            "path": "destination_url",
            "code": "invalid_string",
            "message": "Must be an absolute http(s) URL"
          }
        ]
      }
    }
    422 response: destination blockedJSON
    {
      "error": {
        "type": "validation_error",
        "code": "destination_blocked",
        "message": "This destination was flagged as unsafe and cannot be shortened.",
        "param": "destination_url",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#destination_blocked"
      }
    }
  • Burst limit, daily plan quota or endpoint limit exceeded.

    Error codes: rate_limited

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    429 response: rate limitedJSON
    {
      "error": {
        "type": "rate_limit_error",
        "code": "rate_limited",
        "message": "Daily API quota of 50,000 requests exhausted. Resets at 00:00 UTC.",
        "param": "daily_quota",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/rate-limits"
      }
    }

Import links from a CSV file

POST/links/import

  • Scope links:write
  • pro plan and above

Pro and above (bulk_csv). multipart/form-data with one file part: UTF-8 CSV (optional BOM), comma separated, RFC 4180 quoting, a header row naming the columns destination_url (required), slug, title and description (any order; other columns fail with 422 csv_invalid). At most 10 MB and 10,000 data rows; blank lines are skipped. A file that cannot be read as CSV (bad quoting, missing destination_url column, no data rows, too many rows) fails with 422 csv_invalid and details naming the rows. Rows that parse but are invalid (bad URL, field too long) are recorded as failed rows of the job (validation_failed) and the rest are created exactly as POST /v1/links/bulk does. Answers 202 with the job_ id. Exported results neutralise spreadsheet formulas (cells starting with =, +, -, @, tab or carriage return are prefixed with ').

Parameters

NameInTypeDescription
X-Workspace-IdheaderstringWorkspace to act on. Required for session requests on workspace-scoped routes; optional for API keys (must equal the key's workspace, otherwise 403 workspace_mismatch). For a session, a workspace the user is not an active member of (another tenant, a removed membership, a malformed or unknown id) answers 404 resource_not_found, never 403.
Idempotency-KeyheaderstringUnique string (UUID v4 recommended, ≤ 255 chars). Repeating a POST with the same key and body within 24 hours replays the original response with Idempotent-Replayed: true. One-time secrets are never cached: a replay returns them as null with Idempotent-Redacted: secret.

Request body multipart/form-data

FieldTypeDescription
filerequiredstringThe CSV file (≤ 10 MB, ≤ 10,000 rows).Format: binary
Request body: Request bodyJSON
{
  "file": "string"
}
Example generated from the schema.

Request example

POST /links/importcURL
curl -X POST 'https://api.urlcrop.com/v1/links/import' \
  -H 'Authorization: Bearer uc_live_…' \
  -H 'Content-Type: multipart/form-data' \
  -d '{
  "file": "string"
}'

Responses

  • 202 response

    Job accepted

    JobAccepted schema and example
    FieldTypeDescription
    job_idrequiredstring
    statusrequiredstringOne of: queued, processing, paused, completed, failed
    totalrequiredinteger
    202 response: 202 responseJSON
    {
      "job_id": "job_01JB4PR6S7T8U9V0W1X2Y3Z4A5",
      "status": "queued",
      "total": 0
    }
    Example generated from the schema.
  • 400 response

    Malformed request (invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand). Any credentialed operation can return it: a credential in the query string (api_key, access_token) is invalid_query, and a session request without X-Workspace-Id on a workspace-scoped route is missing_header.

    Error codes: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    400 response: missing headerJSON
    {
      "error": {
        "type": "invalid_request_error",
        "code": "missing_header",
        "message": "The X-Workspace-Id header is required for session requests.",
        "param": "X-Workspace-Id",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#missing_header"
      }
    }
  • 401 response

    Missing or invalid credentials. Always code: unauthenticated; details[0].code gives the reason (missing_credentials, invalid_api_key, api_key_revoked, api_key_expired, session_expired, publishable_key_not_allowed).

    Error codes: unauthenticated

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    401 response: unauthenticatedJSON
    {
      "error": {
        "type": "authentication_error",
        "code": "unauthenticated",
        "message": "The API key is not valid.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#unauthenticated",
        "details": [
          {
            "path": "Authorization",
            "code": "invalid_api_key",
            "message": "No key matches this secret"
          }
        ]
      }
    }
  • 403 response

    Permitted credential but not allowed: insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, two_factor_required (also Owner/Admin writes on Business+ after the 14-day 2FA grace, details[0].grace_ends_at), step_up_required, email_verification_required, signups_paused (sign-up while the platform setting is on), write_blocked_past_due, workspace_suspended, content_policy_hold, origin_not_allowed, or a plan limit (type: plan_limit_error — feature_not_in_plan, plan_limit_reached with param naming the plan limit key). Any credentialed operation can return it: an API key on a session-only route is insufficient_scope, a session write without an allowed Origin is origin_not_allowed, and an API key whose plan has no API access is feature_not_in_plan.

    Error codes: two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, feature_not_in_plan, plan_limit_reached

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    403 response: plan limit reachedJSON
    {
      "error": {
        "type": "plan_limit_error",
        "code": "plan_limit_reached",
        "message": "You have created 2,000 of 2,000 links this billing period. Upgrade or buy a pack to continue.",
        "param": "links_per_month",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/pricing",
        "details": [
          {
            "code": "plan_limit_reached",
            "message": "Monthly link quota reached",
            "limit": 2000,
            "used": 2000,
            "resets_at": "2026-12-01T00:00:00.000Z",
            "upgrade_to": "business"
          }
        ]
      }
    }
    403 response: feature not in planJSON
    {
      "error": {
        "type": "plan_limit_error",
        "code": "feature_not_in_plan",
        "message": "The public API is available on the Pro plan and above.",
        "param": "api_requests_per_day",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/pricing",
        "details": [
          {
            "code": "feature_not_in_plan",
            "message": "Upgrade to Pro",
            "upgrade_to": "pro"
          }
        ]
      }
    }
    403 response: email verification requiredJSON
    {
      "error": {
        "type": "permission_error",
        "code": "email_verification_required",
        "message": "Verify your email address to create links.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#email_verification_required"
      }
    }
    403 response: step up requiredJSON
    {
      "error": {
        "type": "permission_error",
        "code": "step_up_required",
        "message": "Confirm your password or 2FA code to continue.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#step_up_required"
      }
    }
  • 404 response

    Unknown id, or the resource belongs to another workspace (never distinguished). Also returned when a session's X-Workspace-Id names a workspace the user is not an active member of.

    Error codes: resource_not_found

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    404 response: not foundJSON
    {
      "error": {
        "type": "not_found_error",
        "code": "resource_not_found",
        "message": "No link with id lnk_01JB4N8ZK3Q9WXYZ0123456789.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#resource_not_found"
      }
    }
  • 409 response

    State conflict (slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use).

    Error codes: slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    409 response: slug takenJSON
    {
      "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://urlcrop.com/docs/api/errors#slug_taken"
      }
    }
  • 413 response

    Body over the route's limit (payload_too_large; 10 MB on bulk create and CSV import).

    Error codes: invalid_json, payload_too_large

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    413 response: 413 responseJSON
    {
      "error": {
        "type": "invalid_request_error",
        "code": "invalid_json",
        "message": "string",
        "param": "string",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://example.com",
        "details": [
          {
            "path": "string",
            "code": "string",
            "message": "string",
            "limit": 0,
            "used": 0,
            "resets_at": "2026-12-05T10:15:30.000Z",
            "upgrade_to": "string"
          }
        ]
      }
    }
    Example generated from the schema.
  • 415 response

    Wrong Content-Type (unsupported_media_type; CSV import needs multipart/form-data).

    Error codes: invalid_json, unsupported_media_type

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    415 response: 415 responseJSON
    {
      "error": {
        "type": "invalid_request_error",
        "code": "invalid_json",
        "message": "string",
        "param": "string",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://example.com",
        "details": [
          {
            "path": "string",
            "code": "string",
            "message": "string",
            "limit": 0,
            "used": 0,
            "resets_at": "2026-12-05T10:15:30.000Z",
            "upgrade_to": "string"
          }
        ]
      }
    }
    Example generated from the schema.
  • 422 response

    Validation failure (validation_failed, destination_invalid, destination_blocked (details[0].code is policy_violation, with the category, for content-policy blocks), slug_invalid, slug_reserved, slug_not_allowed, handle_not_allowed, title_not_allowed, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, policy_acceptance_required, …).

    Error codes: validation_failed, destination_invalid, destination_blocked, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, policy_acceptance_required

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    422 response: validation failedJSON
    {
      "error": {
        "type": "validation_error",
        "code": "validation_failed",
        "message": "The request body is invalid.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#validation_failed",
        "details": [
          {
            "path": "destination_url",
            "code": "invalid_string",
            "message": "Must be an absolute http(s) URL"
          }
        ]
      }
    }
    422 response: destination blockedJSON
    {
      "error": {
        "type": "validation_error",
        "code": "destination_blocked",
        "message": "This destination was flagged as unsafe and cannot be shortened.",
        "param": "destination_url",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#destination_blocked"
      }
    }
  • 429 response

    Burst limit, daily plan quota or endpoint limit exceeded.

    Error codes: rate_limited

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    429 response: rate limitedJSON
    {
      "error": {
        "type": "rate_limit_error",
        "code": "rate_limited",
        "message": "Daily API quota of 50,000 requests exhausted. Resets at 00:00 UTC.",
        "param": "daily_quota",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/rate-limits"
      }
    }

Check whether a slug is free on a domain

GET/links/availability

  • Scope links:read

reason: taken (exact, or equal under lower() on the shared domain, any workspace, archived links included), held (released on the shared domain by another workspace and still held: a custom slug for 30 days if its link was live 24 hours or got a click, with available_after saying when it frees up; a generated code permanently, with no available_after), reserved, invalid, domain_not_active. An unknown domain or another workspace's domain answers 404. Limited to 60 checks a minute per session user or API key and 120 a minute per workspace (429 rate_limited, param: endpoint, details[0].code: links_availability, with Retry-After).

Parameters

NameInTypeDescription
X-Workspace-IdheaderstringWorkspace to act on. Required for session requests on workspace-scoped routes; optional for API keys (must equal the key's workspace, otherwise 403 workspace_mismatch). For a session, a workspace the user is not an active member of (another tenant, a removed membership, a malformed or unknown id) answers 404 resource_not_found, never 403.
domainrequiredquerystring
slugrequiredquerystring

Request example

GET /links/availabilitycURL
curl 'https://api.urlcrop.com/v1/links/availability?domain=string&slug=string' \
  -H 'Authorization: Bearer uc_live_…'

Responses

  • 200 response

    Availability

    LinkAvailability schema and example
    FieldTypeDescription
    domainrequiredstring
    slugrequiredstring
    availablerequiredboolean
    reasonstring | nullOne of: taken, held, reserved, invalid, domain_not_active, null
    available_afterstring | nullWith reason: held: when the 30-day hold on a custom slug released by another workspace on the shared domain ends (decision 17). Absent (null) for generated codes, which are never reused. Never says who released it.Format: date-time
    200 response: 200 responseJSON
    {
      "domain": "string",
      "slug": "string",
      "available": false,
      "reason": "taken",
      "available_after": "2026-12-05T10:15:30.000Z"
    }
    Example generated from the schema.
  • 400 response

    Malformed request (invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand). Any credentialed operation can return it: a credential in the query string (api_key, access_token) is invalid_query, and a session request without X-Workspace-Id on a workspace-scoped route is missing_header.

    Error codes: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    400 response: missing headerJSON
    {
      "error": {
        "type": "invalid_request_error",
        "code": "missing_header",
        "message": "The X-Workspace-Id header is required for session requests.",
        "param": "X-Workspace-Id",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#missing_header"
      }
    }
  • 401 response

    Missing or invalid credentials. Always code: unauthenticated; details[0].code gives the reason (missing_credentials, invalid_api_key, api_key_revoked, api_key_expired, session_expired, publishable_key_not_allowed).

    Error codes: unauthenticated

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    401 response: unauthenticatedJSON
    {
      "error": {
        "type": "authentication_error",
        "code": "unauthenticated",
        "message": "The API key is not valid.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#unauthenticated",
        "details": [
          {
            "path": "Authorization",
            "code": "invalid_api_key",
            "message": "No key matches this secret"
          }
        ]
      }
    }
  • 403 response

    Permitted credential but not allowed: insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, two_factor_required (also Owner/Admin writes on Business+ after the 14-day 2FA grace, details[0].grace_ends_at), step_up_required, email_verification_required, signups_paused (sign-up while the platform setting is on), write_blocked_past_due, workspace_suspended, content_policy_hold, origin_not_allowed, or a plan limit (type: plan_limit_error — feature_not_in_plan, plan_limit_reached with param naming the plan limit key). Any credentialed operation can return it: an API key on a session-only route is insufficient_scope, a session write without an allowed Origin is origin_not_allowed, and an API key whose plan has no API access is feature_not_in_plan.

    Error codes: two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, feature_not_in_plan, plan_limit_reached

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    403 response: plan limit reachedJSON
    {
      "error": {
        "type": "plan_limit_error",
        "code": "plan_limit_reached",
        "message": "You have created 2,000 of 2,000 links this billing period. Upgrade or buy a pack to continue.",
        "param": "links_per_month",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/pricing",
        "details": [
          {
            "code": "plan_limit_reached",
            "message": "Monthly link quota reached",
            "limit": 2000,
            "used": 2000,
            "resets_at": "2026-12-01T00:00:00.000Z",
            "upgrade_to": "business"
          }
        ]
      }
    }
    403 response: feature not in planJSON
    {
      "error": {
        "type": "plan_limit_error",
        "code": "feature_not_in_plan",
        "message": "The public API is available on the Pro plan and above.",
        "param": "api_requests_per_day",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/pricing",
        "details": [
          {
            "code": "feature_not_in_plan",
            "message": "Upgrade to Pro",
            "upgrade_to": "pro"
          }
        ]
      }
    }
    403 response: email verification requiredJSON
    {
      "error": {
        "type": "permission_error",
        "code": "email_verification_required",
        "message": "Verify your email address to create links.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#email_verification_required"
      }
    }
    403 response: step up requiredJSON
    {
      "error": {
        "type": "permission_error",
        "code": "step_up_required",
        "message": "Confirm your password or 2FA code to continue.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#step_up_required"
      }
    }
  • 404 response

    Unknown id, or the resource belongs to another workspace (never distinguished). Also returned when a session's X-Workspace-Id names a workspace the user is not an active member of.

    Error codes: resource_not_found

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    404 response: not foundJSON
    {
      "error": {
        "type": "not_found_error",
        "code": "resource_not_found",
        "message": "No link with id lnk_01JB4N8ZK3Q9WXYZ0123456789.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#resource_not_found"
      }
    }
  • 429 response

    Burst limit, daily plan quota or endpoint limit exceeded.

    Error codes: rate_limited

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    429 response: rate limitedJSON
    {
      "error": {
        "type": "rate_limit_error",
        "code": "rate_limited",
        "message": "Daily API quota of 50,000 requests exhausted. Resets at 00:00 UTC.",
        "param": "daily_quota",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/rate-limits"
      }
    }

Fetch Open Graph metadata for a URL (nothing stored)

GET/links/metadata

  • Scope links:read

Parameters

NameInTypeDescription
X-Workspace-IdheaderstringWorkspace to act on. Required for session requests on workspace-scoped routes; optional for API keys (must equal the key's workspace, otherwise 403 workspace_mismatch). For a session, a workspace the user is not an active member of (another tenant, a removed membership, a malformed or unknown id) answers 404 resource_not_found, never 403.
urlrequiredquerystring

Request example

GET /links/metadatacURL
curl 'https://api.urlcrop.com/v1/links/metadata?url=https%3A%2F%2Fexample.com' \
  -H 'Authorization: Bearer uc_live_…'

Responses

  • 200 response

    Metadata

    LinkMetadata schema and example
    FieldTypeDescription
    urlrequiredstringFormat: uri
    titlestring | null
    descriptionstring | null
    image_urlstring | nullFormat: uri
    site_namestring | null
    favicon_urlstring | nullFormat: uri
    200 response: 200 responseJSON
    {
      "url": "https://example.com",
      "title": "string",
      "description": "string",
      "image_url": "https://example.com",
      "site_name": "string",
      "favicon_url": "https://example.com"
    }
    Example generated from the schema.
  • 401 response

    Missing or invalid credentials. Always code: unauthenticated; details[0].code gives the reason (missing_credentials, invalid_api_key, api_key_revoked, api_key_expired, session_expired, publishable_key_not_allowed).

    Error codes: unauthenticated

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    401 response: unauthenticatedJSON
    {
      "error": {
        "type": "authentication_error",
        "code": "unauthenticated",
        "message": "The API key is not valid.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#unauthenticated",
        "details": [
          {
            "path": "Authorization",
            "code": "invalid_api_key",
            "message": "No key matches this secret"
          }
        ]
      }
    }
  • 404 response

    Unknown id, or the resource belongs to another workspace (never distinguished). Also returned when a session's X-Workspace-Id names a workspace the user is not an active member of.

    Error codes: resource_not_found

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    404 response: not foundJSON
    {
      "error": {
        "type": "not_found_error",
        "code": "resource_not_found",
        "message": "No link with id lnk_01JB4N8ZK3Q9WXYZ0123456789.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#resource_not_found"
      }
    }
  • 422 response

    Validation failure (validation_failed, destination_invalid, destination_blocked (details[0].code is policy_violation, with the category, for content-policy blocks), slug_invalid, slug_reserved, slug_not_allowed, handle_not_allowed, title_not_allowed, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, policy_acceptance_required, …).

    Error codes: validation_failed, destination_invalid, destination_blocked, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, policy_acceptance_required

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    422 response: validation failedJSON
    {
      "error": {
        "type": "validation_error",
        "code": "validation_failed",
        "message": "The request body is invalid.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#validation_failed",
        "details": [
          {
            "path": "destination_url",
            "code": "invalid_string",
            "message": "Must be an absolute http(s) URL"
          }
        ]
      }
    }
    422 response: destination blockedJSON
    {
      "error": {
        "type": "validation_error",
        "code": "destination_blocked",
        "message": "This destination was flagged as unsafe and cannot be shortened.",
        "param": "destination_url",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#destination_blocked"
      }
    }
  • 429 response

    Burst limit, daily plan quota or endpoint limit exceeded.

    Error codes: rate_limited

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    429 response: rate limitedJSON
    {
      "error": {
        "type": "rate_limit_error",
        "code": "rate_limited",
        "message": "Daily API quota of 50,000 requests exhausted. Resets at 00:00 UTC.",
        "param": "daily_quota",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/rate-limits"
      }
    }

GET/links/{id}

  • Scope links:read

Parameters

NameInTypeDescription
idrequiredpathstringPrefixed ULID (<prefix>_<26 Crockford base32 chars>).
X-Workspace-IdheaderstringWorkspace to act on. Required for session requests on workspace-scoped routes; optional for API keys (must equal the key's workspace, otherwise 403 workspace_mismatch). For a session, a workspace the user is not an active member of (another tenant, a removed membership, a malformed or unknown id) answers 404 resource_not_found, never 403.
expand[]queryarray of stringRelated objects to embed (depth 1). Allowed values are listed per resource (x-expandable).

Request example

GET /links/{id}cURL
curl 'https://api.urlcrop.com/v1/links/lnk_01JB4N8ZK3Q9WXYZ0123456789' \
  -H 'Authorization: Bearer uc_live_…'

Responses

  • Link

    Link schema and example
    FieldTypeDescription
    idrequiredstring
    objectrequiredstringValue: link
    workspace_idrequiredstring
    is_testrequiredboolean
    domain_idstring | nullnull for the system domain crop.sh.
    domainrequiredstring
    slugrequiredstring1–64 chars of [A-Za-z0-9_-]; must not start with @.
    short_urlrequiredstringTest-mode links use https://test.crop.sh/<slug>, which never redirects. For the system domain the host follows Workspace.short_link_host: while staff run the backup short domain (ops switch short_host_fallback) new responses show the backup host (for example https://go.crop.sh/diwali). Both hosts redirect the same slug.Format: uri
    destination_urlrequiredstringAbsolute http(s) URL.Format: uri · max length 2048
    titlestring | nullmax length 200
    descriptionstring | nullmax length 1000
    statusrequiredstringblocked is set by safety; expired by expires_at/click_limit; archived by delete.One of: active, paused, expired, blocked, archived
    utmUtm | null
    utm.sourcestringmax length 200
    utm.mediumstringmax length 200
    utm.campaignstringmax length 200
    utm.termstringmax length 200
    utm.contentstringmax length 200
    utm_template_idstring | null
    password_protectedrequiredbooleanThe password itself is write-only.
    expires_atstring | nullFormat: date-time
    expired_redirect_urlstring | nullFormat: uri
    click_limitinteger | null≥ 1
    ogOpenGraph | null
    og.titlestringmax length 120
    og.descriptionstringmax length 300
    og.image_urlstring | null1200×630 recommended. Upload via POST /assets with purpose: og_image and use the returned url.Format: uri
    append_click_idboolean | nullnull inherits the workspace setting.
    campaign_idstring | null
    tag_idsrequiredarray of string
    pixel_idsarray of stringPhase 2 (Pro+).
    qr_code_idstring | null
    landing_kindstring | nullSet on the link of a dynamic UPI (upi_confirm) or vCard (vcard) QR code: scans open URLCrop's payee confirmation or contact page, so the destination is not a web URL, is never scanned (safety.scanned_at stays null) and is changed on the QR code (PATCH /v1/links/{id} with destination_url → 422 validation_failed, details[].code: qr_landing_link). Null for every other link.One of: upi_confirm, vcard, null
    routing_rulesarray of RoutingRulePhase 2; only present with expand[]=routing_rules.
    routing_rules[].idstring
    routing_rules[].typerequiredstringOne of: geo, device, os, language, time, ab
    routing_rules[].priorityinteger≥ 0
    routing_rules[].conditionsobject
    routing_rules[].conditions.countriesarray of string
    routing_rules[].conditions.devicesarray of string
    routing_rules[].conditions.osarray of string
    routing_rules[].conditions.languagesarray of string
    routing_rules[].conditions.timeobject
    routing_rules[].destination_urlrequiredstringAbsolute http(s) URL.Format: uri · max length 2048
    routing_rules[].weightintegerab only.≥ 1, ≤ 100
    routing_rules[].enabledboolean
    safetyrequiredobject
    safety.statusrequiredstringpending and flagged show the interstitial; blocked never redirects.One of: safe, pending, flagged, blocked
    safety.risk_scoreinteger | null≥ 0, ≤ 100
    safety.scanned_atstring | nullFormat: date-time
    healthrequiredobject
    health.statusrequiredstringOne of: healthy, unhealthy, unknown
    health.http_statusinteger | null
    health.last_checked_atstring | nullFormat: date-time
    clicksrequiredintegerLifetime clicks from rollups (refreshed within 5 s).
    clicks_7dintegerList responses only. Clicks (clicks and QR scans, like clicks) in the last 7 UTC days including today, from rollup_daily; today is partial.
    clicks_prev_7dintegerList responses only. Clicks in the 7 UTC days before clicks_7d's window, for the change indicator.
    scansrequiredinteger
    last_event_atstring | nullFormat: date-time
    metadataMetadataUp to 20 keys (≤ 40 chars) with string values (≤ 500 chars). Stored verbatim.max keys 20
    created_bystring | nullMember id, or API key id when created by a key.
    created_atrequiredstringISO 8601 UTC with milliseconds.Format: date-time
    updated_atrequiredstringISO 8601 UTC with milliseconds.Format: date-time
    archived_atstring | nullFormat: date-time
    200 response: 200 responseJSON
    {
      "id": "lnk_01JB4N8ZK3Q9WXYZ0123456789",
      "object": "link",
      "workspace_id": "ws_01JB4M0A1B2C3D4E5F6G7H8J9K",
      "is_test": false,
      "domain_id": "string",
      "domain": "crop.sh",
      "slug": "diwali",
      "short_url": "https://crop.sh/diwali",
      "destination_url": "https://example.com",
      "title": "string",
      "description": "string",
      "status": "active",
      "utm": {
        "source": "string",
        "medium": "string",
        "campaign": "string",
        "term": "string",
        "content": "string"
      },
      "utm_template_id": "string",
      "password_protected": false,
      "expires_at": "2026-12-05T10:15:30.000Z",
      "expired_redirect_url": "https://example.com",
      "click_limit": 1,
      "og": {
        "title": "string",
        "description": "string",
        "image_url": "https://example.com"
      },
      "append_click_id": false,
      "campaign_id": "string",
      "tag_ids": [
        "string"
      ],
      "pixel_ids": [
        "string"
      ],
      "qr_code_id": "string",
      "landing_kind": "upi_confirm",
      "routing_rules": [
        {
          "id": "rr_01JB4N9A0B1C2D3E4F5G6H7J8K",
          "type": "geo",
          "priority": 0,
          "conditions": {
            "countries": [
              "string"
            ],
            "devices": [
              "mobile"
            ],
            "os": [
              "ios"
            ],
            "languages": [
              "string"
            ],
            "time": {
              "timezone": "string",
              "start": "string",
              "end": "string",
              "days": []
            }
          },
          "destination_url": "https://example.com",
          "weight": 1,
          "enabled": true
        }
      ],
      "safety": {
        "status": "safe",
        "risk_score": 0,
        "scanned_at": "2026-12-05T10:15:30.000Z"
      },
      "health": {
        "status": "healthy",
        "http_status": 0,
        "last_checked_at": "2026-12-05T10:15:30.000Z"
      },
      "clicks": 0,
      "clicks_7d": 0,
      "clicks_prev_7d": 0,
      "scans": 0,
      "last_event_at": "2026-12-05T10:15:30.000Z",
      "metadata": {},
      "created_by": "string",
      "created_at": "2026-12-05T10:15:30.000Z",
      "updated_at": "2026-12-05T10:15:30.000Z",
      "archived_at": "2026-12-05T10:15:30.000Z"
    }
    Example generated from the schema.
  • Malformed request (invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand). Any credentialed operation can return it: a credential in the query string (api_key, access_token) is invalid_query, and a session request without X-Workspace-Id on a workspace-scoped route is missing_header.

    Error codes: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    400 response: missing headerJSON
    {
      "error": {
        "type": "invalid_request_error",
        "code": "missing_header",
        "message": "The X-Workspace-Id header is required for session requests.",
        "param": "X-Workspace-Id",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#missing_header"
      }
    }
  • Missing or invalid credentials. Always code: unauthenticated; details[0].code gives the reason (missing_credentials, invalid_api_key, api_key_revoked, api_key_expired, session_expired, publishable_key_not_allowed).

    Error codes: unauthenticated

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    401 response: unauthenticatedJSON
    {
      "error": {
        "type": "authentication_error",
        "code": "unauthenticated",
        "message": "The API key is not valid.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#unauthenticated",
        "details": [
          {
            "path": "Authorization",
            "code": "invalid_api_key",
            "message": "No key matches this secret"
          }
        ]
      }
    }
  • Permitted credential but not allowed: insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, two_factor_required (also Owner/Admin writes on Business+ after the 14-day 2FA grace, details[0].grace_ends_at), step_up_required, email_verification_required, signups_paused (sign-up while the platform setting is on), write_blocked_past_due, workspace_suspended, content_policy_hold, origin_not_allowed, or a plan limit (type: plan_limit_error — feature_not_in_plan, plan_limit_reached with param naming the plan limit key). Any credentialed operation can return it: an API key on a session-only route is insufficient_scope, a session write without an allowed Origin is origin_not_allowed, and an API key whose plan has no API access is feature_not_in_plan.

    Error codes: two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, feature_not_in_plan, plan_limit_reached

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    403 response: plan limit reachedJSON
    {
      "error": {
        "type": "plan_limit_error",
        "code": "plan_limit_reached",
        "message": "You have created 2,000 of 2,000 links this billing period. Upgrade or buy a pack to continue.",
        "param": "links_per_month",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/pricing",
        "details": [
          {
            "code": "plan_limit_reached",
            "message": "Monthly link quota reached",
            "limit": 2000,
            "used": 2000,
            "resets_at": "2026-12-01T00:00:00.000Z",
            "upgrade_to": "business"
          }
        ]
      }
    }
    403 response: feature not in planJSON
    {
      "error": {
        "type": "plan_limit_error",
        "code": "feature_not_in_plan",
        "message": "The public API is available on the Pro plan and above.",
        "param": "api_requests_per_day",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/pricing",
        "details": [
          {
            "code": "feature_not_in_plan",
            "message": "Upgrade to Pro",
            "upgrade_to": "pro"
          }
        ]
      }
    }
    403 response: email verification requiredJSON
    {
      "error": {
        "type": "permission_error",
        "code": "email_verification_required",
        "message": "Verify your email address to create links.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#email_verification_required"
      }
    }
    403 response: step up requiredJSON
    {
      "error": {
        "type": "permission_error",
        "code": "step_up_required",
        "message": "Confirm your password or 2FA code to continue.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#step_up_required"
      }
    }
  • Unknown id, or the resource belongs to another workspace (never distinguished). Also returned when a session's X-Workspace-Id names a workspace the user is not an active member of.

    Error codes: resource_not_found

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    404 response: not foundJSON
    {
      "error": {
        "type": "not_found_error",
        "code": "resource_not_found",
        "message": "No link with id lnk_01JB4N8ZK3Q9WXYZ0123456789.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#resource_not_found"
      }
    }
  • Burst limit, daily plan quota or endpoint limit exceeded.

    Error codes: rate_limited

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    429 response: rate limitedJSON
    {
      "error": {
        "type": "rate_limit_error",
        "code": "rate_limited",
        "message": "Daily API quota of 50,000 requests exhausted. Resets at 00:00 UTC.",
        "param": "daily_quota",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/rate-limits"
      }
    }

PATCH/links/{id}

  • Scope links:write

A new destination_url is re-scanned like a new link (fast path, then a queued deep scan; safety.status restarts). Changing slug needs custom_slugs (Starter+, else 403 feature_not_in_plan) and releases the old slug; on the shared domain a generated code is never reused and a custom slug is held 30 days for other workspaces if the link was live 24 hours or got a click, otherwise freed at once (at most 200 active holds per workspace; none for test-mode links; the same workspace may reclaim it; custom domains have no hold) (409 slug_taken, with details[0].code: held and details[0].available_after while another workspace's 30-day hold runs; 422 slug_invalid / slug_reserved). A link with a live QR code keeps its slug, because the printed code encodes it: changing it answers 409 state_conflict, param: slug. status pauses or resumes; blocked and archived links answer 409 state_conflict. Setting expires_at or password needs link_controls (Starter+, else 403 feature_not_in_plan); null clears them on any plan; resending the current password is no change, and a new one is hashed only after the other checks (at most 2 at once per workspace and 4 per API process, else 429 rate_limited, param: burst, Retry-After: 1); if the stored password changed concurrently before the write, 409 state_conflict (retry). Changing the destination of a link with more than 1,000 events in 30 days needs a fresh session (403 step_up_required). A new destination also updates payload.url of the link's live dynamic url QR code; its render_token is unchanged (the code encodes the short URL). tag_ids replaces the link's tags ([] removes them; an id that is not a live tag of this workspace answers 422 validation_failed, details[].code: unknown_tag). Phase 1 applies destination_url, slug, title, description, status, metadata, expires_at, password, tag_ids; other fields answer 422 validation_failed (details[].code: unsupported_in_phase) unless absent, null or empty. A body that changes nothing writes nothing.

Parameters

NameInTypeDescription
idrequiredpathstringPrefixed ULID (<prefix>_<26 Crockford base32 chars>).
X-Workspace-IdheaderstringWorkspace to act on. Required for session requests on workspace-scoped routes; optional for API keys (must equal the key's workspace, otherwise 403 workspace_mismatch). For a session, a workspace the user is not an active member of (another tenant, a removed membership, a malformed or unknown id) answers 404 resource_not_found, never 403.

Request body application/json

FieldTypeDescription
destination_urlstringAbsolute http(s) URL.Format: uri · max length 2048
slugstring1–64 chars of [A-Za-z0-9_-]; must not start with @.
titlestring | null
descriptionstring | null
statusstringOne of: active, paused
utmUtm | null
utm.sourcestringmax length 200
utm.mediumstringmax length 200
utm.campaignstringmax length 200
utm.termstringmax length 200
utm.contentstringmax length 200
utm_template_idstring | null
passwordstring | null
expires_atstring | nullFormat: date-time
expired_redirect_urlstring | nullFormat: uri
click_limitinteger | null
ogOpenGraph | null
og.titlestringmax length 120
og.descriptionstringmax length 300
og.image_urlstring | null1200×630 recommended. Upload via POST /assets with purpose: og_image and use the returned url.Format: uri
append_click_idboolean | null
campaign_idstring | null
tag_idsarray of string
pixel_idsarray of string
metadataMetadataUp to 20 keys (≤ 40 chars) with string values (≤ 500 chars). Stored verbatim.max keys 20
Request body: new destinationJSON
{
  "destination_url": "https://brand.in/diwali-sale-extended",
  "expires_at": "2026-11-20T18:29:59.000Z"
}

Request example

PATCH /links/{id}cURL
curl -X PATCH 'https://api.urlcrop.com/v1/links/lnk_01JB4N8ZK3Q9WXYZ0123456789' \
  -H 'Authorization: Bearer uc_live_…' \
  -H 'Content-Type: application/json' \
  -d '{
  "destination_url": "https://brand.in/diwali-sale-extended",
  "expires_at": "2026-11-20T18:29:59.000Z"
}'

Responses

  • Updated

    Link schema and example
    FieldTypeDescription
    idrequiredstring
    objectrequiredstringValue: link
    workspace_idrequiredstring
    is_testrequiredboolean
    domain_idstring | nullnull for the system domain crop.sh.
    domainrequiredstring
    slugrequiredstring1–64 chars of [A-Za-z0-9_-]; must not start with @.
    short_urlrequiredstringTest-mode links use https://test.crop.sh/<slug>, which never redirects. For the system domain the host follows Workspace.short_link_host: while staff run the backup short domain (ops switch short_host_fallback) new responses show the backup host (for example https://go.crop.sh/diwali). Both hosts redirect the same slug.Format: uri
    destination_urlrequiredstringAbsolute http(s) URL.Format: uri · max length 2048
    titlestring | nullmax length 200
    descriptionstring | nullmax length 1000
    statusrequiredstringblocked is set by safety; expired by expires_at/click_limit; archived by delete.One of: active, paused, expired, blocked, archived
    utmUtm | null
    utm.sourcestringmax length 200
    utm.mediumstringmax length 200
    utm.campaignstringmax length 200
    utm.termstringmax length 200
    utm.contentstringmax length 200
    utm_template_idstring | null
    password_protectedrequiredbooleanThe password itself is write-only.
    expires_atstring | nullFormat: date-time
    expired_redirect_urlstring | nullFormat: uri
    click_limitinteger | null≥ 1
    ogOpenGraph | null
    og.titlestringmax length 120
    og.descriptionstringmax length 300
    og.image_urlstring | null1200×630 recommended. Upload via POST /assets with purpose: og_image and use the returned url.Format: uri
    append_click_idboolean | nullnull inherits the workspace setting.
    campaign_idstring | null
    tag_idsrequiredarray of string
    pixel_idsarray of stringPhase 2 (Pro+).
    qr_code_idstring | null
    landing_kindstring | nullSet on the link of a dynamic UPI (upi_confirm) or vCard (vcard) QR code: scans open URLCrop's payee confirmation or contact page, so the destination is not a web URL, is never scanned (safety.scanned_at stays null) and is changed on the QR code (PATCH /v1/links/{id} with destination_url → 422 validation_failed, details[].code: qr_landing_link). Null for every other link.One of: upi_confirm, vcard, null
    routing_rulesarray of RoutingRulePhase 2; only present with expand[]=routing_rules.
    routing_rules[].idstring
    routing_rules[].typerequiredstringOne of: geo, device, os, language, time, ab
    routing_rules[].priorityinteger≥ 0
    routing_rules[].conditionsobject
    routing_rules[].conditions.countriesarray of string
    routing_rules[].conditions.devicesarray of string
    routing_rules[].conditions.osarray of string
    routing_rules[].conditions.languagesarray of string
    routing_rules[].conditions.timeobject
    routing_rules[].destination_urlrequiredstringAbsolute http(s) URL.Format: uri · max length 2048
    routing_rules[].weightintegerab only.≥ 1, ≤ 100
    routing_rules[].enabledboolean
    safetyrequiredobject
    safety.statusrequiredstringpending and flagged show the interstitial; blocked never redirects.One of: safe, pending, flagged, blocked
    safety.risk_scoreinteger | null≥ 0, ≤ 100
    safety.scanned_atstring | nullFormat: date-time
    healthrequiredobject
    health.statusrequiredstringOne of: healthy, unhealthy, unknown
    health.http_statusinteger | null
    health.last_checked_atstring | nullFormat: date-time
    clicksrequiredintegerLifetime clicks from rollups (refreshed within 5 s).
    clicks_7dintegerList responses only. Clicks (clicks and QR scans, like clicks) in the last 7 UTC days including today, from rollup_daily; today is partial.
    clicks_prev_7dintegerList responses only. Clicks in the 7 UTC days before clicks_7d's window, for the change indicator.
    scansrequiredinteger
    last_event_atstring | nullFormat: date-time
    metadataMetadataUp to 20 keys (≤ 40 chars) with string values (≤ 500 chars). Stored verbatim.max keys 20
    created_bystring | nullMember id, or API key id when created by a key.
    created_atrequiredstringISO 8601 UTC with milliseconds.Format: date-time
    updated_atrequiredstringISO 8601 UTC with milliseconds.Format: date-time
    archived_atstring | nullFormat: date-time
    200 response: 200 responseJSON
    {
      "id": "lnk_01JB4N8ZK3Q9WXYZ0123456789",
      "object": "link",
      "workspace_id": "ws_01JB4M0A1B2C3D4E5F6G7H8J9K",
      "is_test": false,
      "domain_id": "string",
      "domain": "crop.sh",
      "slug": "diwali",
      "short_url": "https://crop.sh/diwali",
      "destination_url": "https://example.com",
      "title": "string",
      "description": "string",
      "status": "active",
      "utm": {
        "source": "string",
        "medium": "string",
        "campaign": "string",
        "term": "string",
        "content": "string"
      },
      "utm_template_id": "string",
      "password_protected": false,
      "expires_at": "2026-12-05T10:15:30.000Z",
      "expired_redirect_url": "https://example.com",
      "click_limit": 1,
      "og": {
        "title": "string",
        "description": "string",
        "image_url": "https://example.com"
      },
      "append_click_id": false,
      "campaign_id": "string",
      "tag_ids": [
        "string"
      ],
      "pixel_ids": [
        "string"
      ],
      "qr_code_id": "string",
      "landing_kind": "upi_confirm",
      "routing_rules": [
        {
          "id": "rr_01JB4N9A0B1C2D3E4F5G6H7J8K",
          "type": "geo",
          "priority": 0,
          "conditions": {
            "countries": [
              "string"
            ],
            "devices": [
              "mobile"
            ],
            "os": [
              "ios"
            ],
            "languages": [
              "string"
            ],
            "time": {
              "timezone": "string",
              "start": "string",
              "end": "string",
              "days": []
            }
          },
          "destination_url": "https://example.com",
          "weight": 1,
          "enabled": true
        }
      ],
      "safety": {
        "status": "safe",
        "risk_score": 0,
        "scanned_at": "2026-12-05T10:15:30.000Z"
      },
      "health": {
        "status": "healthy",
        "http_status": 0,
        "last_checked_at": "2026-12-05T10:15:30.000Z"
      },
      "clicks": 0,
      "clicks_7d": 0,
      "clicks_prev_7d": 0,
      "scans": 0,
      "last_event_at": "2026-12-05T10:15:30.000Z",
      "metadata": {},
      "created_by": "string",
      "created_at": "2026-12-05T10:15:30.000Z",
      "updated_at": "2026-12-05T10:15:30.000Z",
      "archived_at": "2026-12-05T10:15:30.000Z"
    }
    Example generated from the schema.
  • Waiting for approval: the workspace has approvals on and the caller's role needs review (Editor). Nothing is published until an Owner or Admin approves.

    ApprovalRequest schema and example
    FieldTypeDescription
    idrequiredstring
    objectrequiredstringValue: approval_request
    resource_typerequiredstringLinks at this phase; qr_code, page, campaign follow with those resources.Value: link
    resource_idrequiredstring | nullThe link (lnk_…) changed or archived; for create, null until applied, then the new link.
    actionrequiredstringOne of: create, update, delete
    payloadrequiredobjectThe proposed body (LinkCreate for create, LinkUpdate for update, {} for delete), with password replaced by password_set: true.
    statusrequiredstringpending until reviewed; applied once approved and written; approved only while the change is being applied; expired 14 days after creation without a decision.One of: pending, approved, rejected, cancelled, applied, expired
    requested_byrequiredApprovalActor
    requested_by.namerequiredstring | null
    requested_by.emailrequiredstring | nullNull when the member has left the workspace.
    reviewed_byrequiredApprovalActor | null
    reviewed_by.namerequiredstring | null
    reviewed_by.emailrequiredstring | nullNull when the member has left the workspace.
    reviewed_atrequiredstring | nullFormat: date-time
    review_noterequiredstring | nullReviewer comment; the reason when rejected.
    applied_atrequiredstring | nullFormat: date-time
    expires_atrequiredstringISO 8601 UTC with milliseconds.Format: date-time
    is_minerequiredbooleanThe signed-in user made the request.
    targetrequiredobject | nullCurrent short URL and destination of the link an update or delete targets (null for create).
    target.short_urlrequiredstring
    target.destination_urlrequiredstring
    target.statusrequiredstring
    created_atrequiredstringISO 8601 UTC with milliseconds.Format: date-time
    updated_atrequiredstringISO 8601 UTC with milliseconds.Format: date-time
    202 response: 202 responseJSON
    {
      "id": "string",
      "object": "approval_request",
      "resource_type": "link",
      "resource_id": "string",
      "action": "create",
      "payload": {},
      "status": "pending",
      "requested_by": {
        "name": "string",
        "email": "string"
      },
      "reviewed_by": {
        "name": "string",
        "email": "string"
      },
      "reviewed_at": "2026-12-05T10:15:30.000Z",
      "review_note": "string",
      "applied_at": "2026-12-05T10:15:30.000Z",
      "expires_at": "2026-12-05T10:15:30.000Z",
      "is_mine": false,
      "target": {
        "short_url": "string",
        "destination_url": "string",
        "status": "string"
      },
      "created_at": "2026-12-05T10:15:30.000Z",
      "updated_at": "2026-12-05T10:15:30.000Z"
    }
    Example generated from the schema.
  • Malformed request (invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand). Any credentialed operation can return it: a credential in the query string (api_key, access_token) is invalid_query, and a session request without X-Workspace-Id on a workspace-scoped route is missing_header.

    Error codes: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    400 response: missing headerJSON
    {
      "error": {
        "type": "invalid_request_error",
        "code": "missing_header",
        "message": "The X-Workspace-Id header is required for session requests.",
        "param": "X-Workspace-Id",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#missing_header"
      }
    }
  • Missing or invalid credentials. Always code: unauthenticated; details[0].code gives the reason (missing_credentials, invalid_api_key, api_key_revoked, api_key_expired, session_expired, publishable_key_not_allowed).

    Error codes: unauthenticated

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    401 response: unauthenticatedJSON
    {
      "error": {
        "type": "authentication_error",
        "code": "unauthenticated",
        "message": "The API key is not valid.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#unauthenticated",
        "details": [
          {
            "path": "Authorization",
            "code": "invalid_api_key",
            "message": "No key matches this secret"
          }
        ]
      }
    }
  • Permitted credential but not allowed: insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, two_factor_required (also Owner/Admin writes on Business+ after the 14-day 2FA grace, details[0].grace_ends_at), step_up_required, email_verification_required, signups_paused (sign-up while the platform setting is on), write_blocked_past_due, workspace_suspended, content_policy_hold, origin_not_allowed, or a plan limit (type: plan_limit_error — feature_not_in_plan, plan_limit_reached with param naming the plan limit key). Any credentialed operation can return it: an API key on a session-only route is insufficient_scope, a session write without an allowed Origin is origin_not_allowed, and an API key whose plan has no API access is feature_not_in_plan.

    Error codes: two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, feature_not_in_plan, plan_limit_reached

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    403 response: plan limit reachedJSON
    {
      "error": {
        "type": "plan_limit_error",
        "code": "plan_limit_reached",
        "message": "You have created 2,000 of 2,000 links this billing period. Upgrade or buy a pack to continue.",
        "param": "links_per_month",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/pricing",
        "details": [
          {
            "code": "plan_limit_reached",
            "message": "Monthly link quota reached",
            "limit": 2000,
            "used": 2000,
            "resets_at": "2026-12-01T00:00:00.000Z",
            "upgrade_to": "business"
          }
        ]
      }
    }
    403 response: feature not in planJSON
    {
      "error": {
        "type": "plan_limit_error",
        "code": "feature_not_in_plan",
        "message": "The public API is available on the Pro plan and above.",
        "param": "api_requests_per_day",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/pricing",
        "details": [
          {
            "code": "feature_not_in_plan",
            "message": "Upgrade to Pro",
            "upgrade_to": "pro"
          }
        ]
      }
    }
    403 response: email verification requiredJSON
    {
      "error": {
        "type": "permission_error",
        "code": "email_verification_required",
        "message": "Verify your email address to create links.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#email_verification_required"
      }
    }
    403 response: step up requiredJSON
    {
      "error": {
        "type": "permission_error",
        "code": "step_up_required",
        "message": "Confirm your password or 2FA code to continue.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#step_up_required"
      }
    }
  • Unknown id, or the resource belongs to another workspace (never distinguished). Also returned when a session's X-Workspace-Id names a workspace the user is not an active member of.

    Error codes: resource_not_found

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    404 response: not foundJSON
    {
      "error": {
        "type": "not_found_error",
        "code": "resource_not_found",
        "message": "No link with id lnk_01JB4N8ZK3Q9WXYZ0123456789.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#resource_not_found"
      }
    }
  • State conflict (slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use).

    Error codes: slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    409 response: slug takenJSON
    {
      "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://urlcrop.com/docs/api/errors#slug_taken"
      }
    }
  • Validation failure (validation_failed, destination_invalid, destination_blocked (details[0].code is policy_violation, with the category, for content-policy blocks), slug_invalid, slug_reserved, slug_not_allowed, handle_not_allowed, title_not_allowed, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, policy_acceptance_required, …).

    Error codes: validation_failed, destination_invalid, destination_blocked, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, policy_acceptance_required

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    422 response: validation failedJSON
    {
      "error": {
        "type": "validation_error",
        "code": "validation_failed",
        "message": "The request body is invalid.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#validation_failed",
        "details": [
          {
            "path": "destination_url",
            "code": "invalid_string",
            "message": "Must be an absolute http(s) URL"
          }
        ]
      }
    }
    422 response: destination blockedJSON
    {
      "error": {
        "type": "validation_error",
        "code": "destination_blocked",
        "message": "This destination was flagged as unsafe and cannot be shortened.",
        "param": "destination_url",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#destination_blocked"
      }
    }
  • Burst limit, daily plan quota or endpoint limit exceeded.

    Error codes: rate_limited

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    429 response: rate limitedJSON
    {
      "error": {
        "type": "rate_limit_error",
        "code": "rate_limited",
        "message": "Daily API quota of 50,000 requests exhausted. Resets at 00:00 UTC.",
        "param": "daily_quota",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/rate-limits"
      }
    }

DELETE/links/{id}

  • Scope links:write

The short URL returns 404 immediately; the link is permanently deleted 30 days after archiving, and its shared-domain slug is then held for other workspaces per decision 17. Archiving an archived link is a no-op (204); a blocked link answers 409 state_conflict.

Parameters

NameInTypeDescription
idrequiredpathstringPrefixed ULID (<prefix>_<26 Crockford base32 chars>).
X-Workspace-IdheaderstringWorkspace to act on. Required for session requests on workspace-scoped routes; optional for API keys (must equal the key's workspace, otherwise 403 workspace_mismatch). For a session, a workspace the user is not an active member of (another tenant, a removed membership, a malformed or unknown id) answers 404 resource_not_found, never 403.

Request example

DELETE /links/{id}cURL
curl -X DELETE 'https://api.urlcrop.com/v1/links/lnk_01JB4N8ZK3Q9WXYZ0123456789' \
  -H 'Authorization: Bearer uc_live_…'

Responses

  • Waiting for approval: the workspace has approvals on and the caller's role needs review (Editor). Nothing is published until an Owner or Admin approves.

    ApprovalRequest schema and example
    FieldTypeDescription
    idrequiredstring
    objectrequiredstringValue: approval_request
    resource_typerequiredstringLinks at this phase; qr_code, page, campaign follow with those resources.Value: link
    resource_idrequiredstring | nullThe link (lnk_…) changed or archived; for create, null until applied, then the new link.
    actionrequiredstringOne of: create, update, delete
    payloadrequiredobjectThe proposed body (LinkCreate for create, LinkUpdate for update, {} for delete), with password replaced by password_set: true.
    statusrequiredstringpending until reviewed; applied once approved and written; approved only while the change is being applied; expired 14 days after creation without a decision.One of: pending, approved, rejected, cancelled, applied, expired
    requested_byrequiredApprovalActor
    requested_by.namerequiredstring | null
    requested_by.emailrequiredstring | nullNull when the member has left the workspace.
    reviewed_byrequiredApprovalActor | null
    reviewed_by.namerequiredstring | null
    reviewed_by.emailrequiredstring | nullNull when the member has left the workspace.
    reviewed_atrequiredstring | nullFormat: date-time
    review_noterequiredstring | nullReviewer comment; the reason when rejected.
    applied_atrequiredstring | nullFormat: date-time
    expires_atrequiredstringISO 8601 UTC with milliseconds.Format: date-time
    is_minerequiredbooleanThe signed-in user made the request.
    targetrequiredobject | nullCurrent short URL and destination of the link an update or delete targets (null for create).
    target.short_urlrequiredstring
    target.destination_urlrequiredstring
    target.statusrequiredstring
    created_atrequiredstringISO 8601 UTC with milliseconds.Format: date-time
    updated_atrequiredstringISO 8601 UTC with milliseconds.Format: date-time
    202 response: 202 responseJSON
    {
      "id": "string",
      "object": "approval_request",
      "resource_type": "link",
      "resource_id": "string",
      "action": "create",
      "payload": {},
      "status": "pending",
      "requested_by": {
        "name": "string",
        "email": "string"
      },
      "reviewed_by": {
        "name": "string",
        "email": "string"
      },
      "reviewed_at": "2026-12-05T10:15:30.000Z",
      "review_note": "string",
      "applied_at": "2026-12-05T10:15:30.000Z",
      "expires_at": "2026-12-05T10:15:30.000Z",
      "is_mine": false,
      "target": {
        "short_url": "string",
        "destination_url": "string",
        "status": "string"
      },
      "created_at": "2026-12-05T10:15:30.000Z",
      "updated_at": "2026-12-05T10:15:30.000Z"
    }
    Example generated from the schema.
  • Deleted or applied. No body.

  • Malformed request (invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand). Any credentialed operation can return it: a credential in the query string (api_key, access_token) is invalid_query, and a session request without X-Workspace-Id on a workspace-scoped route is missing_header.

    Error codes: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    400 response: missing headerJSON
    {
      "error": {
        "type": "invalid_request_error",
        "code": "missing_header",
        "message": "The X-Workspace-Id header is required for session requests.",
        "param": "X-Workspace-Id",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#missing_header"
      }
    }
  • Missing or invalid credentials. Always code: unauthenticated; details[0].code gives the reason (missing_credentials, invalid_api_key, api_key_revoked, api_key_expired, session_expired, publishable_key_not_allowed).

    Error codes: unauthenticated

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    401 response: unauthenticatedJSON
    {
      "error": {
        "type": "authentication_error",
        "code": "unauthenticated",
        "message": "The API key is not valid.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#unauthenticated",
        "details": [
          {
            "path": "Authorization",
            "code": "invalid_api_key",
            "message": "No key matches this secret"
          }
        ]
      }
    }
  • Permitted credential but not allowed: insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, two_factor_required (also Owner/Admin writes on Business+ after the 14-day 2FA grace, details[0].grace_ends_at), step_up_required, email_verification_required, signups_paused (sign-up while the platform setting is on), write_blocked_past_due, workspace_suspended, content_policy_hold, origin_not_allowed, or a plan limit (type: plan_limit_error — feature_not_in_plan, plan_limit_reached with param naming the plan limit key). Any credentialed operation can return it: an API key on a session-only route is insufficient_scope, a session write without an allowed Origin is origin_not_allowed, and an API key whose plan has no API access is feature_not_in_plan.

    Error codes: two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, feature_not_in_plan, plan_limit_reached

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    403 response: plan limit reachedJSON
    {
      "error": {
        "type": "plan_limit_error",
        "code": "plan_limit_reached",
        "message": "You have created 2,000 of 2,000 links this billing period. Upgrade or buy a pack to continue.",
        "param": "links_per_month",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/pricing",
        "details": [
          {
            "code": "plan_limit_reached",
            "message": "Monthly link quota reached",
            "limit": 2000,
            "used": 2000,
            "resets_at": "2026-12-01T00:00:00.000Z",
            "upgrade_to": "business"
          }
        ]
      }
    }
    403 response: feature not in planJSON
    {
      "error": {
        "type": "plan_limit_error",
        "code": "feature_not_in_plan",
        "message": "The public API is available on the Pro plan and above.",
        "param": "api_requests_per_day",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/pricing",
        "details": [
          {
            "code": "feature_not_in_plan",
            "message": "Upgrade to Pro",
            "upgrade_to": "pro"
          }
        ]
      }
    }
    403 response: email verification requiredJSON
    {
      "error": {
        "type": "permission_error",
        "code": "email_verification_required",
        "message": "Verify your email address to create links.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#email_verification_required"
      }
    }
    403 response: step up requiredJSON
    {
      "error": {
        "type": "permission_error",
        "code": "step_up_required",
        "message": "Confirm your password or 2FA code to continue.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#step_up_required"
      }
    }
  • Unknown id, or the resource belongs to another workspace (never distinguished). Also returned when a session's X-Workspace-Id names a workspace the user is not an active member of.

    Error codes: resource_not_found

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    404 response: not foundJSON
    {
      "error": {
        "type": "not_found_error",
        "code": "resource_not_found",
        "message": "No link with id lnk_01JB4N8ZK3Q9WXYZ0123456789.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#resource_not_found"
      }
    }
  • State conflict (slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use).

    Error codes: slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    409 response: slug takenJSON
    {
      "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://urlcrop.com/docs/api/errors#slug_taken"
      }
    }
  • Burst limit, daily plan quota or endpoint limit exceeded.

    Error codes: rate_limited

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    429 response: rate limitedJSON
    {
      "error": {
        "type": "rate_limit_error",
        "code": "rate_limited",
        "message": "Daily API quota of 50,000 requests exhausted. Resets at 00:00 UTC.",
        "param": "daily_quota",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/rate-limits"
      }
    }

Stats shortcut for one link

GET/links/{id}/stats

  • Scope insights:read

Parameters

NameInTypeDescription
idrequiredpathstringPrefixed ULID (<prefix>_<26 Crockford base32 chars>).
X-Workspace-IdheaderstringWorkspace to act on. Required for session requests on workspace-scoped routes; optional for API keys (must equal the key's workspace, otherwise 403 workspace_mismatch). For a session, a workspace the user is not an active member of (another tenant, a removed membership, a malformed or unknown id) answers 404 resource_not_found, never 403.
fromquerystringStart of the period (inclusive). YYYY-MM-DD or RFC 3339. Default 30 days ago.
toquerystringEnd of the period (inclusive). Default now.
timezonequerystringIANA timezone for bucketing. Default is the workspace timezone.
include_botsquerybooleanWhen true, flagged bot events are added into click, scan and view counts (the rollups' bots column). Default false excludes them. unique_visitors never includes bots.

Request example

GET /links/{id}/statscURL
curl 'https://api.urlcrop.com/v1/links/lnk_01JB4N8ZK3Q9WXYZ0123456789/stats' \
  -H 'Authorization: Bearer uc_live_…'

Responses

  • 200 response

    Stats

    StatsSummary schema and example
    FieldTypeDescription
    objectrequiredstringValue: stats
    subject_idstringPrefixed ULID (<prefix>_<26 Crockford base32 chars>).
    periodrequiredobject
    period.fromrequiredstringISO 8601 UTC with milliseconds.Format: date-time
    period.torequiredstringISO 8601 UTC with milliseconds.Format: date-time
    period.timezonerequiredstring
    truncated_to_retentionbooleanTrue when period.from was moved forward to the plan's analytics_retention_days window (also sent as X-URLCrop-Truncated retention).
    totalsrequiredobject
    totals.clicksinteger
    totals.scansinteger
    totals.viewsinteger
    totals.unique_visitorsintegerSum of daily unique visitors over the period ("unique visitors (daily)").
    totals.over_quota_eventsintegerEvents beyond the monthly event quota in the UTC months overlapping the period. Counted, but stored without detail, so they are not in timeseries, top or the other totals.
    totals.conversionsintegerPhase 2.
    totals.revenueMoney
    totals.revenue.amountrequiredintegerInteger minor units (paise, cents). Never a float.
    totals.revenue.currencyrequiredstringISO 4217 code.One of: INR, USD
    timeseriesrequiredarray of object
    timeseries[].daterequiredstringFormat: date
    timeseries[].clicksinteger
    timeseries[].scansinteger
    timeseries[].viewsinteger
    timeseries[].unique_visitorsinteger
    toprequiredobject
    top.countriesarray of BreakdownRow
    top.countries[].keyrequiredstringDimension value (ISO country code, device class, referrer host, link id…).
    top.countries[].labelstring
    top.countries[].valuerequirednumber
    top.countries[].sharenumberFraction of the total (0–1).
    top.devicesarray of BreakdownRow
    top.devices[].keyrequiredstringDimension value (ISO country code, device class, referrer host, link id…).
    top.devices[].labelstring
    top.devices[].valuerequirednumber
    top.devices[].sharenumberFraction of the total (0–1).
    top.referrersarray of BreakdownRow
    top.referrers[].keyrequiredstringDimension value (ISO country code, device class, referrer host, link id…).
    top.referrers[].labelstring
    top.referrers[].valuerequirednumber
    top.referrers[].sharenumberFraction of the total (0–1).
    200 response: monthJSON
    {
      "object": "stats",
      "subject_id": "lnk_01JB4N8ZK3Q9WXYZ0123456789",
      "period": {
        "from": "2026-11-05T00:00:00.000Z",
        "to": "2026-12-05T23:59:59.999Z",
        "timezone": "Asia/Kolkata"
      },
      "totals": {
        "clicks": 8450,
        "scans": 1210,
        "unique_visitors": 7120
      },
      "timeseries": [
        {
          "date": "2026-12-04",
          "clicks": 410,
          "scans": 55
        },
        {
          "date": "2026-12-05",
          "clicks": 388,
          "scans": 61
        }
      ],
      "top": {
        "countries": [
          {
            "key": "IN",
            "label": "India",
            "value": 7890,
            "share": 0.82
          }
        ],
        "devices": [
          {
            "key": "mobile",
            "value": 8300,
            "share": 0.86
          }
        ],
        "referrers": [
          {
            "key": "whatsapp",
            "value": 6100,
            "share": 0.63
          }
        ]
      }
    }
  • 304 response

    The If-None-Match ETag matches; no body. Sent for cacheable aggregate reads (Cache-Control: private, max-age=30, weak ETag).

  • 400 response

    Malformed request (invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand). Any credentialed operation can return it: a credential in the query string (api_key, access_token) is invalid_query, and a session request without X-Workspace-Id on a workspace-scoped route is missing_header.

    Error codes: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    400 response: missing headerJSON
    {
      "error": {
        "type": "invalid_request_error",
        "code": "missing_header",
        "message": "The X-Workspace-Id header is required for session requests.",
        "param": "X-Workspace-Id",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#missing_header"
      }
    }
  • 401 response

    Missing or invalid credentials. Always code: unauthenticated; details[0].code gives the reason (missing_credentials, invalid_api_key, api_key_revoked, api_key_expired, session_expired, publishable_key_not_allowed).

    Error codes: unauthenticated

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    401 response: unauthenticatedJSON
    {
      "error": {
        "type": "authentication_error",
        "code": "unauthenticated",
        "message": "The API key is not valid.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#unauthenticated",
        "details": [
          {
            "path": "Authorization",
            "code": "invalid_api_key",
            "message": "No key matches this secret"
          }
        ]
      }
    }
  • 403 response

    Permitted credential but not allowed: insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, two_factor_required (also Owner/Admin writes on Business+ after the 14-day 2FA grace, details[0].grace_ends_at), step_up_required, email_verification_required, signups_paused (sign-up while the platform setting is on), write_blocked_past_due, workspace_suspended, content_policy_hold, origin_not_allowed, or a plan limit (type: plan_limit_error — feature_not_in_plan, plan_limit_reached with param naming the plan limit key). Any credentialed operation can return it: an API key on a session-only route is insufficient_scope, a session write without an allowed Origin is origin_not_allowed, and an API key whose plan has no API access is feature_not_in_plan.

    Error codes: two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, feature_not_in_plan, plan_limit_reached

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    403 response: plan limit reachedJSON
    {
      "error": {
        "type": "plan_limit_error",
        "code": "plan_limit_reached",
        "message": "You have created 2,000 of 2,000 links this billing period. Upgrade or buy a pack to continue.",
        "param": "links_per_month",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/pricing",
        "details": [
          {
            "code": "plan_limit_reached",
            "message": "Monthly link quota reached",
            "limit": 2000,
            "used": 2000,
            "resets_at": "2026-12-01T00:00:00.000Z",
            "upgrade_to": "business"
          }
        ]
      }
    }
    403 response: feature not in planJSON
    {
      "error": {
        "type": "plan_limit_error",
        "code": "feature_not_in_plan",
        "message": "The public API is available on the Pro plan and above.",
        "param": "api_requests_per_day",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/pricing",
        "details": [
          {
            "code": "feature_not_in_plan",
            "message": "Upgrade to Pro",
            "upgrade_to": "pro"
          }
        ]
      }
    }
    403 response: email verification requiredJSON
    {
      "error": {
        "type": "permission_error",
        "code": "email_verification_required",
        "message": "Verify your email address to create links.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#email_verification_required"
      }
    }
    403 response: step up requiredJSON
    {
      "error": {
        "type": "permission_error",
        "code": "step_up_required",
        "message": "Confirm your password or 2FA code to continue.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#step_up_required"
      }
    }
  • 404 response

    Unknown id, or the resource belongs to another workspace (never distinguished). Also returned when a session's X-Workspace-Id names a workspace the user is not an active member of.

    Error codes: resource_not_found

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    404 response: not foundJSON
    {
      "error": {
        "type": "not_found_error",
        "code": "resource_not_found",
        "message": "No link with id lnk_01JB4N8ZK3Q9WXYZ0123456789.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#resource_not_found"
      }
    }
  • 429 response

    Burst limit, daily plan quota or endpoint limit exceeded.

    Error codes: rate_limited

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    429 response: rate limitedJSON
    {
      "error": {
        "type": "rate_limit_error",
        "code": "rate_limited",
        "message": "Daily API quota of 50,000 requests exhausted. Resets at 00:00 UTC.",
        "param": "daily_quota",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/rate-limits"
      }
    }

Destination health

GET/links/{id}/health

  • Scope links:read

Parameters

NameInTypeDescription
idrequiredpathstringPrefixed ULID (<prefix>_<26 Crockford base32 chars>).
X-Workspace-IdheaderstringWorkspace to act on. Required for session requests on workspace-scoped routes; optional for API keys (must equal the key's workspace, otherwise 403 workspace_mismatch). For a session, a workspace the user is not an active member of (another tenant, a removed membership, a malformed or unknown id) answers 404 resource_not_found, never 403.

Request example

GET /links/{id}/healthcURL
curl 'https://api.urlcrop.com/v1/links/lnk_01JB4N8ZK3Q9WXYZ0123456789/health' \
  -H 'Authorization: Bearer uc_live_…'

Responses

  • 200 response

    Health

    LinkHealth schema and example
    FieldTypeDescription
    link_idrequiredstring
    statusrequiredstringOne of: healthy, unhealthy, unknown
    http_statusinteger | null
    last_checked_atstring | nullFormat: date-time
    consecutive_failuresrequiredinteger
    historyarray of objectmax items 30
    history[].checked_atstringISO 8601 UTC with milliseconds.Format: date-time
    history[].http_statusinteger | null
    history[].latency_msinteger | null
    history[].errorstring | null
    200 response: 200 responseJSON
    {
      "link_id": "string",
      "status": "healthy",
      "http_status": 0,
      "last_checked_at": "2026-12-05T10:15:30.000Z",
      "consecutive_failures": 0,
      "history": [
        {
          "checked_at": "2026-12-05T10:15:30.000Z",
          "http_status": 0,
          "latency_ms": 0,
          "error": "string"
        }
      ]
    }
    Example generated from the schema.
  • 400 response

    Malformed request (invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand). Any credentialed operation can return it: a credential in the query string (api_key, access_token) is invalid_query, and a session request without X-Workspace-Id on a workspace-scoped route is missing_header.

    Error codes: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    400 response: missing headerJSON
    {
      "error": {
        "type": "invalid_request_error",
        "code": "missing_header",
        "message": "The X-Workspace-Id header is required for session requests.",
        "param": "X-Workspace-Id",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#missing_header"
      }
    }
  • 401 response

    Missing or invalid credentials. Always code: unauthenticated; details[0].code gives the reason (missing_credentials, invalid_api_key, api_key_revoked, api_key_expired, session_expired, publishable_key_not_allowed).

    Error codes: unauthenticated

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    401 response: unauthenticatedJSON
    {
      "error": {
        "type": "authentication_error",
        "code": "unauthenticated",
        "message": "The API key is not valid.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#unauthenticated",
        "details": [
          {
            "path": "Authorization",
            "code": "invalid_api_key",
            "message": "No key matches this secret"
          }
        ]
      }
    }
  • 403 response

    Permitted credential but not allowed: insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, two_factor_required (also Owner/Admin writes on Business+ after the 14-day 2FA grace, details[0].grace_ends_at), step_up_required, email_verification_required, signups_paused (sign-up while the platform setting is on), write_blocked_past_due, workspace_suspended, content_policy_hold, origin_not_allowed, or a plan limit (type: plan_limit_error — feature_not_in_plan, plan_limit_reached with param naming the plan limit key). Any credentialed operation can return it: an API key on a session-only route is insufficient_scope, a session write without an allowed Origin is origin_not_allowed, and an API key whose plan has no API access is feature_not_in_plan.

    Error codes: two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, feature_not_in_plan, plan_limit_reached

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    403 response: plan limit reachedJSON
    {
      "error": {
        "type": "plan_limit_error",
        "code": "plan_limit_reached",
        "message": "You have created 2,000 of 2,000 links this billing period. Upgrade or buy a pack to continue.",
        "param": "links_per_month",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/pricing",
        "details": [
          {
            "code": "plan_limit_reached",
            "message": "Monthly link quota reached",
            "limit": 2000,
            "used": 2000,
            "resets_at": "2026-12-01T00:00:00.000Z",
            "upgrade_to": "business"
          }
        ]
      }
    }
    403 response: feature not in planJSON
    {
      "error": {
        "type": "plan_limit_error",
        "code": "feature_not_in_plan",
        "message": "The public API is available on the Pro plan and above.",
        "param": "api_requests_per_day",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/pricing",
        "details": [
          {
            "code": "feature_not_in_plan",
            "message": "Upgrade to Pro",
            "upgrade_to": "pro"
          }
        ]
      }
    }
    403 response: email verification requiredJSON
    {
      "error": {
        "type": "permission_error",
        "code": "email_verification_required",
        "message": "Verify your email address to create links.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#email_verification_required"
      }
    }
    403 response: step up requiredJSON
    {
      "error": {
        "type": "permission_error",
        "code": "step_up_required",
        "message": "Confirm your password or 2FA code to continue.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#step_up_required"
      }
    }
  • 404 response

    Unknown id, or the resource belongs to another workspace (never distinguished). Also returned when a session's X-Workspace-Id names a workspace the user is not an active member of.

    Error codes: resource_not_found

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    404 response: not foundJSON
    {
      "error": {
        "type": "not_found_error",
        "code": "resource_not_found",
        "message": "No link with id lnk_01JB4N8ZK3Q9WXYZ0123456789.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#resource_not_found"
      }
    }
  • 429 response

    Burst limit, daily plan quota or endpoint limit exceeded.

    Error codes: rate_limited

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    429 response: rate limitedJSON
    {
      "error": {
        "type": "rate_limit_error",
        "code": "rate_limited",
        "message": "Daily API quota of 50,000 requests exhausted. Resets at 00:00 UTC.",
        "param": "daily_quota",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/rate-limits"
      }
    }

Queue an immediate health check (10/hour)

POST/links/{id}/health/check

  • Scope links:write

Parameters

NameInTypeDescription
idrequiredpathstringPrefixed ULID (<prefix>_<26 Crockford base32 chars>).
X-Workspace-IdheaderstringWorkspace to act on. Required for session requests on workspace-scoped routes; optional for API keys (must equal the key's workspace, otherwise 403 workspace_mismatch). For a session, a workspace the user is not an active member of (another tenant, a removed membership, a malformed or unknown id) answers 404 resource_not_found, never 403.
Idempotency-KeyheaderstringUnique string (UUID v4 recommended, ≤ 255 chars). Repeating a POST with the same key and body within 24 hours replays the original response with Idempotent-Replayed: true. One-time secrets are never cached: a replay returns them as null with Idempotent-Redacted: secret.

Request example

POST /links/{id}/health/checkcURL
curl -X POST 'https://api.urlcrop.com/v1/links/lnk_01JB4N8ZK3Q9WXYZ0123456789/health/check' \
  -H 'Authorization: Bearer uc_live_…'

Responses

  • 202 response

    Queued; poll GET /links/{id}/health

    LinkHealth schema and example
    FieldTypeDescription
    link_idrequiredstring
    statusrequiredstringOne of: healthy, unhealthy, unknown
    http_statusinteger | null
    last_checked_atstring | nullFormat: date-time
    consecutive_failuresrequiredinteger
    historyarray of objectmax items 30
    history[].checked_atstringISO 8601 UTC with milliseconds.Format: date-time
    history[].http_statusinteger | null
    history[].latency_msinteger | null
    history[].errorstring | null
    202 response: 202 responseJSON
    {
      "link_id": "string",
      "status": "healthy",
      "http_status": 0,
      "last_checked_at": "2026-12-05T10:15:30.000Z",
      "consecutive_failures": 0,
      "history": [
        {
          "checked_at": "2026-12-05T10:15:30.000Z",
          "http_status": 0,
          "latency_ms": 0,
          "error": "string"
        }
      ]
    }
    Example generated from the schema.
  • 400 response

    Malformed request (invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand). Any credentialed operation can return it: a credential in the query string (api_key, access_token) is invalid_query, and a session request without X-Workspace-Id on a workspace-scoped route is missing_header.

    Error codes: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    400 response: missing headerJSON
    {
      "error": {
        "type": "invalid_request_error",
        "code": "missing_header",
        "message": "The X-Workspace-Id header is required for session requests.",
        "param": "X-Workspace-Id",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#missing_header"
      }
    }
  • 401 response

    Missing or invalid credentials. Always code: unauthenticated; details[0].code gives the reason (missing_credentials, invalid_api_key, api_key_revoked, api_key_expired, session_expired, publishable_key_not_allowed).

    Error codes: unauthenticated

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    401 response: unauthenticatedJSON
    {
      "error": {
        "type": "authentication_error",
        "code": "unauthenticated",
        "message": "The API key is not valid.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#unauthenticated",
        "details": [
          {
            "path": "Authorization",
            "code": "invalid_api_key",
            "message": "No key matches this secret"
          }
        ]
      }
    }
  • 403 response

    Permitted credential but not allowed: insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, two_factor_required (also Owner/Admin writes on Business+ after the 14-day 2FA grace, details[0].grace_ends_at), step_up_required, email_verification_required, signups_paused (sign-up while the platform setting is on), write_blocked_past_due, workspace_suspended, content_policy_hold, origin_not_allowed, or a plan limit (type: plan_limit_error — feature_not_in_plan, plan_limit_reached with param naming the plan limit key). Any credentialed operation can return it: an API key on a session-only route is insufficient_scope, a session write without an allowed Origin is origin_not_allowed, and an API key whose plan has no API access is feature_not_in_plan.

    Error codes: two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, feature_not_in_plan, plan_limit_reached

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    403 response: plan limit reachedJSON
    {
      "error": {
        "type": "plan_limit_error",
        "code": "plan_limit_reached",
        "message": "You have created 2,000 of 2,000 links this billing period. Upgrade or buy a pack to continue.",
        "param": "links_per_month",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/pricing",
        "details": [
          {
            "code": "plan_limit_reached",
            "message": "Monthly link quota reached",
            "limit": 2000,
            "used": 2000,
            "resets_at": "2026-12-01T00:00:00.000Z",
            "upgrade_to": "business"
          }
        ]
      }
    }
    403 response: feature not in planJSON
    {
      "error": {
        "type": "plan_limit_error",
        "code": "feature_not_in_plan",
        "message": "The public API is available on the Pro plan and above.",
        "param": "api_requests_per_day",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/pricing",
        "details": [
          {
            "code": "feature_not_in_plan",
            "message": "Upgrade to Pro",
            "upgrade_to": "pro"
          }
        ]
      }
    }
    403 response: email verification requiredJSON
    {
      "error": {
        "type": "permission_error",
        "code": "email_verification_required",
        "message": "Verify your email address to create links.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#email_verification_required"
      }
    }
    403 response: step up requiredJSON
    {
      "error": {
        "type": "permission_error",
        "code": "step_up_required",
        "message": "Confirm your password or 2FA code to continue.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#step_up_required"
      }
    }
  • 404 response

    Unknown id, or the resource belongs to another workspace (never distinguished). Also returned when a session's X-Workspace-Id names a workspace the user is not an active member of.

    Error codes: resource_not_found

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    404 response: not foundJSON
    {
      "error": {
        "type": "not_found_error",
        "code": "resource_not_found",
        "message": "No link with id lnk_01JB4N8ZK3Q9WXYZ0123456789.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#resource_not_found"
      }
    }
  • 409 response

    State conflict (slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use).

    Error codes: slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    409 response: slug takenJSON
    {
      "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://urlcrop.com/docs/api/errors#slug_taken"
      }
    }
  • 422 response

    Validation failure (validation_failed, destination_invalid, destination_blocked (details[0].code is policy_violation, with the category, for content-policy blocks), slug_invalid, slug_reserved, slug_not_allowed, handle_not_allowed, title_not_allowed, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, policy_acceptance_required, …).

    Error codes: validation_failed, destination_invalid, destination_blocked, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, policy_acceptance_required

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    422 response: validation failedJSON
    {
      "error": {
        "type": "validation_error",
        "code": "validation_failed",
        "message": "The request body is invalid.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#validation_failed",
        "details": [
          {
            "path": "destination_url",
            "code": "invalid_string",
            "message": "Must be an absolute http(s) URL"
          }
        ]
      }
    }
    422 response: destination blockedJSON
    {
      "error": {
        "type": "validation_error",
        "code": "destination_blocked",
        "message": "This destination was flagged as unsafe and cannot be shortened.",
        "param": "destination_url",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#destination_blocked"
      }
    }
  • 429 response

    Burst limit, daily plan quota or endpoint limit exceeded.

    Error codes: rate_limited

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    429 response: rate limitedJSON
    {
      "error": {
        "type": "rate_limit_error",
        "code": "rate_limited",
        "message": "Daily API quota of 50,000 requests exhausted. Resets at 00:00 UTC.",
        "param": "daily_quota",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/rate-limits"
      }
    }

Safety scan result and history

GET/links/{id}/safety

  • Scope links:read

Parameters

NameInTypeDescription
idrequiredpathstringPrefixed ULID (<prefix>_<26 Crockford base32 chars>).
X-Workspace-IdheaderstringWorkspace to act on. Required for session requests on workspace-scoped routes; optional for API keys (must equal the key's workspace, otherwise 403 workspace_mismatch). For a session, a workspace the user is not an active member of (another tenant, a removed membership, a malformed or unknown id) answers 404 resource_not_found, never 403.

Request example

GET /links/{id}/safetycURL
curl 'https://api.urlcrop.com/v1/links/lnk_01JB4N8ZK3Q9WXYZ0123456789/safety' \
  -H 'Authorization: Bearer uc_live_…'

Responses

  • 200 response

    Safety

    SafetyHistory schema and example
    FieldTypeDescription
    currentrequiredSafetyScan
    current.idrequiredstring
    current.objectrequiredstringValue: safety_scan
    current.link_idstring
    current.statusrequiredstringOne of: safe, pending, flagged, blocked
    current.risk_scoreinteger | null
    current.sourcesarray of stringWhich checks contributed (web_risk, urlhaus, phishtank, heuristics).
    current.reasonsarray of string
    current.scanned_atrequiredstringISO 8601 UTC with milliseconds.Format: date-time
    historyrequiredarray of SafetyScan
    history[].idrequiredstring
    history[].objectrequiredstringValue: safety_scan
    history[].link_idstring
    history[].statusrequiredstringOne of: safe, pending, flagged, blocked
    history[].risk_scoreinteger | null
    history[].sourcesarray of stringWhich checks contributed (web_risk, urlhaus, phishtank, heuristics).
    history[].reasonsarray of string
    history[].scanned_atrequiredstringISO 8601 UTC with milliseconds.Format: date-time
    200 response: 200 responseJSON
    {
      "current": {
        "id": "scan_01JB4NA1B2C3D4E5F6G7H8J9K0",
        "object": "safety_scan",
        "link_id": "string",
        "status": "safe",
        "risk_score": 0,
        "sources": [
          "string"
        ],
        "reasons": [
          "string"
        ],
        "scanned_at": "2026-12-05T10:15:30.000Z"
      },
      "history": [
        {
          "id": "scan_01JB4NA1B2C3D4E5F6G7H8J9K0",
          "object": "safety_scan",
          "link_id": "string",
          "status": "safe",
          "risk_score": 0,
          "sources": [
            "string"
          ],
          "reasons": [
            "string"
          ],
          "scanned_at": "2026-12-05T10:15:30.000Z"
        }
      ]
    }
    Example generated from the schema.
  • 400 response

    Malformed request (invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand). Any credentialed operation can return it: a credential in the query string (api_key, access_token) is invalid_query, and a session request without X-Workspace-Id on a workspace-scoped route is missing_header.

    Error codes: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    400 response: missing headerJSON
    {
      "error": {
        "type": "invalid_request_error",
        "code": "missing_header",
        "message": "The X-Workspace-Id header is required for session requests.",
        "param": "X-Workspace-Id",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#missing_header"
      }
    }
  • 401 response

    Missing or invalid credentials. Always code: unauthenticated; details[0].code gives the reason (missing_credentials, invalid_api_key, api_key_revoked, api_key_expired, session_expired, publishable_key_not_allowed).

    Error codes: unauthenticated

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    401 response: unauthenticatedJSON
    {
      "error": {
        "type": "authentication_error",
        "code": "unauthenticated",
        "message": "The API key is not valid.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#unauthenticated",
        "details": [
          {
            "path": "Authorization",
            "code": "invalid_api_key",
            "message": "No key matches this secret"
          }
        ]
      }
    }
  • 403 response

    Permitted credential but not allowed: insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, two_factor_required (also Owner/Admin writes on Business+ after the 14-day 2FA grace, details[0].grace_ends_at), step_up_required, email_verification_required, signups_paused (sign-up while the platform setting is on), write_blocked_past_due, workspace_suspended, content_policy_hold, origin_not_allowed, or a plan limit (type: plan_limit_error — feature_not_in_plan, plan_limit_reached with param naming the plan limit key). Any credentialed operation can return it: an API key on a session-only route is insufficient_scope, a session write without an allowed Origin is origin_not_allowed, and an API key whose plan has no API access is feature_not_in_plan.

    Error codes: two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, feature_not_in_plan, plan_limit_reached

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    403 response: plan limit reachedJSON
    {
      "error": {
        "type": "plan_limit_error",
        "code": "plan_limit_reached",
        "message": "You have created 2,000 of 2,000 links this billing period. Upgrade or buy a pack to continue.",
        "param": "links_per_month",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/pricing",
        "details": [
          {
            "code": "plan_limit_reached",
            "message": "Monthly link quota reached",
            "limit": 2000,
            "used": 2000,
            "resets_at": "2026-12-01T00:00:00.000Z",
            "upgrade_to": "business"
          }
        ]
      }
    }
    403 response: feature not in planJSON
    {
      "error": {
        "type": "plan_limit_error",
        "code": "feature_not_in_plan",
        "message": "The public API is available on the Pro plan and above.",
        "param": "api_requests_per_day",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/pricing",
        "details": [
          {
            "code": "feature_not_in_plan",
            "message": "Upgrade to Pro",
            "upgrade_to": "pro"
          }
        ]
      }
    }
    403 response: email verification requiredJSON
    {
      "error": {
        "type": "permission_error",
        "code": "email_verification_required",
        "message": "Verify your email address to create links.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#email_verification_required"
      }
    }
    403 response: step up requiredJSON
    {
      "error": {
        "type": "permission_error",
        "code": "step_up_required",
        "message": "Confirm your password or 2FA code to continue.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#step_up_required"
      }
    }
  • 404 response

    Unknown id, or the resource belongs to another workspace (never distinguished). Also returned when a session's X-Workspace-Id names a workspace the user is not an active member of.

    Error codes: resource_not_found

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    404 response: not foundJSON
    {
      "error": {
        "type": "not_found_error",
        "code": "resource_not_found",
        "message": "No link with id lnk_01JB4N8ZK3Q9WXYZ0123456789.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#resource_not_found"
      }
    }
  • 429 response

    Burst limit, daily plan quota or endpoint limit exceeded.

    Error codes: rate_limited

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    429 response: rate limitedJSON
    {
      "error": {
        "type": "rate_limit_error",
        "code": "rate_limited",
        "message": "Daily API quota of 50,000 requests exhausted. Resets at 00:00 UTC.",
        "param": "daily_quota",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/rate-limits"
      }
    }

A link's safety appeals

GET/links/{id}/appeals

  • Scope links:read

The link's appeals against a safety block, newest first (at most 50). The link's safety history shows them next to the scans. Readable by every role that may view the link's safety; another workspace's link is 404.

Parameters

NameInTypeDescription
idrequiredpathstringPrefixed ULID (<prefix>_<26 Crockford base32 chars>).
X-Workspace-IdheaderstringWorkspace to act on. Required for session requests on workspace-scoped routes; optional for API keys (must equal the key's workspace, otherwise 403 workspace_mismatch). For a session, a workspace the user is not an active member of (another tenant, a removed membership, a malformed or unknown id) answers 404 resource_not_found, never 403.

Request example

GET /links/{id}/appealscURL
curl 'https://api.urlcrop.com/v1/links/lnk_01JB4N8ZK3Q9WXYZ0123456789/appeals' \
  -H 'Authorization: Bearer uc_live_…'

Responses

  • 200 response

    Appeals, newest first

    SafetyAppealList schema and example
    FieldTypeDescription
    datarequiredarray of SafetyAppeal
    data[].objectrequiredstringValue: safety_appeal
    data[].idrequiredstring
    data[].workspace_idrequiredstring
    data[].targetrequiredobject
    data[].destination_hostrequiredstring | nullThe link's current destination host (never the full URL).
    data[].textrequiredstringmax length 2000
    data[].evidence_urlrequiredstring | null
    data[].statusrequiredstringupheld: the link is safe for 30 days; rejected: final for that destination.One of: open, upheld, rejected
    data[].submitted_atrequiredstringFormat: date-time
    data[].due_atrequiredstringDecision deadline, 2 business days after filing.Format: date-time
    data[].decided_atrequiredstring | nullFormat: date-time
    data[].decision_reasonrequiredstring | nullTrust & Safety's note on the decision.
    paginationrequiredPagination
    pagination.limitrequiredinteger
    pagination.has_morerequiredboolean
    pagination.next_cursorrequiredstring | nullPass as cursor to fetch the next page. null on the last page.
    pagination.totalintegerOnly with include_total=true; estimated above 10,000.
    200 response: 200 responseJSON
    {
      "data": [
        {
          "object": "safety_appeal",
          "id": "apl_01JB4NA1B2C3D4E5F6G7H8J9K0",
          "workspace_id": "string",
          "target": {
            "kind": "link",
            "id": "string"
          },
          "destination_host": "string",
          "text": "string",
          "evidence_url": "string",
          "status": "open",
          "submitted_at": "2026-12-05T10:15:30.000Z",
          "due_at": "2026-12-05T10:15:30.000Z",
          "decided_at": "2026-12-05T10:15:30.000Z",
          "decision_reason": "string"
        }
      ],
      "pagination": {
        "limit": 0,
        "has_more": false,
        "next_cursor": "string",
        "total": 0
      }
    }
    Example generated from the schema.
  • 400 response

    Malformed request (invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand). Any credentialed operation can return it: a credential in the query string (api_key, access_token) is invalid_query, and a session request without X-Workspace-Id on a workspace-scoped route is missing_header.

    Error codes: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    400 response: missing headerJSON
    {
      "error": {
        "type": "invalid_request_error",
        "code": "missing_header",
        "message": "The X-Workspace-Id header is required for session requests.",
        "param": "X-Workspace-Id",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#missing_header"
      }
    }
  • 401 response

    Missing or invalid credentials. Always code: unauthenticated; details[0].code gives the reason (missing_credentials, invalid_api_key, api_key_revoked, api_key_expired, session_expired, publishable_key_not_allowed).

    Error codes: unauthenticated

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    401 response: unauthenticatedJSON
    {
      "error": {
        "type": "authentication_error",
        "code": "unauthenticated",
        "message": "The API key is not valid.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#unauthenticated",
        "details": [
          {
            "path": "Authorization",
            "code": "invalid_api_key",
            "message": "No key matches this secret"
          }
        ]
      }
    }
  • 403 response

    Permitted credential but not allowed: insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, two_factor_required (also Owner/Admin writes on Business+ after the 14-day 2FA grace, details[0].grace_ends_at), step_up_required, email_verification_required, signups_paused (sign-up while the platform setting is on), write_blocked_past_due, workspace_suspended, content_policy_hold, origin_not_allowed, or a plan limit (type: plan_limit_error — feature_not_in_plan, plan_limit_reached with param naming the plan limit key). Any credentialed operation can return it: an API key on a session-only route is insufficient_scope, a session write without an allowed Origin is origin_not_allowed, and an API key whose plan has no API access is feature_not_in_plan.

    Error codes: two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, feature_not_in_plan, plan_limit_reached

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    403 response: plan limit reachedJSON
    {
      "error": {
        "type": "plan_limit_error",
        "code": "plan_limit_reached",
        "message": "You have created 2,000 of 2,000 links this billing period. Upgrade or buy a pack to continue.",
        "param": "links_per_month",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/pricing",
        "details": [
          {
            "code": "plan_limit_reached",
            "message": "Monthly link quota reached",
            "limit": 2000,
            "used": 2000,
            "resets_at": "2026-12-01T00:00:00.000Z",
            "upgrade_to": "business"
          }
        ]
      }
    }
    403 response: feature not in planJSON
    {
      "error": {
        "type": "plan_limit_error",
        "code": "feature_not_in_plan",
        "message": "The public API is available on the Pro plan and above.",
        "param": "api_requests_per_day",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/pricing",
        "details": [
          {
            "code": "feature_not_in_plan",
            "message": "Upgrade to Pro",
            "upgrade_to": "pro"
          }
        ]
      }
    }
    403 response: email verification requiredJSON
    {
      "error": {
        "type": "permission_error",
        "code": "email_verification_required",
        "message": "Verify your email address to create links.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#email_verification_required"
      }
    }
    403 response: step up requiredJSON
    {
      "error": {
        "type": "permission_error",
        "code": "step_up_required",
        "message": "Confirm your password or 2FA code to continue.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#step_up_required"
      }
    }
  • 404 response

    Unknown id, or the resource belongs to another workspace (never distinguished). Also returned when a session's X-Workspace-Id names a workspace the user is not an active member of.

    Error codes: resource_not_found

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    404 response: not foundJSON
    {
      "error": {
        "type": "not_found_error",
        "code": "resource_not_found",
        "message": "No link with id lnk_01JB4N8ZK3Q9WXYZ0123456789.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#resource_not_found"
      }
    }
  • 429 response

    Burst limit, daily plan quota or endpoint limit exceeded.

    Error codes: rate_limited

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    429 response: rate limitedJSON
    {
      "error": {
        "type": "rate_limit_error",
        "code": "rate_limited",
        "message": "Daily API quota of 50,000 requests exhausted. Resets at 00:00 UTC.",
        "param": "daily_quota",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/rate-limits"
      }
    }

The QR code attached to this link

GET/links/{id}/qr-code

  • Scope qr:read

Parameters

NameInTypeDescription
idrequiredpathstringPrefixed ULID (<prefix>_<26 Crockford base32 chars>).
X-Workspace-IdheaderstringWorkspace to act on. Required for session requests on workspace-scoped routes; optional for API keys (must equal the key's workspace, otherwise 403 workspace_mismatch). For a session, a workspace the user is not an active member of (another tenant, a removed membership, a malformed or unknown id) answers 404 resource_not_found, never 403.

Request example

GET /links/{id}/qr-codecURL
curl 'https://api.urlcrop.com/v1/links/lnk_01JB4N8ZK3Q9WXYZ0123456789/qr-code' \
  -H 'Authorization: Bearer uc_live_…'

Responses

  • 200 response

    QR code

    QrCode schema and example
    FieldTypeDescription
    idrequiredstring
    objectrequiredstringValue: qr_code
    workspace_idrequiredstring
    is_testrequiredboolean
    namerequiredstringmax length 120
    typerequiredstringMVP types. Phase 2 adds google_review, menu, app_store, sms, email (until then 422 unsupported_qr_type). wifi is static only.One of: url, whatsapp, upi, vcard, wifi
    dynamicrequiredbooleanDynamic codes encode a short URL and are editable and tracked; static codes encode the payload itself.
    link_idstring | nullThe Link a dynamic code points to.
    short_urlstring | nullFormat: uri
    contentstringThe exact string encoded in the QR (short URL for dynamic, payload string for static).
    payloadQrPayloadUrl | QrPayloadWhatsapp | QrPayloadUpi | QrPayloadVcard | QrPayloadWifiShape depends on type.
    designrequiredQrDesignDesign, schema version 1. Basic kit (every plan): colours, module_shape square/rounded/dots, eye_shape: square, error_correction. Full kit (qr_brand_kit = full, Starter+; else 403 feature_not_in_plan, param: qr_brand_kit): logo, gradient, eye_color, eye_shape rounded/circle/leaf, module_shape: classy, frame. A design that fails the server decode test is saved, but its renders answer 422 qr_unreadable.
    design.foreground_colorstring
    design.background_colorstring
    design.gradientobject | null
    design.gradient.typestringOne of: linear, radial
    design.gradient.fromstring
    design.gradient.tostring
    design.gradient.angleinteger
    design.module_shapestringclassy needs the full kit.One of: square, rounded, dots, classy
    design.eye_shapestringAnything other than square needs the full kit.One of: square, rounded, circle, leaf
    design.eye_colorstring | nullFull kit.
    design.logoobject | null
    design.logo.asset_idstringFrom POST /assets with purpose: qr_logo (PNG, JPEG or WebP).
    design.logo.size_rationumber≥ 0.1, ≤ 0.3
    design.logo.paddinginteger
    design.logo.remove_backgroundboolean
    design.logo.urlstring15-minute signed /v1/files/… preview of the logo (responses only; ignored on writes).Format: uri
    design.frameobject | null
    design.frame.stylestringFrame styles.One of: none, label, badge, speech, card, crop_corner
    design.frame.textstringCTA text, 1–24 characters (presets "Scan me", "Pay here").min length 1, max length 24
    design.frame.colorstring
    design.frame.text_colorstring
    design.error_correctionstringWith a logo the level is raised to at least Q (logo.size_ratio ≤ 0.2) or H (above); a lower requested level is raised, a higher one kept. Responses return the effective level.One of: L, M, Q, H
    render_tokenrequiredstringRotates whenever design or payload changes, so cached renders are never stale.
    render_urlsrequiredobject | nullPublic, cacheable, unguessable render URLs (Cache-Control: public, immutable). null for wifi codes: their renders contain the network password, are stored encrypted and are served only by the authenticated /v1/qr-codes/{id}.{png,svg,pdf} routes with Cache-Control: private, no-store.
    render_urls.pngrequiredstringFormat: uri
    render_urls.svgrequiredstringFormat: uri
    render_urls.pdfrequiredstringFormat: uri
    campaign_idstring | null
    tag_idsarray of string
    scansrequiredinteger
    last_scanned_atstring | nullTime of the most recent counted scan (flushed by ingest), or null.Format: date-time
    metadataMetadataUp to 20 keys (≤ 40 chars) with string values (≤ 500 chars). Stored verbatim.max keys 20
    created_atrequiredstringISO 8601 UTC with milliseconds.Format: date-time
    updated_atrequiredstringISO 8601 UTC with milliseconds.Format: date-time
    200 response: 200 responseJSON
    {
      "id": "qr_01JB4P0Q1R2S3T4U5V6W7X8Y9Z",
      "object": "qr_code",
      "workspace_id": "ws_01JB4M0A1B2C3D4E5F6G7H8J9K",
      "is_test": false,
      "name": "string",
      "type": "url",
      "dynamic": false,
      "link_id": "string",
      "short_url": "https://example.com",
      "content": "string",
      "payload": {
        "url": "https://example.com"
      },
      "design": {
        "foreground_color": "#000000",
        "background_color": "#FFFFFF",
        "gradient": {
          "type": "linear",
          "from": "string",
          "to": "string",
          "angle": 0
        },
        "module_shape": "square",
        "eye_shape": "square",
        "eye_color": "string",
        "logo": {
          "asset_id": "string",
          "size_ratio": 0.2,
          "padding": 2,
          "remove_background": true,
          "url": "https://example.com"
        },
        "frame": {
          "style": "none",
          "text": "Scan me",
          "color": "string",
          "text_color": "string"
        },
        "error_correction": "M"
      },
      "render_token": "qrt_01JB4Q0R1S2T3U4V5W6X7Y8Z9A",
      "render_urls": {
        "png": "https://api.urlcrop.com/v1/public/qr/qrt_01JB4Q0R1S2T3U4V5W6X7Y8Z9A.png?size=1024",
        "svg": "https://example.com",
        "pdf": "https://example.com"
      },
      "campaign_id": "string",
      "tag_ids": [
        "string"
      ],
      "scans": 0,
      "last_scanned_at": "2026-12-05T10:15:30.000Z",
      "metadata": {},
      "created_at": "2026-12-05T10:15:30.000Z",
      "updated_at": "2026-12-05T10:15:30.000Z"
    }
    Example generated from the schema.
  • 400 response

    Malformed request (invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand). Any credentialed operation can return it: a credential in the query string (api_key, access_token) is invalid_query, and a session request without X-Workspace-Id on a workspace-scoped route is missing_header.

    Error codes: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    400 response: missing headerJSON
    {
      "error": {
        "type": "invalid_request_error",
        "code": "missing_header",
        "message": "The X-Workspace-Id header is required for session requests.",
        "param": "X-Workspace-Id",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#missing_header"
      }
    }
  • 401 response

    Missing or invalid credentials. Always code: unauthenticated; details[0].code gives the reason (missing_credentials, invalid_api_key, api_key_revoked, api_key_expired, session_expired, publishable_key_not_allowed).

    Error codes: unauthenticated

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    401 response: unauthenticatedJSON
    {
      "error": {
        "type": "authentication_error",
        "code": "unauthenticated",
        "message": "The API key is not valid.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#unauthenticated",
        "details": [
          {
            "path": "Authorization",
            "code": "invalid_api_key",
            "message": "No key matches this secret"
          }
        ]
      }
    }
  • 403 response

    Permitted credential but not allowed: insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, two_factor_required (also Owner/Admin writes on Business+ after the 14-day 2FA grace, details[0].grace_ends_at), step_up_required, email_verification_required, signups_paused (sign-up while the platform setting is on), write_blocked_past_due, workspace_suspended, content_policy_hold, origin_not_allowed, or a plan limit (type: plan_limit_error — feature_not_in_plan, plan_limit_reached with param naming the plan limit key). Any credentialed operation can return it: an API key on a session-only route is insufficient_scope, a session write without an allowed Origin is origin_not_allowed, and an API key whose plan has no API access is feature_not_in_plan.

    Error codes: two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, feature_not_in_plan, plan_limit_reached

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    403 response: plan limit reachedJSON
    {
      "error": {
        "type": "plan_limit_error",
        "code": "plan_limit_reached",
        "message": "You have created 2,000 of 2,000 links this billing period. Upgrade or buy a pack to continue.",
        "param": "links_per_month",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/pricing",
        "details": [
          {
            "code": "plan_limit_reached",
            "message": "Monthly link quota reached",
            "limit": 2000,
            "used": 2000,
            "resets_at": "2026-12-01T00:00:00.000Z",
            "upgrade_to": "business"
          }
        ]
      }
    }
    403 response: feature not in planJSON
    {
      "error": {
        "type": "plan_limit_error",
        "code": "feature_not_in_plan",
        "message": "The public API is available on the Pro plan and above.",
        "param": "api_requests_per_day",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/pricing",
        "details": [
          {
            "code": "feature_not_in_plan",
            "message": "Upgrade to Pro",
            "upgrade_to": "pro"
          }
        ]
      }
    }
    403 response: email verification requiredJSON
    {
      "error": {
        "type": "permission_error",
        "code": "email_verification_required",
        "message": "Verify your email address to create links.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#email_verification_required"
      }
    }
    403 response: step up requiredJSON
    {
      "error": {
        "type": "permission_error",
        "code": "step_up_required",
        "message": "Confirm your password or 2FA code to continue.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#step_up_required"
      }
    }
  • 404 response

    Unknown id, or the resource belongs to another workspace (never distinguished). Also returned when a session's X-Workspace-Id names a workspace the user is not an active member of.

    Error codes: resource_not_found

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    404 response: not foundJSON
    {
      "error": {
        "type": "not_found_error",
        "code": "resource_not_found",
        "message": "No link with id lnk_01JB4N8ZK3Q9WXYZ0123456789.",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/errors#resource_not_found"
      }
    }
  • 429 response

    Burst limit, daily plan quota or endpoint limit exceeded.

    Error codes: rate_limited

    Error schema and example
    FieldTypeDescription
    errorrequiredobject
    error.typerequiredstringOne of: invalid_request_error, authentication_error, permission_error, plan_limit_error, not_found_error, conflict_error, validation_error, rate_limit_error, api_error
    error.coderequiredstringComplete error-code catalogue. @urlcrop/shared/errors is tested against this list. param on plan_limit_error responses is always a plan limit key.One of: invalid_json, invalid_query, missing_header, invalid_cursor, invalid_sort, invalid_expand, unauthenticated, two_factor_required, step_up_required, email_verification_required, signups_paused, insufficient_scope, insufficient_role, workspace_mismatch, wrong_environment, workspace_suspended, write_blocked_past_due, content_policy_hold, origin_not_allowed, turnstile_failed, second_approver_required, view_as_read_only, support_access_approval_required, feature_not_in_plan, plan_limit_reached, resource_not_found, route_not_found, slug_taken, domain_already_exists, member_already_exists, invitation_pending, idempotency_in_progress, state_conflict, resource_in_use, payload_too_large, unsupported_media_type, validation_failed, destination_invalid, destination_blocked, destination_unreachable, slug_invalid, slug_reserved, domain_not_active, domain_proxied, idempotency_key_reused, unsupported_qr_type, qr_unreadable, invalid_routing_rule, invalid_currency, csv_invalid, attribution_key_missing, policy_acceptance_required, rate_limited, internal_error, upstream_unavailable, service_unavailable, timeout
    error.messagerequiredstringHuman-readable, safe to show to end users.
    error.paramstringField, header or scope the error refers to, when applicable.
    error.request_idrequiredstring
    error.doc_urlrequiredstringFormat: uri
    error.detailsarray of object
    error.details[].pathstring
    error.details[].coderequiredstringZod issue code, unsupported_in_phase, a destination safety reason, or a 401 reason.
    error.details[].messagerequiredstring
    error.details[].limitinteger | nullPlan limit in force (plan_limit_error only; null = unlimited).
    error.details[].usedintegerCurrent usage (plan_limit_error only).
    error.details[].resets_atstringWhen a periodic limit resets (plan_limit_error only).Format: date-time
    error.details[].upgrade_tostringPlan slug that lifts a plan limit (plan_limit_error only).
    429 response: rate limitedJSON
    {
      "error": {
        "type": "rate_limit_error",
        "code": "rate_limited",
        "message": "Daily API quota of 50,000 requests exhausted. Resets at 00:00 UTC.",
        "param": "daily_quota",
        "request_id": "req_01JB4N8ZK3Q9WXYZ0123456789",
        "doc_url": "https://urlcrop.com/docs/api/rate-limits"
      }
    }