QRTY API
A REST API for dynamic multi-link QR codes, organizations, SSO/SCIM provisioning, webhooks, and scan analytics. Base URL: https://api.qrty.page
30-second quickstart
- Create an API key in Settings > API Keys.
- Copy the full key (shown once) — it starts with
qrty_live_. - Make your first request (see the code panel →).
Authentication
Protected endpoints accept a Bearer token in the Authorization header. QRTY supports three credential types.
API keys
Keys look like qrty_live_ followed by 32 hex characters. Only the prefix and a SHA-256 hash are stored server-side — you'll see the full key once.
Each key carries one or more permissions:
read— list and fetchcreate— create QR codes and foldersupdate— edit metadata, style, links, tagsdelete— delete resourcesanalytics— read scan analytics
Session JWT
Issued to users signed in via the web app (Auth0 or enterprise OIDC). Required for managing orgs, SSO, SCIM, and webhooks.
SCIM bearer token
A per-org token used only by your IdP against /scim/v2/*. Rotatable from the org SSO settings.
QR codes
The core resource. A QR code points to one or more links; the short URL stays the same even when you edit or reorder targets.
/api/qrapi-key · createCreate a QR code
Creates a dynamic QR code with one or more target links. The `links` array is required; everything else is optional.
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer qrty_live_<32-hex> |
| Content-Typerequired | string | application/json |
Body parameters
| Name | Type | Description |
|---|---|---|
| linksrequired | array | Array of link objects ({ url, title?, isActive?, scheduledStart?, scheduledEnd? }). |
| title | string | Display title shown in your dashboard. |
| ttl | number | Time-to-live in seconds. Omit for permanent. |
| folderId | string | Place the QR inside an existing folder. |
| tags | string[] | Tag labels. |
| style | object | { foregroundColor, backgroundColor, dotStyle }. |
curl -X POST https://api.qrty.page/api/qr \
-H "Authorization: Bearer qrty_live_..." \
-H "Content-Type: application/json" \
-d '{
"links": [{"url": "https://example.com", "title": "Example"}],
"ttl": 2592000,
"title": "Campaign Q2",
"style": {
"foregroundColor": "#1a1a1a",
"backgroundColor": "#ffffff",
"dotStyle": "rounded"
}
}'{
"id": "abc12345",
"qrUrl": "https://api.qrty.page/api/qr/abc12345",
"qrDataUrl": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0...",
"viewUrl": "https://api.qrty.page/l/abc12345",
"expiresAt": "2026-06-01T09:25:46.011Z"
}/api/qr/:idpublicGet QR image
Public. Returns the QR code rendered as an SVG.
Path parameters
| Name | Type | Description |
|---|---|---|
| idrequired | string | Short QR id. |
Query parameters
| Name | Type | Description |
|---|---|---|
| size | number | Pixel width/height. Default 256. |
curl https://api.qrty.page/api/qr/abc12345?size=512 -o qr.svg<svg ...>...</svg>/api/qr/:idapi-key · updateUpdate QR metadata
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer qrty_live_<32-hex> |
Path parameters
| Name | Type | Description |
|---|---|---|
| idrequired | string | QR id. |
Body parameters
| Name | Type | Description |
|---|---|---|
| title | string | New title. |
| ttl | number | New TTL in seconds. |
| folderId | string | Move to folder. |
curl -X PATCH https://api.qrty.page/api/qr/abc12345 \
-H "Authorization: Bearer qrty_live_..." \
-H "Content-Type: application/json" \
-d '{ "title": "Renamed" }'{ "id": "abc12345", "title": "Renamed" }/api/qr/:idapi-key · deleteDelete a QR code
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer qrty_live_<32-hex> |
Path parameters
| Name | Type | Description |
|---|---|---|
| idrequired | string | QR id. |
curl -X DELETE https://api.qrty.page/api/qr/abc12345 \
-H "Authorization: Bearer qrty_live_..."{ "success": true }/api/qr/:id/analyticsapi-key · analyticsScan & click analytics
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer qrty_live_<32-hex> |
Path parameters
| Name | Type | Description |
|---|---|---|
| idrequired | string | QR id. |
curl https://api.qrty.page/api/qr/abc12345/analytics \
-H "Authorization: Bearer qrty_live_..."{
"id": "abc12345",
"title": "Campaign Q2",
"links": [{ "url": "https://example.com", "title": "Example" }],
"createdAt": "2026-05-02T10:00:00Z",
"expiresAt": "2026-06-01T10:00:00Z",
"analytics": {
"totalScans": 128,
"scansByDay": [{ "date": "2026-05-02", "count": 42 }],
"scansByCountry": [{ "country": "US", "count": 60 }],
"clicksByLink": [{ "linkIndex": 0, "url": "https://example.com", "clicks": 94 }]
}
}/api/qr/:id/styleapi-key · updateUpdate QR style
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer qrty_live_<32-hex> |
Path parameters
| Name | Type | Description |
|---|---|---|
| idrequired | string | QR id. |
Body parameters
| Name | Type | Description |
|---|---|---|
| foregroundColor | string | Hex color, e.g. #1a1a1a. |
| backgroundColor | string | Hex color. |
| dotStyle | 'square' | 'rounded' | 'dots' | Module style. |
/api/qr/:id/linksapi-key · updateReplace all links
Replaces the entire link list. Supports scheduling via `scheduledStart`/`scheduledEnd` and toggling individual targets with `isActive`.
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer qrty_live_<32-hex> |
Path parameters
| Name | Type | Description |
|---|---|---|
| idrequired | string | QR id. |
Body parameters
| Name | Type | Description |
|---|---|---|
| linksrequired | array | Ordered array of link objects. |
curl -X PUT https://api.qrty.page/api/qr/abc12345/links \
-H "Authorization: Bearer qrty_live_..." \
-H "Content-Type: application/json" \
-d '{
"links": [
{"url": "https://example.com", "title": "Primary", "isActive": true},
{"url": "https://promo.com",
"scheduledStart": "2026-06-01T00:00:00Z",
"scheduledEnd": "2026-06-30T23:59:59Z"}
]
}'/api/qr/:id/links/:linkIndexapi-key · updatePatch one link
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer qrty_live_<32-hex> |
Path parameters
| Name | Type | Description |
|---|---|---|
| idrequired | string | QR id. |
| linkIndexrequired | number | Zero-based index. |
/api/qr/:id/passwordapi-key · updateSet password
Require a password before the short URL resolves to the link list.
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer qrty_live_<32-hex> |
Body parameters
| Name | Type | Description |
|---|---|---|
| passwordrequired | string | Plaintext; hashed server-side. |
/api/qr/:id/passwordapi-key · updateRemove password
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer qrty_live_<32-hex> |
{ "success": true }/api/qr/:id/verify-passwordpublicVerify password
Public. On success returns the unlocked link list directly — no token is issued.
Body parameters
| Name | Type | Description |
|---|---|---|
| passwordrequired | string | User-supplied password. |
{ "links": [{ "url": "https://example.com", "title": "Example" }] }/api/dashboardapi-key · readList your QR codes
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer qrty_live_<32-hex> |
Query parameters
| Name | Type | Description |
|---|---|---|
| folderId | string | Filter by folder. |
| tag | string | Filter by tag. |
| search | string | Substring match against title. |
{
"qrCodes": [
{ "id": "...", "title": "...", "links": [ ... ], "createdAt": "...", "expiresAt": "..." }
],
"totalQRCodes": 42,
"totalScans": 1234
}Organizations
Role hierarchy: owner > admin > editor > viewer. Most endpoints accept either the org id or slug as :orgId. Accepts either an API key or a session JWT.
/api/orgsapi-keyCreate an organization
Caller becomes `owner`.
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer qrty_live_<32-hex> |
Body parameters
| Name | Type | Description |
|---|---|---|
| namerequired | string | Display name (min 2 chars). |
| slugrequired | string | URL-safe slug. Must be unique. |
curl -X POST https://api.qrty.page/api/orgs \
-H "Authorization: Bearer qrty_live_..." \
-H "Content-Type: application/json" \
-d '{ "name": "Acme Inc", "slug": "acme" }'{
"id": "org_...",
"name": "Acme Inc",
"slug": "acme",
"createdAt": "2026-05-02T12:34:56.000Z",
"createdBy": "user_...",
"updatedAt": "2026-05-02T12:34:56.000Z"
}/api/orgsapi-keyList your orgs
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer qrty_live_<32-hex> |
{ "error": "Unauthorized" }/api/orgs/:orgIdapi-keyGet org (with members)
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer qrty_live_<32-hex> |
{ "error": "Unauthorized" }/api/orgs/:orgIdapi-keyUpdate org
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer qrty_live_<32-hex> |
Body parameters
| Name | Type | Description |
|---|---|---|
| name | string | New name. |
| slug | string | New slug. |
| logoUrl | string | Logo URL. |
{ "error": "Unauthorized" }/api/orgs/:orgIdapi-keyDelete org (owner only)
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer qrty_live_<32-hex> |
{ "success": true }/api/orgs/:orgId/inviteapi-keyInvite a member
Sends an invite email via Resend.
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer qrty_live_<32-hex> |
Body parameters
| Name | Type | Description |
|---|---|---|
| emailrequired | string | Invitee email. |
| rolerequired | 'admin' | 'editor' | 'viewer' | Role at acceptance. |
{ "id": "inv_...", "email": "a@b.com", "role": "editor" }/api/orgs/:orgId/invites/:inviteId/acceptapi-keyAccept invite
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer qrty_live_<32-hex> |
{ "error": "Unauthorized" }/api/orgs/:orgId/members/:memberIdapi-keyChange a role (owner only)
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer qrty_live_<32-hex> |
{ "error": "Unauthorized" }/api/orgs/:orgId/members/:memberIdapi-keyRemove member
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer qrty_live_<32-hex> |
{ "error": "Unauthorized" }/api/orgs/:orgId/activityapi-keyActivity log
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer qrty_live_<32-hex> |
Query parameters
| Name | Type | Description |
|---|---|---|
| limit | number | Default 50. |
| offset | number | Default 0. |
{ "error": "Unauthorized" }/api/orgs/:orgId/dashboardapi-keyOrg-wide dashboard
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer qrty_live_<32-hex> |
{ "error": "Unauthorized" }SSO (OIDC)
Per-organization SSO using any OpenID Connect provider (Okta, Azure AD, Auth0, Google Workspace, etc.). Admin-only.
/api/orgs/:orgId/sso/oidcapi-keyConfigure OIDC
Create or update the org OIDC config. Response includes the redirect URIs you need to register with your IdP.
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer qrty_live_<32-hex> |
Body parameters
| Name | Type | Description |
|---|---|---|
| issuerrequired | string | IdP issuer URL. |
| clientIdrequired | string | OAuth client id. |
| clientSecretrequired | string | OAuth client secret. |
| scopes | string | Space-separated scopes. Default: "openid profile email". |
| enabled | boolean | Toggle SSO on/off. |
| allowJit | boolean | Just-in-time provisioning. |
| defaultRole | 'admin' | 'editor' | 'viewer' | Role assigned to JIT-provisioned users. |
curl -X PUT https://api.qrty.page/api/orgs/acme/sso/oidc \
-H "Authorization: Bearer <session-jwt>" \
-H "Content-Type: application/json" \
-d '{
"issuer": "https://your-tenant.okta.com",
"clientId": "0oa...",
"clientSecret": "...",
"scopes": "openid profile email",
"enabled": true,
"allowJit": true,
"defaultRole": "viewer"
}'{
"config": { "issuer": "...", "clientId": "...", "enabled": true },
"redirectUris": [
"https://api.qrty.page/api/auth/oidc/callback",
"https://api.qrty.page/api/auth/oidc/initiate",
"https://api.qrty.page/api/auth/oidc/login?org=acme",
"https://api.qrty.page/api/auth/oidc/universal-logout/acme"
]
}/api/auth/oidc/loginpublicStart a user-initiated login
Query parameters
| Name | Type | Description |
|---|---|---|
| orgrequired | string | Org slug. |
| return_to | string | Path to land on after login. |
{ "error": "Too Many Requests", "retryAfter": 45 }/api/auth/oidc/initiatepublicIdP-initiated login
Query parameters
| Name | Type | Description |
|---|---|---|
| issrequired | string | IdP issuer. |
| org | string | Org hint. |
| target_link_uri | string | Deep-link target. |
{ "error": "Too Many Requests", "retryAfter": 45 }/api/auth/oidc/callbackpublicOIDC callback
Query parameters
| Name | Type | Description |
|---|---|---|
| coderequired | string | Authorization code. |
| staterequired | string | State parameter. |
{ "error": "Too Many Requests", "retryAfter": 45 }/api/auth/oidc/sessionpublicExchange code → session JWT
Body parameters
| Name | Type | Description |
|---|---|---|
| coderequired | string | Short-lived code from /callback. |
{ "token": "<jwt>", "user": { "id": "...", "email": "..." } }/api/auth/oidc/universal-logout/:orgIdpublicOkta Universal Logout
Revokes all sessions for the org. Called by Okta.
{ "error": "Too Many Requests", "retryAfter": 45 }Redirect URIs to register
- https://api.qrty.page/api/auth/oidc/callback
- https://api.qrty.page/api/auth/oidc/initiate
- https://api.qrty.page/api/auth/oidc/login?org={slug}
- https://api.qrty.page/api/auth/oidc/universal-logout/{slug}
SCIM 2.0 provisioning
Automate user and group lifecycle from your IdP (Okta, Azure AD, OneLogin). Endpoints are compliant with RFC 7644.
Base URL: https://api.qrty.page/scim/v2/{orgIdOrSlug}. Use the SCIM bearer token in the Authorization header on every request.
/api/orgs/:orgId/sso/scimapi-keyEnable SCIM / rotate token
Response contains the new token only once — store it in your IdP immediately.
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer qrty_live_<32-hex> |
Body parameters
| Name | Type | Description |
|---|---|---|
| enabledrequired | boolean | Enable or disable SCIM. |
| rotateToken | boolean | Issue a new bearer token. |
curl -X PUT https://api.qrty.page/api/orgs/acme/sso/scim \
-H "Authorization: Bearer <session-jwt>" \
-H "Content-Type: application/json" \
-d '{ "enabled": true, "rotateToken": true }'{
"scim": {
"id": "...",
"orgId": "...",
"enabled": true,
"tokenPrefix": "qrty_scim_...",
"tokenConfigured": true,
"createdAt": "2026-05-02T09:40:10.152Z",
"updatedAt": "2026-05-02T09:40:10.152Z"
},
"baseUrl": "https://api.qrty.page/scim/v2/acme",
"token": "qrty_scim_<64-hex-char-token>"
}/scim/v2/:orgId/ServiceProviderConfigscim tokenService provider config
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer <scim bearer token> |
{ "error": "Unauthorized" }/scim/v2/:orgId/Schemasscim tokenSupported schemas
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer <scim bearer token> |
{ "error": "Unauthorized" }/scim/v2/:orgId/ResourceTypesscim tokenSupported resource types
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer <scim bearer token> |
{ "error": "Unauthorized" }/scim/v2/:orgId/Usersscim tokenList users
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer <scim bearer token> |
Query parameters
| Name | Type | Description |
|---|---|---|
| filter | string | SCIM filter, e.g. userName eq "a@b.com". |
| startIndex | number | 1-based. |
| count | number | Page size. |
{ "error": "Unauthorized" }/scim/v2/:orgId/Usersscim tokenCreate user
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer <scim bearer token> |
| Content-Typerequired | string | application/scim+json |
Body parameters
| Name | Type | Description |
|---|---|---|
| userNamerequired | string | Unique username (usually email). |
| emailsrequired | array | At least one email, with `primary: true`. |
| name | object | { givenName, familyName }. |
| active | boolean | Default true. |
| externalId | string | IdP-side id. |
curl -X POST https://api.qrty.page/scim/v2/acme/Users \
-H "Authorization: Bearer <scim-token>" \
-H "Content-Type: application/scim+json" \
-d '{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName": "alice@acme.com",
"emails": [{ "primary": true, "value": "alice@acme.com" }],
"active": true
}'{ "id": "...", "userName": "alice@acme.com", "active": true }/scim/v2/:orgId/Users/:userIdscim tokenGet user
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer <scim bearer token> |
{ "error": "Unauthorized" }/scim/v2/:orgId/Users/:userIdscim tokenReplace user
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer <scim bearer token> |
{ "error": "Unauthorized" }/scim/v2/:orgId/Users/:userIdscim tokenPartial user update
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer <scim bearer token> |
{ "error": "Unauthorized" }/scim/v2/:orgId/Users/:userIdscim tokenDeactivate user
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer <scim bearer token> |
{ "error": "Unauthorized" }/scim/v2/:orgId/Groupsscim tokenList groups
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer <scim bearer token> |
{ "error": "Unauthorized" }/scim/v2/:orgId/Groupsscim tokenCreate group
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer <scim bearer token> |
Body parameters
| Name | Type | Description |
|---|---|---|
| displayNamerequired | string | Group name. |
| members | array | [{ value: userId }] |
{ "error": "Unauthorized" }/scim/v2/:orgId/Groups/:groupIdscim tokenGet group
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer <scim bearer token> |
{ "error": "Unauthorized" }/scim/v2/:orgId/Groups/:groupIdscim tokenReplace group
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer <scim bearer token> |
{ "error": "Unauthorized" }/scim/v2/:orgId/Groups/:groupIdscim tokenPartial group update
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer <scim bearer token> |
{ "error": "Unauthorized" }/scim/v2/:orgId/Groups/:groupIdscim tokenDelete group
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer <scim bearer token> |
{ "error": "Unauthorized" }Webhooks
Receive real-time event notifications at your URL. Management endpoints require a session JWT — they cannot be managed with API keys.
Events
qr.createdqr.updatedqr.deletedqr.scannedlink.clicked
Delivery headers
X-QRTY-Event— event typeX-QRTY-Signature— hex HMAC-SHA256 of raw body, using your webhook secretContent-Type: application/json
/api/webhookssession jwtCreate webhook
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer <session jwt> |
Body parameters
| Name | Type | Description |
|---|---|---|
| urlrequired | string | HTTPS endpoint. |
| eventsrequired | string[] | One or more event names. |
curl -X POST https://api.qrty.page/api/webhooks \
-H "Authorization: Bearer <session-jwt>" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/qrty",
"events": ["qr.scanned", "link.clicked"]
}'{ "id": "wh_123", "url": "...", "secret": "<only-shown-once>", "events": ["qr.scanned"] }/api/webhookssession jwtList webhooks
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer <session jwt> |
{ "error": "Unauthorized" }/api/webhooks/:idsession jwtUpdate webhook
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer <session jwt> |
{ "error": "Unauthorized" }/api/webhooks/:idsession jwtDelete webhook
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer <session jwt> |
{ "error": "Unauthorized" }/api/webhooks/:id/deliveriessession jwtRecent deliveries
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer <session jwt> |
{ "error": "Unauthorized" }/api/webhooks/:id/testsession jwtSend a test payload
Headers
| Name | Type | Description |
|---|---|---|
| Authorizationrequired | string | Bearer <session jwt> |
{ "error": "Unauthorized" }Example payload
{
"event": "qr.scanned",
"timestamp": "2026-05-02T12:00:00Z",
"data": {
"qr_id": "abc12345",
"country": "US",
"device": "mobile"
}
}Rate limits
Limits are per-IP, per-endpoint, with a 60-second sliding window.
| Endpoint | Limit |
|---|---|
| POST /api/qr | 10 / min |
| GET /api/qr/* | 60 / min |
| GET /l/* | 120 / min |
| (default) | 100 / min |
Response headers
X-RateLimit-Limit— cap for the windowX-RateLimit-Remaining— requests leftX-RateLimit-Reset— Unix seconds until resetRetry-After— seconds to wait (sent on 429)
Errors
All error responses use a consistent JSON shape: { "error": "..." }. Some endpoints add a message. Status codes follow HTTP conventions.
| Status | Meaning |
|---|---|
| 400 | Validation or malformed request |
| 401 | Missing or invalid credentials |
| 403 | Authenticated but not permitted |
| 404 | Resource not found |
| 409 | Conflict (e.g. duplicate slug) |
| 429 | Rate limit exceeded |
| 500 | Server error |
Changelog
2026-05-02
- Launched public API documentation at
/docs. - Added SSO (OIDC) and SCIM 2.0 provisioning reference.
- Added language tabs (cURL, JS, Python) and response tabs (200/4xx/5xx).