CirculateQR Developer Docs
Generate, track, and route dynamic QR codes programmatically. RESTful JSON API with Bearer token authentication.
Base URLhttps://circulateqr.com/api/v1
⚡ Quick Start
Get up and running in under 2 minutes. You need an API key from Settings → API Keys. API access requires Artisan (read) or Stealth (read + write) tier.
1. Create your first QR code
curl -X POST https://circulateqr.com/api/v1/qr/create \
-H "Authorization: Bearer cqr_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"destination":"https://example.com","name":"My First QR","type":"DYNAMIC"}'const res = await fetch("https://circulateqr.com/api/v1/qr/create", {
method: "POST",
headers: {
Authorization: "Bearer cqr_live_YOUR_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
destination: "https://example.com",
name: "My First QR",
type: "DYNAMIC",
}),
});
const qr = await res.json();
console.log(qr.shortUrl); // https://circulateqr.com/go/abc123import requests
res = requests.post(
"https://circulateqr.com/api/v1/qr/create",
headers={"Authorization": "Bearer cqr_live_YOUR_KEY"},
json={"destination": "https://example.com", "name": "My First QR", "type": "DYNAMIC"},
)
print(res.json()["shortUrl"]) # https://circulateqr.com/go/abc123🔑 Authentication
All requests require a Bearer token in the Authorization header.
Authorization: Bearer cqr_live_your_api_key_here
⚠Your API key is a secret. Never expose it in client-side code, public repos, or browser requests. Always call from your server.
API Reference — QR Codes
POST/api/v1/qr/createwrite
Create a new dynamic or static QR code with optional device routing rules and custom metadata.
REQUEST BODY
{
"destination": "https://example.com",
"name": "My QR Code",
"type": "DYNAMIC",
"design": { "foreground": "#000000", "background": "#FFFFFF" },
"rules": {
"ios": "https://apps.apple.com/...",
"android": "https://play.google.com/..."
},
"metadata": { "campaign": "summer-2026" }
}RESPONSE
{
"id": "uuid",
"shortId": "abc123",
"name": "My QR Code",
"destination": "https://example.com",
"shortUrl": "https://circulateqr.com/go/abc123",
"type": "DYNAMIC",
"imageUrl": "data:image/png;base64,...",
"createdAt": "2026-05-14T00:00:00Z"
}GET/api/v1/qr/list?limit=50&offset=0&search=read
List all QR codes for the authenticated user. Supports pagination and search.
RESPONSE
{
"data": [{ "id": "uuid", "shortId": "abc123", "name": "...", "destination": "...", "shortUrl": "...", "status": "active", "createdAt": "..." }],
"pagination": { "total": 42, "limit": 50, "offset": 0, "hasMore": false }
}GET/api/v1/qr/:idread
Get full details for a single QR code, including scan count and workflow rules.
RESPONSE
{
"id": "uuid", "shortId": "abc123", "name": "My QR",
"destination": "https://example.com", "shortUrl": "...",
"totalScans": 1234,
"rules": [{ "name": "iOS Routing", "actionType": "redirect", "enabled": true }],
"createdAt": "...", "updatedAt": "..."
}PATCH/api/v1/qr/:idwrite
Update the destination URL, name, or status of a QR code. The short URL stays the same.
REQUEST BODY
{ "destination": "https://new-url.com", "name": "Updated Name", "status": "active" }RESPONSE
{ "id": "uuid", "shortId": "abc123", "destination": "https://new-url.com", "updatedAt": "..." }DELETE/api/v1/qr/:idwrite
Deactivate a QR code. Scans will be redirected to a locked page. This is a soft delete.
RESPONSE
{ "id": "uuid", "status": "deactivated" }GET/api/v1/qr/:id/analytics?period=30dread
Get scan analytics for a QR code. Returns device, OS, country, and browser breakdowns with daily trend data.
RESPONSE
{
"summary": { "totalScans": 1234, "uniqueCountries": 12 },
"breakdown": {
"devices": { "mobile": 800, "desktop": 400, "tablet": 34 },
"operatingSystems": { "iOS": 500, "Android": 300, "Windows": 400 },
"countries": { "US": 600, "UK": 200 },
"browsers": { "Chrome": 500, "Safari": 400 }
},
"dailyTrend": [{ "date": "2026-05-14", "scans": 42 }],
"recentScans": [{ "device": "mobile", "os": "iOS", "country": "US", "timestamp": "..." }]
}POST/api/v1/qr/batchwrite
Create up to 1,000 QR codes in a single request. Each item in the array is processed independently — partial failures don't block the batch.
REQUEST BODY
{
"codes": [
{ "destination": "https://example.com/1", "name": "Product 1" },
{ "destination": "https://example.com/2", "name": "Product 2" }
]
}RESPONSE
{
"created": 2, "failed": 0, "total": 2,
"results": [
{ "index": 0, "id": "uuid", "shortId": "abc123", "shortUrl": "..." },
{ "index": 1, "id": "uuid", "shortId": "def456", "shortUrl": "..." }
]
}🔔 Webhooks
Register HTTPS endpoints to receive real-time POST notifications when your QR codes are scanned. Webhooks include an HMAC signature for verification.
ℹWebhooks that fail 10 consecutive times are automatically disabled. Check the failure count via the list endpoint and re-enable by re-registering.
POST/api/v1/webhookswrite
Register a new webhook endpoint. Returns a signing secret — save it immediately, it won't be shown again.
REQUEST BODY
{
"url": "https://your-server.com/webhooks/cqr",
"name": "Production Webhook",
"events": ["scan"],
"qrFilter": ["uuid-1", "uuid-2"]
}RESPONSE
{
"id": "uuid",
"url": "https://your-server.com/webhooks/cqr",
"secret": "whsec_a1b2c3d4...",
"events": ["scan"],
"note": "Save this secret — it will not be shown again."
}GET/api/v1/webhooksread
List all registered webhook endpoints with delivery status.
RESPONSE
{
"endpoints": [{ "id": "uuid", "url": "...", "active": true, "failureCount": 0, "lastSuccessAt": "..." }],
"limits": { "used": 1, "max": 100 }
}DELETE/api/v1/webhookswrite
Delete a webhook endpoint by ID.
REQUEST BODY
RESPONSE
{ "id": "webhook-uuid", "status": "deleted" }Verifying Webhook Signatures
Every webhook delivery includes an X-CQR-Signature header. Verify it using your signing secret:
const crypto = require("crypto");
function verifyWebhook(body, timestamp, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(timestamp + "." + body)
.digest("hex");
return signature === "sha256=" + expected;
}
// In your webhook handler:
app.post("/webhooks/cqr", (req, res) => {
const sig = req.headers["x-cqr-signature"];
const ts = req.headers["x-cqr-timestamp"];
if (!verifyWebhook(JSON.stringify(req.body), ts, sig, WEBHOOK_SECRET)) {
return res.status(401).send("Invalid signature");
}
// Process the event...
console.log(req.body.event); // "scan"
console.log(req.body.qrId);
res.status(200).send("OK");
});Webhook payload structure:
{
"event": "scan",
"qrId": "uuid",
"shortId": "abc123",
"qrName": "My QR Code",
"destination": "https://example.com",
"scanData": {
"device": "mobile",
"os": "iOS",
"browser": "Safari",
"country": "US",
"city": "Phoenix",
"timestamp": "2026-05-14T12:00:00Z"
}
}⏱ Rate Limits
Rate limits are enforced per API key per minute. When exceeded, the API returns 429 Too Many Requests with these headers:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 998
X-RateLimit-Reset: 1716681600
Retry-After: 42
❌ Error Handling
All errors return a JSON object with an error field:
{ "error": "Description of what went wrong" }400
Bad Request
Invalid or missing parameters
401
Unauthorized
Missing/invalid API key
403
Forbidden
Tier too low or scope insufficient
404
Not Found
Resource doesn't exist or not owned by you
429
Too Many Requests
Rate limit exceeded
500
Server Error
Something went wrong on our end
🏷 Tier Permissions
CAPABILITY
ARTISAN $39/mo
STEALTH $149/mo
Batch Create (up to 1K)
—
✓
Rate Limit
1,000/min
10,000/min
📋 Changelog
NEWMay 2026
v1 API launch — QR CRUD, analytics, batch, webhooks
🛟 Status & Support
For support, reach out via the dashboard or email support@circulateqr.com.
Ready to build?
Create a free account, upgrade to Artisan or Stealth, and start integrating.