REST API reference
All endpoints are under https://api.turtleqr.com/v1/.
Every /v1/* route requires a Bearer token. Public routes are documented
separately with their own auth notes.
Back to Docs hub or jump to MCP reference.
Authentication
Pass your API key in the Authorization header on every request to a
protected route:
Authorization: Bearer qr_live_YOUR_KEY_HERE
Keys starting with qr_live_ are production keys. Keys starting with
qr_test_ behave identically but are scoped to test workspaces and are
safe to use during integration work. Generate keys in the dashboard under
API keys.
All authenticated endpoints are workspace-scoped. The key determines which
workspace the request acts on. To act on a specific workspace, pass
?workspaceId=ws_... as a query parameter.
Errors
All error responses follow a consistent envelope:
{
"error": "machine_readable_code",
"detail": "Human-readable explanation.",
"issues": [...] // present only on 400 validation errors
}
| Status | Error code | Meaning |
|---|---|---|
400 | validation_failed | Request body failed Zod validation. issues array has field-level detail. |
401 | unauthorized | Missing or invalid Bearer token. |
403 | forbidden | Token is valid but does not have access to the requested resource. |
404 | not_found | Resource does not exist, or belongs to a different workspace. |
402 | plan_limit_exceeded | Requested action requires a higher plan tier. |
409 | slug_taken | Custom slug is already in use. |
413 | file_too_large | Uploaded file exceeds the per-type size cap. |
Codes
A code is a dynamic QR code that encodes a short URL at
qr.turtleqr.com/{slug}. The short URL resolves immediately and can
be retargeted at any time without reprinting the code.
POST /v1/codes
Create a new QR code. Returns the created code object including the
slug and shortUrl.
Auth
Bearer token required.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
type | string enum | No (default: url) | Code semantic type. See code type payloads. |
targetUrl | string (URL) | Required for url type | Where the code redirects. Must be http(s)://. |
slug | string | No | Custom short slug ([a-zA-Z0-9_-], max 100). Auto-generated (8 chars) if omitted. |
domain | string | No | Custom short domain (e.g. qr.acme.com). Must be provisioned in your workspace. |
fallbackUrl | string (URL) | No | Where scanners go after the code expires or hits its scan cap. |
expiresAt | ISO 8601 datetime | No (Pro+) | After this timestamp, redirects go to fallbackUrl. Pro+ only. |
activeFrom | ISO 8601 datetime | No (Pro+) | Before this timestamp, redirects go to fallbackUrl. Pro+ only. |
activeUntil | ISO 8601 datetime | No (Pro+) | Alias for expiresAt. Pro+ only. |
maxScans | integer | No (Pro+) | Redirect cap. After this many scans, redirects go to fallbackUrl. Pro+ only. |
tags | string[] | No | Up to 10 tags. Each tag is alphanumeric plus _ : . -, max 48 chars. |
folderId | string | No | Folder to assign the code to on create. |
password | string | No (Pro+) | Password gate. Scanners must enter this before the redirect fires. Stored as Argon2id hash. Pro+ only. |
passwordHint | string (max 100) | No | Optional hint shown on the password gate page. |
style | object | No | Visual customization. See style fields below. |
scanTrackingEnabled | boolean | No (default: true) | Set to false to disable scan recording for this code. |
Style fields (style object)
| Field | Type | Description |
|---|---|---|
foreground | hex color | Module color. Default: #2D7A4E. |
background | hex color | Background color. Default: transparent. |
frame | string enum | Frame style. Free tier restricted to branded frames. See full enum in the API. |
pattern | string enum | Module shape: square, rounded, dots, classy, classy-rounded, extra-rounded, horizontal-stripe, vertical-stripe. |
corners | string enum | Eye-marker shape: square, rounded, extra-rounded, circle, leaf, pin, inpoint, outpoint, cross. |
centerText | string (max 8) | Short text centered in the code. Mutually exclusive with a logo. |
ctaText | string (max 60) | Call-to-action text rendered in the frame band. |
gradient | object | Background gradient: {type, from, to, angle}. type is linear or radial. |
eyeOuter / eyeInner | hex color | Independent colors for the outer ring and inner square of each finder eye. |
centerIcon | object | Social icon centered in the code: {id, scale}. id is one of: instagram, tiktok, youtube, x, facebook, linkedin, pinterest, snapchat. |
Example
curl -X POST https://api.turtleqr.com/v1/codes \
-H "Authorization: Bearer qr_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "url",
"targetUrl": "https://example.com/autumn-menu",
"slug": "autumn-menu",
"fallbackUrl": "https://example.com/",
"tags": ["q4", "print"]
}'
GET /v1/codes
List QR codes in the workspace. Returns newest first.
Auth
Bearer token required.
Query parameters
| Param | Type | Description |
|---|---|---|
workspaceId | string | Target workspace. Defaults to the key's workspace. |
limit | integer | Page size (default 100, max 100). |
cursor | string | Pagination cursor from a previous response. |
folderId | string | Filter by folder. Pass none for codes with no folder. |
tag | string | Filter to codes carrying this tag. |
status | string | Filter by status: active, expired, suspended. |
q | string | Full-text search on slug, label, and target URL. |
Example
curl https://api.turtleqr.com/v1/codes \
-H "Authorization: Bearer qr_live_YOUR_KEY"
GET /v1/codes/:id
Read a single code by id. Returns the full code object including payload and style.
Auth
Bearer token required. Returns 404 if the code belongs to a different workspace.
Example
curl https://api.turtleqr.com/v1/codes/code_abc123 \
-H "Authorization: Bearer qr_live_YOUR_KEY"
PATCH /v1/codes/:id
Update a code. All fields are optional. Only the fields you send are changed. The KV redirect cache is invalidated; new scans see the change within 60 seconds globally.
Auth
Bearer token required.
Body
Same shape as POST /v1/codes. Omit fields you do not want to change.
Send null to clear a nullable field (e.g., "fallbackUrl": null).
Example
curl -X PATCH https://api.turtleqr.com/v1/codes/code_abc123 \
-H "Authorization: Bearer qr_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"targetUrl": "https://example.com/new-destination"}'
DELETE /v1/codes/:id
Soft-delete a code. The code status is set to deleted; scanners
hit the workspace fallback URL. The row is retained and is recoverable by an admin.
Auth
Bearer token required.
Example
curl -X DELETE https://api.turtleqr.com/v1/codes/code_abc123 \
-H "Authorization: Bearer qr_live_YOUR_KEY"
POST /v1/codes/:id/logo
Upload a logo image to center in the QR code. Replaces any prior logo.
The logo is stored in R2 and the code's style.logoR2Key is updated.
Auth
Bearer token required.
Body
Multipart form-data with a field named file. Accepted MIME types:
image/png, image/jpeg, image/svg+xml.
Maximum size: 1 MB.
Example
curl -X POST https://api.turtleqr.com/v1/codes/code_abc123/logo \
-H "Authorization: Bearer qr_live_YOUR_KEY" \
-F "file=@logo.png;type=image/png"
POST /v1/codes/:id/pdf-file
Upload a PDF to a pdf-type code. The file is stored in R2 and
served via the public stream endpoint GET /v1/files/pdf/:codeId.
Maximum size: determined by the Worker's body size limit (default 100 MB).
Auth
Bearer token required. Code must be type pdf.
Body
Multipart form-data with a field named file. MIME type must be
application/pdf.
POST /v1/codes/:id/image-file
Upload one image for an image-carousel-type code. Hard cap: 10 images
per carousel. Each image is stored in R2 at images/{codeId}/{index}.{ext}.
Auth
Bearer token required. Code must be type image-carousel.
Body
Multipart form-data with a field named file. Accepted MIME types:
image/jpeg, image/png, image/webp, image/gif.
Maximum size per image: 5 MB. Optional field index sets the slot.
DELETE /v1/codes/:id/image-file/:index
Remove one image from a carousel by its index. Remaining images are re-indexed to stay contiguous.
Auth
Bearer token required.
POST /v1/codes/:id/image-file/reorder
Reorder carousel images. The body specifies old indices in their new order.
Auth
Bearer token required.
Body
{ "order": [2, 0, 1] }
POST /v1/codes/:id/assign-folder
Assign or unassign a code from a folder.
Auth
Bearer token required.
Body
{ "folderId": "folder_abc123" } // assign
{ "folderId": null } // unassign
Analytics
All analytics endpoints require that the code belongs to the authenticated workspace. Range parameters are shared across all three endpoints.
Shared range parameters
| Param | Values | Default |
|---|---|---|
range | 7d, 30d, 90d, all | 30d |
start | ISO 8601 date | Overrides range when set. Max span 365 days. |
end | ISO 8601 date | Pair with start. |
GET /v1/codes/:id/analytics
Aggregated dashboard data: totals, time series, and breakdowns by country, city, region, device, OS, browser, hour-of-day, and day-of-week.
Auth
Bearer token required.
Response shape (abbreviated)
{
"totals": { "scans": 1240, "uniqueIpHashes": 830, "countries": 12 },
"series": [{ "t": "2026-05-01", "scans": 42 }, ...],
"byCountry": [{ "key": "US", "scans": 800 }, ...],
"byDevice": [{ "key": "mobile", "scans": 1100 }, ...],
"byHour": [0, 3, 12, ...], // 24 slots, 0-indexed
"byDayOfWeek": [0, 100, 200, ...] // 7 slots, 0=Sunday
}
Example
curl "https://api.turtleqr.com/v1/codes/code_abc123/analytics?range=30d" \
-H "Authorization: Bearer qr_live_YOUR_KEY"
GET /v1/codes/:id/scans
Cursor-paginated raw scan events, newest first. Each scan row includes:
scannedAt, country, region, city,
device, os, browser, referer.
Auth
Bearer token required.
Query parameters
| Param | Default | Max |
|---|---|---|
limit | 50 | 200 |
cursor | (first page) | Opaque ISO timestamp from nextCursor. |
Example
curl "https://api.turtleqr.com/v1/codes/code_abc123/scans?limit=25" \
-H "Authorization: Bearer qr_live_YOUR_KEY"
GET /v1/codes/:id/scans.csv
RFC 4180 CSV export of the raw scan log. Capped at 100,000 rows. The response
sets Content-Disposition: attachment; filename="scans-{id}-{range}.csv".
Auth
Bearer token required. Pro+ only.
Query parameters
Same range parameters as analytics.
Example
curl -o scans.csv \
"https://api.turtleqr.com/v1/codes/code_abc123/scans.csv?range=90d" \
-H "Authorization: Bearer qr_live_YOUR_KEY"
Folders
Folders are workspace-scoped containers. Each code belongs to at most one folder. Deleting a folder unassigns its codes; codes are not deleted. Plan limits: 3 folders on free/personal, 50 on pro, unlimited on business+.
POST /v1/folders
Auth
Bearer token required.
Body
{
"name": "Summer campaign", // required, 1-80 chars
"color": "#2D7A4E" // optional, hex color
}
Example
curl -X POST https://api.turtleqr.com/v1/folders \
-H "Authorization: Bearer qr_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Summer campaign", "color": "#5BA876"}'
GET /v1/folders
List all folders in the workspace, ordered by position. Each row includes a
codeCount field.
Auth
Bearer token required.
PATCH /v1/folders/:id
Update folder name, color, or position.
Auth
Bearer token required.
Body
{
"name": "Autumn campaign", // optional
"color": "#B85C00", // optional
"position": 2 // optional, non-negative integer
}
DELETE /v1/folders/:id
Delete a folder. Codes in the folder are unassigned (not deleted).
Auth
Bearer token required.
Routing rules
Smart per-code rules evaluated before the redirect fires. Four rule types: geo, device, time-of-day, and A/B test. Creating, updating, and deleting rules requires a Pro+ plan. Free workspaces can read existing rules on a downgraded code but cannot create new ones. Maximum 20 rules per code.
GET /v1/codes/:codeId/routing-rules
List routing rules for a code.
Auth
Bearer token required.
POST /v1/codes/:codeId/routing-rules
Create a routing rule. Pro+ only.
Auth
Bearer token required (Pro+).
Body
{
"type": "geo", // "geo" | "device" | "time" | "ab"
"label": "US visitors",
"targetUrl": "https://example.com/us",
"priority": 1,
// Geo rule condition:
"condition": {
"countryIn": ["US", "CA"],
"regionIn": ["California"],
"cityIn": ["San Francisco"]
},
// Device rule condition:
"condition": {
"deviceIn": ["mobile", "tablet"],
"osIn": ["iOS", "Android"]
},
// Time rule condition:
"condition": {
"daysOfWeek": [1, 2, 3, 4, 5], // 0=Sun, 6=Sat
"startTime": "09:00",
"endTime": "17:00",
"timezone": "America/Chicago"
},
// A/B rule: set "weight" instead of "condition"
"weight": 50 // percent; remaining traffic goes to base targetUrl
}
PATCH /v1/codes/:codeId/routing-rules/:ruleId
Update a routing rule. Pro+ only. Same body shape as POST, all fields optional.
Auth
Bearer token required (Pro+).
DELETE /v1/codes/:codeId/routing-rules/:ruleId
Delete a routing rule. Pro+ only.
Auth
Bearer token required (Pro+).
Billing
GET /v1/billing
Returns the workspace's current plan, Stripe subscription state, usage counters, the public price catalog, and the Pro trial status.
Auth
Bearer token required.
Response shape (abbreviated)
{
"workspaceId": "ws_abc",
"plan": "pro",
"subscriptionStatus": "active",
"subscriptionCurrentPeriodEnd": "2026-06-18T00:00:00Z",
"trialEndsAt": null,
"proTrialActive": false,
"codesCount": 42,
"scansCount": 3800,
"prices": {
"pro_monthly": { "id": "price_...", "amount": 900, "currency": "usd", "interval": "month" },
"pro_annual": { "id": "price_...", "amount": 8600, "currency": "usd", "interval": "year" }
}
}
GET /v1/billing/public
Public price catalog. No authentication required. Returns the same
prices object as GET /v1/billing without workspace state.
Auth
None.
Public file streams
These routes serve user-uploaded assets without authentication. Both are rate-limited at 30 requests per IP per hour via KV.
GET /v1/files/image/:codeId/:index
Stream one image from an image-carousel code. Returns the raw image with
Cache-Control: public, max-age=3600. Returns 404 when the code is
not type image-carousel, not active, or the index is out of range.
Auth
None. Rate-limited.
GET /v1/files/pdf/:codeId
Stream the PDF from a pdf-type code.
Returns Content-Disposition: inline for embedded viewing.
Returns 404 when the code is not type pdf or not active.
Auth
None. Rate-limited.
Status
GET /v1/status/summary
Overall and per-component operational status with 90-day history. Used by the status page. Cached at 60-second TTL in KV to avoid database load.
Auth
None.
GET /v1/status/rss
RSS 2.0 feed of recent incidents. Cached at 60-second TTL.
Auth
None.
Code type payloads
Every code has a type field. When the type is not url,
you must include the matching nested payload object in the request body.
Direct-payload types encode their content into the QR matrix; hosted-page types
encode the short URL and the redirect-worker renders an HTML page at scan time.
| Type | Category | Payload key | Key fields |
|---|---|---|---|
url | Redirect | (none) | targetUrl on the root object. |
text | Direct | text | text (string, max 1500) |
vcard | Direct | card | firstName, lastName, organization, phones, emails, website, address, notes. At least one name/org required. |
wifi | Direct | wifi | ssid, encryption (WPA | WEP | nopass), password, hidden |
email | Direct | email | to, subject, body |
phone | Direct | phone | number (digits, +, space, parens, dash) |
sms | Direct | sms | number, body (max 1500) |
whatsapp | Direct | whatsapp | number (7-15 digits), message |
crypto | Direct | crypto | chain (bitcoin | ethereum | litecoin | dogecoin | solana | usdc-eth), address, amount, label |
event | Direct | event | title, startsAt (ISO 8601), endsAt, location, description |
calendar | Direct | calendar | title, startIso, endIso, location, description, allDay |
location | Direct | location | latitude, longitude, label |
social | Direct | social | platform (instagram | tiktok | x | facebook | linkedin | youtube | snapchat | pinterest), handle |
app | Redirect | app | iosAppId, androidPackageName, fallbackUrl. At least one of ios/android required. |
list-of-links | Hosted page | listOfLinks | items (1-25 items, each with label, url, icon), title, bio, avatarUrl |
coupon | Hosted page | coupon | code (max 60), description, expiresAt, terms, redirectUrl, brandColor |
menu | Hosted page | menu | businessName, sections (1-20 sections, each with title and items), description, brandColor, hours, phone, address |
form | Hosted page | form | title, fields (1-20 fields, each with id, label, type), submitLabel, successMessage, brandColor |
app-download | Hosted page | appDownload | appName, iosUrl, androidUrl, fallbackUrl. At least one of ios/android required. |
video | Hosted page | video | videoUrl (YouTube or Vimeo), title, description, posterUrl |
business-page | Hosted page | businessPage | name, tagline, photoUrl, links (max 15), socials (max 7), cta, brandColor |
pdf | Hosted page | pdf | r2Key, originalFilename, fileSize, description, downloadOnly. Use the PDF upload endpoint. |
image-carousel | Hosted page | imageCarousel | images (1-10 items), title, description. Use the image upload endpoint. |