Skip to content

QR codes

QR codes endpoints of the URLCrop API: 9 operations. Dynamic QR codes (editable, tracked) and static QR codes, with design and cached renders.

List QR codes

GET/qr-codes

  • Scope qr: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).
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).
typequerystringMVP 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
dynamicqueryboolean
campaign_idquerystring
tag_idsquerystring
qquerystring

Request example

GET /qr-codescURL
curl 'https://api.urlcrop.com/v1/qr-codes' \
  -H 'Authorization: Bearer uc_live_…'

Responses

  • 200 response

    QR codes

    QrCodeList schema and example
    FieldTypeDescription
    datarequiredarray of QrCode
    data[].idrequiredstring
    data[].objectrequiredstringValue: qr_code
    data[].workspace_idrequiredstring
    data[].is_testrequiredboolean
    data[].namerequiredstringmax length 120
    data[].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
    data[].dynamicrequiredbooleanDynamic codes encode a short URL and are editable and tracked; static codes encode the payload itself.
    data[].link_idstring | nullThe Link a dynamic code points to.
    data[].short_urlstring | nullFormat: uri
    data[].contentstringThe exact string encoded in the QR (short URL for dynamic, payload string for static).
    data[].payloadQrPayloadUrl | QrPayloadWhatsapp | QrPayloadUpi | QrPayloadVcard | QrPayloadWifiShape depends on type.
    data[].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.
    data[].design.foreground_colorstring
    data[].design.background_colorstring
    data[].design.gradientobject | null
    data[].design.module_shapestringclassy needs the full kit.One of: square, rounded, dots, classy
    data[].design.eye_shapestringAnything other than square needs the full kit.One of: square, rounded, circle, leaf
    data[].design.eye_colorstring | nullFull kit.
    data[].design.logoobject | null
    data[].design.frameobject | null
    data[].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
    data[].render_tokenrequiredstringRotates whenever design or payload changes, so cached renders are never stale.
    data[].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.
    data[].render_urls.pngrequiredstringFormat: uri
    data[].render_urls.svgrequiredstringFormat: uri
    data[].render_urls.pdfrequiredstringFormat: uri
    data[].campaign_idstring | null
    data[].tag_idsarray of string
    data[].scansrequiredinteger
    data[].last_scanned_atstring | nullTime of the most recent counted scan (flushed by ingest), or null.Format: date-time
    data[].metadataMetadataUp to 20 keys (≤ 40 chars) with string values (≤ 500 chars). Stored verbatim.max keys 20
    data[].created_atrequiredstringISO 8601 UTC with milliseconds.Format: date-time
    data[].updated_atrequiredstringISO 8601 UTC with milliseconds.Format: 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": "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"
        }
      ],
      "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"
      }
    }

Create a QR code

POST/qr-codes

  • Scope qr:write

Dynamic codes (default) create a Link — or reuse link_id — and encode its short URL, so the destination stays editable and every scan is tracked. Static codes encode the payload directly and are neither tracked nor editable (wifi is static only). Every saved QR code, static or dynamic, counts toward the plan's QR quota (403 plan_limit_reached, param: dynamic_qr_total); a dynamic code's link (created_via: qr) also counts toward links_per_month. Reusing a link_id that already has a live QR code → 409 state_conflict (param: link_id). Dynamic upi and vcard codes open a payee confirmation page / contact card page when scanned. A vCard website goes through the same safety fast path as a link destination (blocked → 422 destination_blocked, not allowed → 422 destination_invalid, param: payload.website). Needs a verified email (403 email_verification_required). The decode test (512 px raster) runs at render time, not on save: creation enqueues a background render of the default PNG, and a render of a design that fails it answers 422 qr_unreadable (the saved code is kept).

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
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
payloadrequiredQrPayloadUrl | QrPayloadWhatsapp | QrPayloadUpi | QrPayloadVcard | QrPayloadWifiShape depends on type.
namestringmax length 120
designQrDesignDesign, 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
dynamicboolean
domainstringDynamic only. Domain for the generated short URL.
slugstring1–64 chars of [A-Za-z0-9_-]; must not start with @.
link_idstringDynamic url type only — attach to an existing link instead of creating one.
campaign_idstring
tag_idsarray of string
metadataMetadataUp to 20 keys (≤ 40 chars) with string values (≤ 500 chars). Stored verbatim.max keys 20
Request body: Branded UPI payment QR for a shop counterJSON
{
  "type": "upi",
  "name": "Counter 1 — UPI",
  "payload": {
    "vpa": "brand@upi",
    "payee_name": "Brand India Pvt Ltd"
  },
  "design": {
    "foreground_color": "#1A1A2E",
    "module_shape": "rounded",
    "eye_shape": "rounded",
    "logo": {
      "asset_id": "ast_01JB4PA1B2C3D4E5F6G7H8J9K0"
    },
    "frame": {
      "style": "label",
      "text": "Pay here"
    }
  }
}
Request body: whatsappJSON
{
  "type": "whatsapp",
  "name": "Menu card — WhatsApp",
  "payload": {
    "phone": "+919876543210",
    "message": "Hi! I'd like to order."
  },
  "slug": "wa-menu"
}

