Authentication
How to authenticate against the QR Code Agency API. API keys, rotation, revocation, best practices.
Authentication
Every call to /api/v1/generate/, /api/v1/generate/bulk/, and
/api/v1/usage/ requires an API key in the X-Api-Key header. There is
no OAuth, no JWT, no cookie. One key, one header.
The dashboard endpoints (key management, dynamic QR CRUD, webhooks, billing) use a JWT bearer token instead. You will not need that for a machine-to-machine integration.
API key format
smk_aBc12dEf3GhI4jKlMnOpQrStUvWxYz67890- Always starts with
smk_ - 36 URL-safe characters after the prefix
- Alphabet is letters, digits,
-,_
danger: Treat keys like passwords A key is bearer-only. Anyone holding it can spend your quota and incur overage charges. Never commit a key to git, never embed in client-side JavaScript, never paste in a screenshot for support. We rotate the moment a leak is suspected.
Where to put the key
In every request, set the X-Api-Key header:
cURL
curl -H "X-Api-Key: smk_yOuRkEy" https://api.qrstudio.agency/api/v1/usage/Python
headers = {"X-Api-Key": "smk_yOuRkEy"}
requests.post(url, headers=headers, json=payload)JavaScript
fetch(url, {
method: "POST",
headers: {
"X-Api-Key": "smk_yOuRkEy",
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});C#
httpClient.DefaultRequestHeaders.Add("X-Api-Key", "smk_yOuRkEy");Go
req.Header.Set("X-Api-Key", "smk_yOuRkEy")Create a key
Two paths depending on who you are.
A. From the dashboard (preferred)
- Sign in at qrstudio.agency with your account
- Go to Settings -> API keys -> Create key
- Pick a name (
prod-billing-renderer,staging,marketing-campaign) - Copy the raw key shown once. Paste it into your secrets manager.
B. Programmatically (server-to-server)
If you are authenticated as a Django user holding a JWT, POST to
/api/v1/keys/:
curl -X POST https://api.qrstudio.agency/api/v1/keys/ \
-H "Authorization: Bearer <your-jwt>" \
-H "Content-Type: application/json" \
-d '{"name": "production-server-1", "plan": "starter"}'Response:
{
"id": 7,
"name": "production-server-1",
"raw_key": "smk_aBc12dEf3GhI4jKlMnOpQrStUvWxYz67890",
"key_prefix": "smk_aBc12dEf",
"plan": "starter",
"warning": "Store this key securely. It will NOT be shown again."
}important: One-time disclosure The raw key appears only at creation. We store a SHA-256 hash and cannot recover the original. If you lose it, revoke the old one and create a new one.
List, rotate, and revoke keys
| Action | Endpoint | Result |
|---|---|---|
| List your keys | GET /api/v1/keys/ | Array of metadata. Never the raw key. |
| Rotate a key | POST /api/v1/keys/<id>/rotate/ | Issues a new raw key, old key invalidated immediately. |
| Revoke a key | DELETE /api/v1/keys/<id>/ | Permanently rejects further calls with 401. |
See the full reference in Keys management.
Best practices
tip: Rotate every 90 days Create the new key, deploy it, deploy verification, revoke the old one. Use the rotate endpoint to keep the same key id and audit trail.
tip: One key per environment Do not share keys between dev, staging, and prod. If staging gets compromised, you do not want production exposed.
tip: One key per integration Issue separate keys for your website, mobile app, marketing tool, and internal jobs. The audit log shows which key did what, narrower keys make incidents easier to scope.
tip: Store keys in a secrets manager AWS Secrets Manager, Google Secret Manager, HashiCorp Vault, Doppler, 1Password Secrets Automation, GitHub Actions secrets, Vercel Environment Variables. Do not hardcode in source.
Calling from the browser
Direct browser calls expose your key in DevTools. Always proxy through your own backend:
sequenceDiagram
participant B as Browser
participant Y as Your backend
participant Q as QR Code Agency
B->>Y: POST /generate-qr<br/>(no API key, just a session cookie)
Y->>Y: Authenticate user
Y->>Q: POST /api/v1/generate/<br/>X-Api-Key: smk_...
Q-->>Y: 200 OK + image bytes
Y-->>B: 200 OK + image bytesYour backend authenticates the human (cookie, session, JWT), validates the request, then calls QR Code Agency with the secret key on its own.
warning: CORS is intentionally restrictive The API does not send
Access-Control-Allow-Origin: *. Browser requests are blocked by design. This is not a bug.
How auth resolves on the server
For curiosity, the server tries authentication classes in order until one succeeds:
X-Api-Keyheader -> resolved by SHA-256 hash lookup againstQrApiKey. Returns(owner, key)or falls through.Authorization: Bearer <jwt>-> SimpleJWT validates the access token. Returns the user or falls through.- Session cookie -> Django session middleware. Used by the dashboard exclusively.
Each step returns None on miss to fall through to the next, so a
request carrying both an API key and a session cookie is authenticated
as the API key owner.
Common errors
| Status | Why | Fix |
|---|---|---|
401 | Header missing or wrong format | Include X-Api-Key: smk_... |
401 | Key revoked or rotated | Use the new key or create one |
401 | Hash mismatch (typo, copy paste error) | Verify against the dashboard |
403 | Plan does not allow what you asked | Upgrade plan or reduce request |
429 | Monthly quota hit on Free/Enterprise | Upgrade or wait for the 1st |
For a deeper troubleshooting trail, see Errors.