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
}
StatusError codeMeaning
400validation_failedRequest body failed Zod validation. issues array has field-level detail.
401unauthorizedMissing or invalid Bearer token.
403forbiddenToken is valid but does not have access to the requested resource.
404not_foundResource does not exist, or belongs to a different workspace.
402plan_limit_exceededRequested action requires a higher plan tier.
409slug_takenCustom slug is already in use.
413file_too_largeUploaded 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

FieldTypeRequiredDescription
typestring enumNo (default: url)Code semantic type. See code type payloads.
targetUrlstring (URL)Required for url typeWhere the code redirects. Must be http(s)://.
slugstringNoCustom short slug ([a-zA-Z0-9_-], max 100). Auto-generated (8 chars) if omitted.
domainstringNoCustom short domain (e.g. qr.acme.com). Must be provisioned in your workspace.
fallbackUrlstring (URL)NoWhere scanners go after the code expires or hits its scan cap.
expiresAtISO 8601 datetimeNo (Pro+)After this timestamp, redirects go to fallbackUrl. Pro+ only.
activeFromISO 8601 datetimeNo (Pro+)Before this timestamp, redirects go to fallbackUrl. Pro+ only.
activeUntilISO 8601 datetimeNo (Pro+)Alias for expiresAt. Pro+ only.
maxScansintegerNo (Pro+)Redirect cap. After this many scans, redirects go to fallbackUrl. Pro+ only.
tagsstring[]NoUp to 10 tags. Each tag is alphanumeric plus _ : . -, max 48 chars.
folderIdstringNoFolder to assign the code to on create.
passwordstringNo (Pro+)Password gate. Scanners must enter this before the redirect fires. Stored as Argon2id hash. Pro+ only.
passwordHintstring (max 100)NoOptional hint shown on the password gate page.
styleobjectNoVisual customization. See style fields below.
scanTrackingEnabledbooleanNo (default: true)Set to false to disable scan recording for this code.

Style fields (style object)

FieldTypeDescription
foregroundhex colorModule color. Default: #2D7A4E.
backgroundhex colorBackground color. Default: transparent.
framestring enumFrame style. Free tier restricted to branded frames. See full enum in the API.
patternstring enumModule shape: square, rounded, dots, classy, classy-rounded, extra-rounded, horizontal-stripe, vertical-stripe.
cornersstring enumEye-marker shape: square, rounded, extra-rounded, circle, leaf, pin, inpoint, outpoint, cross.
centerTextstring (max 8)Short text centered in the code. Mutually exclusive with a logo.
ctaTextstring (max 60)Call-to-action text rendered in the frame band.
gradientobjectBackground gradient: {type, from, to, angle}. type is linear or radial.
eyeOuter / eyeInnerhex colorIndependent colors for the outer ring and inner square of each finder eye.
centerIconobjectSocial 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

ParamTypeDescription
workspaceIdstringTarget workspace. Defaults to the key's workspace.
limitintegerPage size (default 100, max 100).
cursorstringPagination cursor from a previous response.
folderIdstringFilter by folder. Pass none for codes with no folder.
tagstringFilter to codes carrying this tag.
statusstringFilter by status: active, expired, suspended.
qstringFull-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/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

ParamValuesDefault
range7d, 30d, 90d, all30d
startISO 8601 dateOverrides range when set. Max span 365 days.
endISO 8601 datePair 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

ParamDefaultMax
limit50200
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.

TypeCategoryPayload keyKey fields
urlRedirect(none)targetUrl on the root object.
textDirecttexttext (string, max 1500)
vcardDirectcardfirstName, lastName, organization, phones, emails, website, address, notes. At least one name/org required.
wifiDirectwifissid, encryption (WPA | WEP | nopass), password, hidden
emailDirectemailto, subject, body
phoneDirectphonenumber (digits, +, space, parens, dash)
smsDirectsmsnumber, body (max 1500)
whatsappDirectwhatsappnumber (7-15 digits), message
cryptoDirectcryptochain (bitcoin | ethereum | litecoin | dogecoin | solana | usdc-eth), address, amount, label
eventDirecteventtitle, startsAt (ISO 8601), endsAt, location, description
calendarDirectcalendartitle, startIso, endIso, location, description, allDay
locationDirectlocationlatitude, longitude, label
socialDirectsocialplatform (instagram | tiktok | x | facebook | linkedin | youtube | snapchat | pinterest), handle
appRedirectappiosAppId, androidPackageName, fallbackUrl. At least one of ios/android required.
list-of-linksHosted pagelistOfLinksitems (1-25 items, each with label, url, icon), title, bio, avatarUrl
couponHosted pagecouponcode (max 60), description, expiresAt, terms, redirectUrl, brandColor
menuHosted pagemenubusinessName, sections (1-20 sections, each with title and items), description, brandColor, hours, phone, address
formHosted pageformtitle, fields (1-20 fields, each with id, label, type), submitLabel, successMessage, brandColor
app-downloadHosted pageappDownloadappName, iosUrl, androidUrl, fallbackUrl. At least one of ios/android required.
videoHosted pagevideovideoUrl (YouTube or Vimeo), title, description, posterUrl
business-pageHosted pagebusinessPagename, tagline, photoUrl, links (max 15), socials (max 7), cta, brandColor
pdfHosted pagepdfr2Key, originalFilename, fileSize, description, downloadOnly. Use the PDF upload endpoint.
image-carouselHosted pageimageCarouselimages (1-10 items), title, description. Use the image upload endpoint.