Request example

POST /qr-codescURL
curl -X POST 'https://api.urlcrop.com/v1/qr-codes' \
  -H 'Authorization: Bearer uc_live_…' \
  -H 'Content-Type: application/json' \
  -d '{
  "type": "upi",
  "name": "Counter 1 — UPI",
  "payload": {
    "vpa": "brand@upi",
    "payee_name": "Brand India Pvt Ltd"
  },
  "design": {
    "foreground_color": "#1A1A2E",
    "module_shape": "rounded",
    "eye_shape": "rounded",
    "logo": {
      "asset_id": "ast_01JB4PA1B2C3D4E5F6G7H8J9K0"
    },
    "frame": {
      "style": "label",
      "text": "Pay here"
    }
  }
}'

Responses

  • 201 response

    QR code created

    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
    201 response: createdJSON
    {
      "id": "qr_01JB4P0Q1R2S3T4U5V6W7X8Y9Z",
      "object": "qr_code",
      "workspace_id": "ws_01JB4M0A1B2C3D4E5F6G7H8J9K",
      "is_test": false,
      "name": "Counter 1 — UPI",
      "type": "upi",
      "dynamic": true,
      "link_id": "lnk_01JB4PT8U9V0W1X2Y3Z4A5B6C7",
      "short_url": "https://crop.sh/k7Qp3mX",
      "content": "https://crop.sh/k7Qp3mX",
      "payload": {
        "vpa": "brand@upi",
        "payee_name": "Brand India Pvt Ltd",
        "amount": null,
        "note": null
      },
      "design": {
        "foreground_color": "#1A1A2E",
        "background_color": "#FFFFFF",
        "module_shape": "rounded",
        "eye_shape": "rounded",
        "logo": {
          "asset_id": "ast_01JB4PA1B2C3D4E5F6G7H8J9K0",
          "size_ratio": 0.2,
          "padding": 2,
          "remove_background": true
        },
        "frame": {
          "style": "label",
          "text": "Pay here"
        },
        "error_correction": "H"
      },
      "render_token": "qrt_01JB4Q0R1S2T3U4V5W6X7Y8Z9A",
      "render_urls": {
        "png": "https://api.urlcrop.com/v1/public/qr/qrt_01JB4Q0R1S2T3U4V5W6X7Y8Z9A.png?size=1024",
        "svg": "https://api.urlcrop.com/v1/public/qr/qrt_01JB4Q0R1S2T3U4V5W6X7Y8Z9A.svg",
        "pdf": "https://api.urlcrop.com/v1/public/qr/qrt_01JB4Q0R1S2T3U4V5W6X7Y8Z9A.pdf"
      },
      "campaign_id": null,
      "tag_ids": [],
      "scans": 0,
      "metadata": {},
      "created_at": "2026-12-05T10:20:00.000Z",
      "updated_at": "2026-12-05T10:20:00.000Z"
    }
  • 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"
      }
    }

Get a QR code

GET/qr-codes/{id}

  • 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.
expand[]queryarray of stringRelated objects to embed (depth 1). Allowed values are listed per resource (x-expandable).

Request example

