# xQR Platform — Complete API Documentation > Build with QR codes, short links, analytics, and link-in-bio pages using the xQR API. --- # Getting Started ## Base URL All API requests use: `https://xqr.co/api/v1` The API is versioned via the URL path. Current stable version: v1. ## Authentication All requests require a Bearer token in the Authorization header: ``` Authorization: Bearer xqr_pk_{prefix}.{secret} ``` - **Prefix** (8 hex chars): used for key lookup, safe to log - **Secret** (64 URL-safe chars): the credential, never log - Keys are SHA-256 hashed server-side; plaintext is never stored - Create keys at Settings → Developers in the xQR dashboard ### Scopes | Scope | Description | |-------|-------------| | `*` | Full access (all scopes) | | `links:read` | List and view short links | | `links:write` | Create, update, delete short links | | `qr:read` | List and view QR templates | | `qr:write` | Create, update, delete QR templates | | `qr:render` | Generate QR code images | | `analytics:read` | Query analytics data | | `assets:read` | List and view assets | | `assets:write` | Upload, publish, delete assets | | `bio:read` | List and view Link-in-Bio pages | | `bio:write` | Create, update, publish, delete bio pages | | `reports:read` | View reports | | `reports:write` | Create and manage reports | | `workspace:read` | View workspace info, usage, and members | | `webhooks:read` | List and view webhook endpoints | | `webhooks:write` | Create, update, delete webhooks | ### Plan Requirements | Plan | API Keys | Monthly API Calls | Burst (req/s) | |------|----------|-------------------|---------------| | Free | 0 | 0 | — | | Pro | 2 | 1,000 | 10 | | Business | 10 | 25,000 | 50 | | Enterprise | Unlimited | 100,000 | 200 | ## Response Envelope ### Success ```json { "data": { ... }, "meta": { "request_id": "req_abc123", "rate_limit": { "limit": 600, "remaining": 599, "reset": 1742572800 } } } ``` ### Error ```json { "error": { "code": "INSUFFICIENT_SCOPE", "message": "This endpoint requires the links:write scope.", "status": 403 }, "meta": { "request_id": "req_abc123" } } ``` ## Pagination List endpoints accept `limit` (1–200, default 50) and `offset` (default 0) query parameters. Response meta includes `total`, `limit`, `offset`. ## Rate Limit Headers | Header | Description | |--------|-------------| | `X-RateLimit-Limit` | Monthly quota total | | `X-RateLimit-Remaining` | Monthly calls remaining | | `X-RateLimit-Burst-Limit` | Max requests per second | | `X-RateLimit-Burst-Remaining` | Burst tokens remaining | | `Retry-After` | Seconds to wait (on 429) | ## Error Codes | Code | Status | Description | Fix | |------|--------|-------------|-----| | `INVALID_API_KEY` | 401 | Key is malformed or not found | Check key format: `xqr_pk_{prefix}.{secret}` | | `REVOKED_API_KEY` | 401 | Key has been revoked | Create a new key in dashboard | | `EXPIRED_API_KEY` | 401 | Key has expired | Create or rotate key | | `INSUFFICIENT_SCOPE` | 403 | Key lacks required scope | Add scope to key or create new key with scope | | `FEATURE_NOT_AVAILABLE` | 403 | Plan doesn't include feature | Upgrade plan | | `PLAN_LIMIT_EXCEEDED` | 403 | Resource limit reached | Delete resources or upgrade plan | | `VALIDATION_ERROR` | 400 | Invalid request body | Check field types and constraints | | `SLUG_TAKEN` | 409 | Custom slug already in use | Choose different slug | | `RATE_LIMIT_EXCEEDED` | 429 | Monthly quota exhausted | Wait for billing cycle reset or upgrade | | `BURST_LIMIT_EXCEEDED` | 429 | Too many requests/second | Add backoff, check Retry-After header | | `RESOURCE_NOT_FOUND` | 404 | Resource doesn't exist | Verify resource ID | | `INTERNAL_ERROR` | 500 | Unexpected server error | Retry with exponential backoff; contact support with request_id | --- # API Reference — Links ## POST /v1/links — Create Link **Scope:** `links:write` Request body: | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `url` | string | Yes | Destination URL (HTTP/HTTPS) | | `custom_slug` | string | No | Custom short code (3–64 chars, alphanumeric + hyphens) | | `labels` | string[] | No | Tags for organization (max 10) | | `routing_rules` | object[] | No | Conditional routing rules | | `expires_at` | string | No | ISO 8601 expiration datetime | Response: `201 Created` ```json { "data": { "id": "uuid", "short_url": "https://xqr.co/slug", "short_code": "slug", "destination": "https://example.com", "enabled": true, "labels": [], "routing_rules": [], "scan_count": 0, "last_scan_at": null, "expires_at": null, "created_at": "ISO8601", "updated_at": "ISO8601" } } ``` ## GET /v1/links — List Links **Scope:** `links:read` Query parameters: `limit`, `offset`, `label` (filter), `search` (destination URL search) Response: `200 OK` with array of link objects in `data`, pagination in `meta`. ## GET /v1/links/{link_id} — Get Link **Scope:** `links:read` Path: `link_id` (UUID) Response: `200 OK` with link object. ## PATCH /v1/links/{link_id} — Update Link **Scope:** `links:write` Request body (all optional): `url`, `custom_slug`, `labels`, `routing_rules`, `enabled`, `expires_at` Response: `200 OK` with updated link object. ## DELETE /v1/links/{link_id} — Delete Link **Scope:** `links:write` Response: `204 No Content` ## POST /v1/links/bulk — Bulk Create Links **Scope:** `links:write` Request body: | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `links` | object[] | Yes | Array of link objects (max 100) | Each link object: `{ url, custom_slug?, labels? }` Response: `201 Created` with `data.created` (array) and `data.errors` (array). ## GET /v1/links/bulk/{batch_id} — Bulk Status **Scope:** `links:read` Response: `200 OK` with batch status, progress, and results. ## GET /v1/links/{link_id}/qr — Get Link QR Code **Scope:** `qr:render` Query parameters: `size` (1–50), `format` (`svg`|`png`), `foreground`, `background` Response: Binary image (`image/svg+xml` or `image/png`). --- # API Reference — QR Codes ## POST /v1/qr/templates — Create Template **Scope:** `qr:write` Request body: | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `name` | string | Yes | Template name | | `description` | string | No | Template description | | `design` | object | No | Design config: foreground, background, error_correction, logo_url, dot_style, corner_style | Response: `201 Created` with template object. ## GET /v1/qr/templates — List Templates **Scope:** `qr:read` Query parameters: `limit`, `offset` ## GET /v1/qr/templates/{template_id} — Get Template **Scope:** `qr:read` ## PATCH /v1/qr/templates/{template_id} — Update Template **Scope:** `qr:write` ## DELETE /v1/qr/templates/{template_id} — Delete Template **Scope:** `qr:write` Response: `204 No Content` ## POST /v1/qr/generate — Generate QR Code **Scope:** `qr:render` Request body: | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `content` | string | Yes | Text or URL to encode | | `size` | number | No | Size 1–50 (default 10) | | `format` | string | No | `png` or `svg` (default `svg`) | | `error_correction` | string | No | `L`, `M`, `Q`, or `H` | | `foreground` | string | No | Hex color | | `background` | string | No | Hex color | Response: Binary image. ## POST /v1/qr/templates/{template_id}/render — Render Template **Scope:** `qr:render` Request body: `content` (required), `size` (optional) Response: Binary image using template's design settings. --- # API Reference — Analytics ## GET /v1/analytics/summary — Summary **Scope:** `analytics:read` Query parameters: `period` (`7d`, `30d`, `90d`, `all`) Response: Total scans, unique visitors, top countries, top devices, trend data. ## GET /v1/analytics/timeseries — Timeseries **Scope:** `analytics:read` Query parameters: `period`, `granularity` (`hour`|`day`), `link_id` Response: Array of `{ timestamp, scans }` data points. ## GET /v1/analytics/geography — Geography **Scope:** `analytics:read` Query parameters: `period`, `link_id` Response: Array of `{ country, country_code, scans, percentage }`. ## GET /v1/analytics/devices — Devices **Scope:** `analytics:read` Query parameters: `period`, `link_id` Response: Breakdown by device type (mobile, desktop, tablet), browser, OS. ## GET /v1/analytics/top-links — Top Links **Scope:** `analytics:read` Query parameters: `period`, `limit` Response: Array of link objects sorted by scan count. ## GET /v1/analytics/links/{link_id} — Link Stats **Scope:** `analytics:read` Query parameters: `period` Response: Detailed analytics for a specific link including timeseries, geography, devices. --- # API Reference — Link-in-Bio ## POST /v1/bio/pages — Create Page **Scope:** `bio:write` Request body: | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `title` | string | Yes | Page title | | `slug` | string | No | Custom URL slug | | `bio` | string | No | Bio text | | `avatar_url` | string | No | Avatar image URL | | `theme` | string | No | Theme name | | `links` | object[] | No | Array of `{ title, url, icon? }` | Response: `201 Created` ## GET /v1/bio/pages — List Pages **Scope:** `bio:read` ## GET /v1/bio/pages/{page_id} — Get Page **Scope:** `bio:read` ## PATCH /v1/bio/pages/{page_id} — Update Page **Scope:** `bio:write` ## DELETE /v1/bio/pages/{page_id} — Delete Page **Scope:** `bio:write` ## POST /v1/bio/pages/{page_id}/publish — Publish Page **Scope:** `bio:write` Makes the page publicly accessible at `https://xqr.bio/{slug}`. ## POST /v1/bio/pages/{page_id}/unpublish — Unpublish Page **Scope:** `bio:write` ## GET /v1/bio/pages/{page_id}/stats — Page Stats **Scope:** `analytics:read` Response: Page views, unique visitors, link clicks. --- # API Reference — Assets ## POST /v1/assets/presign — Presign Upload **Scope:** `assets:write` Request body: `filename` (required), `content_type` (required) Response: `presigned_url` (PUT to upload), `asset_id` ## POST /v1/assets/{asset_id}/confirm — Confirm Upload **Scope:** `assets:write` Call after uploading to presigned URL. ## GET /v1/assets/{asset_id} — Get Asset **Scope:** `assets:read` ## GET /v1/assets — List Assets **Scope:** `assets:read` ## DELETE /v1/assets/{asset_id} — Delete Asset **Scope:** `assets:write` ## POST /v1/assets/{asset_id}/publish — Publish Asset **Scope:** `assets:write` Copies asset from private bucket to public CDN. --- # API Reference — Workspace ## GET /v1/workspace — Get Workspace **Scope:** `workspace:read` Response: Workspace name, plan, owner, created_at. ## GET /v1/workspace/members — List Members **Scope:** `workspace:read` Response: Array of members with roles. ## GET /v1/workspace/usage — Get Usage **Scope:** `workspace:read` Response: Current usage vs plan limits (links, QR templates, assets, API calls, members). --- # API Reference — Webhooks ## POST /v1/webhooks — Create Webhook **Scope:** `webhooks:write` Request body: | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `url` | string | Yes | HTTPS endpoint URL | | `events` | string[] | Yes | Events to subscribe to (or `["*"]`) | | `description` | string | No | Human-readable label | Response: `201 Created` with webhook object including `signing_secret`. ## GET /v1/webhooks — List Webhooks **Scope:** `webhooks:read` ## GET /v1/webhooks/{webhook_id} — Get Webhook **Scope:** `webhooks:read` ## PATCH /v1/webhooks/{webhook_id} — Update Webhook **Scope:** `webhooks:write` ## DELETE /v1/webhooks/{webhook_id} — Delete Webhook **Scope:** `webhooks:write` ## GET /v1/webhooks/{webhook_id}/deliveries — List Deliveries **Scope:** `webhooks:read` Response: Recent delivery attempts with status codes, response times, and payloads. --- # Webhook Events ## Event Types | Event | Description | |-------|-------------| | `link.created` | Short link created | | `link.updated` | Short link modified | | `link.deleted` | Short link deleted | | `link.scanned` | Link or QR code scanned | | `link.threshold` | Scan count milestone reached | | `bio.published` | Bio page published | | `bio.unpublished` | Bio page unpublished | | `asset.uploaded` | Asset upload confirmed | | `subscription.updated` | Workspace plan changed | | `test.ping` | Test event from dashboard | ## Webhook Payload Format ```json { "event": "link.created", "timestamp": "2026-03-21T10:30:00Z", "data": { ... } } ``` ## Signature Verification Webhooks include `X-XQR-Signature` header (HMAC-SHA256 of raw body with signing secret): ``` X-XQR-Signature: sha256= ``` Verify: ```python import hmac, hashlib expected = hmac.new(signing_secret.encode(), raw_body, hashlib.sha256).hexdigest() assert hmac.compare_digest(f"sha256={expected}", signature_header) ``` ## Retry Strategy Failed deliveries (non-2xx or timeout) retry with exponential backoff: - Attempt 1: immediate - Attempt 2: 1 minute - Attempt 3: 5 minutes - Attempt 4: 30 minutes - Attempt 5: 2 hours - Attempt 6: 12 hours After 6 failures, the delivery is marked as failed. The webhook endpoint is disabled after 7 consecutive days of failures. --- # MCP Server The `@xqr/mcp-server` npm package connects AI agents to your xQR workspace via the Model Context Protocol (stdio transport). ## Quick Start ```bash XQR_API_KEY=xqr_pk_... npx @xqr/mcp-server ``` ## 27 Tools ### Links: create_link, list_links, get_link, update_link, delete_link, bulk_create_links, get_link_qr ### QR: generate_qr, list_qr_templates, create_qr_template, render_qr_template ### Analytics: analytics_summary, analytics_link_stats, analytics_timeseries, analytics_geography, analytics_devices, analytics_top_links ### Bio: list_bio_pages, get_bio_page, create_bio_page, update_bio_page, publish_bio_page ### Assets: list_assets, get_asset, publish_asset, delete_asset ### Workspace: get_workspace, get_workspace_usage, list_workspace_members ## 7 Resources | URI | Description | |-----|-------------| | `xqr://workspace` | Workspace details | | `xqr://workspace/usage` | Usage vs limits | | `xqr://links` | All short links | | `xqr://links/{link_id}` | Single link details | | `xqr://analytics/summary` | 30-day analytics KPIs | | `xqr://templates` | QR design templates | | `xqr://bio-pages` | Link-in-Bio pages | --- # Routing Rules Schema Links support conditional routing via `routing_rules`: ```json { "routing_rules": [ { "destination": "https://example.de/promo", "priority": 1, "conditions": { "countries": ["DE", "AT", "CH"], "country_mode": "include" } } ] } ``` Available conditions: - `countries` + `country_mode` (`include`|`exclude`): ISO country codes - `device_types`: Array of `mobile`, `tablet`, `desktop` - `hours`: `{ start, end }` in 24h format - `percentage`: Traffic split for A/B testing (0–100) - `expires_at`: Rule expiration (ISO 8601) Rules evaluate in priority order (lowest first). First match wins; no match uses default `url`.