GET /qr-codes/{id}cURL
curl 'https://api.urlcrop.com/v1/qr-codes/lnk_01JB4N8ZK3Q9WXYZ0123456789' \
  -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"
      }
    }

Update name, design or payload

PATCH/qr-codes/{id}

  • Scope qr:write

Any design or payload change rotates render_token; printed codes keep working because they encode the short URL. payload is editable on dynamic codes only (it moves the link destination; a vCard website is safety-checked as on create). A static code encodes its payload directly, so sending a different payload → 422 validation_failed with param: payload and details[].code: immutable_static_payload; resending the stored payload is no change. A Wi-Fi payload without password keeps the stored (encrypted) password. Every design or payload change enqueues a background render of the default PNG; the decode test runs at render time, so a design that fails it is saved and its renders answer 422 qr_unreadable. Every check that can refuse (design and full-kit gate, logo asset, campaign_id, tag_ids, payload, vCard website safety) runs before the link destination moves, so a refused PATCH changes nothing. The link update and the QR write are two transactions; a logo asset deleted between them answers 422 on design.logo.asset_id with the link already moved, and a retry converges.

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
namestring
designQrDesignDesign, 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
payloadQrPayloadUrl | QrPayloadWhatsapp | QrPayloadUpi | QrPayloadVcard | QrPayloadWifiShape depends on type.
campaign_idstring | null
tag_idsarray of string
metadataMetadataUp to 20 keys (≤ 40 chars) with string values (≤ 500 chars). Stored verbatim.max keys 20
Request body: Request bodyJSON
{
  "name": "string",
  "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"
  },
  "payload": {
    "url": "https://example.com"
  },
  "campaign_id": "string",
  "tag_ids": [
    "string"
  ],
  "metadata": {}
}
Example generated from the schema.

Request example

PATCH /qr-codes/{id}cURL
curl -X PATCH 'https://api.urlcrop.com/v1/qr-codes/lnk_01JB4N8ZK3Q9WXYZ0123456789' \
  -H 'Authorization: Bearer uc_live_…' \
  -H 'Content-Type: application/json' \
  -d '{
  "name": "string",
  "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"
  },
  "payload": {
    "url": "https://example.com"
  },
  "campaign_id": "string",
  "tag_ids": [
    "string"
  ],
  "metadata": {}
}'

Responses

  • 200 response

    Updated

    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"
      }
    }
  • 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"
      }
    }

Delete a QR code

DELETE/qr-codes/{id}

  • Scope qr:write

Soft-deletes the code and frees its quota slot; its public renders answer 410. For a static vcard or upi code (the image embeds the contact or payee details) the API also enqueues a purge of its seven public render URLs (.png, .png?size=512|1024|2048|4096, .svg, .pdf) from the Cloudflare edge cache.

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.
delete_linkquerybooleanAlso archive the underlying link. By default the link is kept and keeps redirecting (scans of a printed code then count as plain clicks). The QR is soft-deleted, frees its quota slot and its public renders answer 410.

Request example

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

Responses

  • 204 response

    Deleted or applied. No body.

  • 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"
      }
    }

Render PNG (authenticated)

GET/qr-codes/{id}.png

  • Scope qr:read

Private render with ETag and Cache-Control: private, max-age=3600 (private, no-store for wifi codes, whose image contains the password); supports If-None-Match → 304. Test-mode codes are not rendered (409 state_conflict). Any QrDesign field may be passed as a query override (e.g. foreground_color=%23000000). For embedding and print-shop downloads prefer the public cached render_urls. Free plan renders carry the "Made with URLCrop" badge.

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.
sizequeryintegerPNG width in pixels; only the preset sizes are rendered (any other value is 400 invalid_query).One of: 512, 1024, 2048, 4096
marginqueryintegerQuiet zone in modules.

Request example

GET /qr-codes/{id}.pngcURL
curl 'https://api.urlcrop.com/v1/qr-codes/lnk_01JB4N8ZK3Q9WXYZ0123456789.png' \
  -H 'Authorization: Bearer uc_live_…'

Responses

  • 200 response

    PNG image

    Body schema and example
    200 response: 200 responseJSON
    "string"
    Example generated from the schema.
  • 304 response

    Not modified

  • 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

    Test-mode QR codes are not rendered (state_conflict).

    Error codes: invalid_json, state_conflict

    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: 409 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.
  • 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"
      }
    }
  • 503 response

    The QR render was not ready within 8 s (service_unavailable); a qr.render job is running. Retry after Retry-After seconds (2). Sent with Cache-Control: no-store.

    Error codes: invalid_json, service_unavailable

    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).
    503 response: 503 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.

Render SVG (authenticated)

GET/qr-codes/{id}.svg

  • 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.
marginqueryinteger

Request example

GET /qr-codes/{id}.svgcURL
curl 'https://api.urlcrop.com/v1/qr-codes/lnk_01JB4N8ZK3Q9WXYZ0123456789.svg' \
  -H 'Authorization: Bearer uc_live_…'

Responses

  • 200 response

    SVG image

    Body schema and example
    200 response: 200 responseJSON
    "string"
    Example generated from the schema.
  • 304 response

    Not modified

  • 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

    Test-mode QR codes are not rendered (state_conflict).

    Error codes: invalid_json, state_conflict

    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: 409 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.
  • 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"
      }
    }
  • 503 response

    The QR render was not ready within 8 s (service_unavailable); a qr.render job is running. Retry after Retry-After seconds (2). Sent with Cache-Control: no-store.

    Error codes: invalid_json, service_unavailable

    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).
    503 response: 503 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.

Render print-ready PDF (authenticated)

GET/qr-codes/{id}.pdf

  • 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.
pagequerystringOne of: a4, letter, label-50mm
bleed_mmqueryintegerBleed with crop marks when > 0.

Request example

GET /qr-codes/{id}.pdfcURL
curl 'https://api.urlcrop.com/v1/qr-codes/lnk_01JB4N8ZK3Q9WXYZ0123456789.pdf' \
  -H 'Authorization: Bearer uc_live_…'

Responses

  • 200 response

    PDF

    Body schema and example
    200 response: 200 responseJSON
    "string"
    Example generated from the schema.
  • 304 response

    Not modified

  • 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

    Test-mode QR codes are not rendered (state_conflict).

    Error codes: invalid_json, state_conflict

    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: 409 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.
  • 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"
      }
    }
  • 503 response

    The QR render was not ready within 8 s (service_unavailable); a qr.render job is running. Retry after Retry-After seconds (2). Sent with Cache-Control: no-store.

    Error codes: invalid_json, service_unavailable

    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).
    503 response: 503 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.

Scan stats for one QR code

GET/qr-codes/{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 /qr-codes/{id}/statscURL
curl 'https://api.urlcrop.com/v1/qr-codes/lnk_01JB4N8ZK3Q9WXYZ0123456789/stats' \
  -H 'Authorization: Bearer uc_live_…'

Responses

  • 200 response

    Stats (scan events only)

    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: 200 responseJSON
    {
      "object": "stats",
      "subject_id": "lnk_01JB4N8ZK3Q9WXYZ0123456789",
      "period": {
        "from": "2026-12-05T10:15:30.000Z",
        "to": "2026-12-05T10:15:30.000Z",
        "timezone": "string"
      },
      "truncated_to_retention": false,
      "totals": {
        "clicks": 0,
        "scans": 0,
        "views": 0,
        "unique_visitors": 0,
        "over_quota_events": 0,
        "conversions": 0,
        "revenue": {
          "amount": 149900,
          "currency": "INR"
        }
      },
      "timeseries": [
        {
          "date": "2026-12-05",
          "clicks": 0,
          "scans": 0,
          "views": 0,
          "unique_visitors": 0
        }
      ],
      "top": {
        "countries": [
          {
            "key": "IN",
            "label": "India",
            "value": 0,
            "share": 0
          }
        ],
        "devices": [
          {
            "key": "IN",
            "label": "India",
            "value": 0,
            "share": 0
          }
        ],
        "referrers": [
          {
            "key": "IN",
            "label": "India",
            "value": 0,
            "share": 0
          }
        ]
      }
    }
    Example generated from the schema.
  • 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"
      }
    }