API Reference
Create and manage short links, their campaign children, their QR codes, and their click stats. Everything you can do in the dashboard, you can do here. Successful responses wrap the result in data; errors return { "error": { "code", "message" } }, where code is stable and message is a sentence for a person.
The full spec is OpenAPI 3.1, at https://1qrl.app/api/v1/openapi.json.
Authentication
Create an API key under Settings, then send it as a bearer token. A key is shown once, when it's made, so store it then.
curl https://1qrl.app/api/v1/links \
-H "Authorization: Bearer sk_live_your_key_here"
Responses and errors
Success wraps the result in data. An error carries a stable code to branch on, and a message written for a person.
{ "error": { "code": "not_found", "message": "Link not found." } }
| 400 | invalid_request | Something in the request is missing or malformed. The message names it. |
| 401 | unauthorized | No valid API key or session was sent, or the key was revoked. |
| 403 | forbidden | The route needs a signed-in session; the key routes refuse API keys. |
| 404 | not_found | No such link or key in your account, or it was deleted; or no logo is saved under that key. |
| 409 | conflict | The custom slug is taken, or the account already holds the most active keys. |
| 429 | rate_limited | A daily limit was reached. Limits reset at midnight UTC. |
| 500 | server_error | Something failed on our side. Try again later. |
Rate limits
Limits apply per account, not per key, and reset at midnight UTC.
- 1,000 API requests a day, shared by all of the account's keys.
- 100 new links a day, campaign links included. Links made in the dashboard and on the homepage count toward the same total.
- 10 active keys at a time.
Every response to a key-authenticated request carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset for the request limit. Going over either daily limit returns 429.
Paging
Lists come newest first, 50 to a page unless you pass limit, up to 100. While there is more, meta.nextCursor is a string: pass it back as cursor for the next page. On the last page it is null.
Endpoints
Links
Short links and the QR style each one carries.
List your links
GET/api/v1/links
Your top-level links, newest first, each with its campaign links. Pages hold 50 links unless you ask for up to 100. To get the next page, pass the previous page's meta.nextCursor as cursor; it is null on the last page.
Parameters
limit | integer | In the query. Links on the page. From 1 to 100. Defaults to 50. |
cursor | string | In the query. The meta.nextCursor of the previous page. Opaque; pass it back unchanged. |
Request
curl https://1qrl.app/api/v1/links \
-H "Authorization: Bearer sk_live_your_key_here"
Response
{
"data": [
{
"id": 4821,
"slug": "spring-menu",
"domain": "1qrl.cc",
"shortUrl": "https://1qrl.cc/spring-menu",
"longUrl": "https://example.com/menus/spring",
"title": "Spring menu",
"qrConfig": {
"fg": "#1B1A20",
"bg": "#FFFFFF",
"dotStyle": "rounded",
"finderStyle": "rounded",
"logo": null
},
"parentId": null,
"utm": {
"source": null,
"medium": null,
"campaign": null
},
"createdAt": "2026-10-05T14:02:11.000Z",
"children": []
}
],
"meta": {
"nextCursor": "4821"
}
}
Statuses
200A page of links.400Something in the request is missing or malformed. The message names it.401No valid API key or session.429A daily limit was reached: API requests, or new links (campaign links included), each counted per account and reset at midnight UTC.500Something failed on our side. Try again later.
Create a link
POST/api/v1/links
Shortens longUrl. Without a slug, 1QRL picks a short random one. Each new link counts toward the account's 100 a day.
Body
longUrlrequired | string | The destination, with its scheme. |
slug | string | A custom slug of 6 to 32 lowercase letters, digits, and hyphens, not starting with a hyphen or holding two in a row. Some words are reserved. A taken slug, including one retired by a delete, is a 409. |
title | string | |
domain | string | The redirect host. Defaults to 1qrl.cc. One of 1qrl.cc. |
qrConfig | QRConfig | |
Request
curl -X POST https://1qrl.app/api/v1/links \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{"longUrl":"https://example.com/menus/spring","slug":"spring-menu","title":"Spring menu"}'
Response
{
"data": {
"id": 4821,
"slug": "spring-menu",
"domain": "1qrl.cc",
"shortUrl": "https://1qrl.cc/spring-menu",
"longUrl": "https://example.com/menus/spring",
"title": "Spring menu",
"qrConfig": {
"fg": "#1B1A20",
"bg": "#FFFFFF",
"dotStyle": "rounded",
"finderStyle": "rounded",
"logo": null
},
"parentId": null,
"utm": {
"source": null,
"medium": null,
"campaign": null
},
"createdAt": "2026-10-05T14:02:11.000Z"
}
}
Statuses
201The new link.400Something in the request is missing or malformed. The message names it.401No valid API key or session.409The custom slug is taken.429A daily limit was reached: API requests, or new links (campaign links included), each counted per account and reset at midnight UTC.500Something failed on our side. Try again later.
Get a link
GET/api/v1/links/{id}
One link, with its campaign links.
Parameters
idrequired | integer | The link's id. A campaign link has its own. |
Request
curl https://1qrl.app/api/v1/links/4821 \
-H "Authorization: Bearer sk_live_your_key_here"
Statuses
200The link.400Something in the request is missing or malformed. The message names it.401No valid API key or session.404No such link in your account, or it was deleted.429A daily limit was reached: API requests, or new links (campaign links included), each counted per account and reset at midnight UTC.
Update a link
PATCH/api/v1/links/{id}
Changes the title, the destination, or the QR style; send only what changes. A new longUrl also moves the link's campaign links, which share it. The slug can't change: printed codes depend on it.
Parameters
idrequired | integer | The link's id. A campaign link has its own. |
Body
title | string or null | |
longUrl | string | |
qrConfig | QRConfig | |
Request
curl -X PATCH https://1qrl.app/api/v1/links/4821 \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{"longUrl":"https://example.com/menus/summer","qrConfig":{"dotStyle":"dots"}}'
Statuses
200The updated link.400Something in the request is missing or malformed. The message names it.401No valid API key or session.404No such link in your account, or it was deleted.429A daily limit was reached: API requests, or new links (campaign links included), each counted per account and reset at midnight UTC.500Something failed on our side. Try again later.
Delete a link
DELETE/api/v1/links/{id}
Deletes the link and its campaign links, and retires their slugs for good: from then on they answer 410 Gone, and no one can claim them again, including you. There is no undo.
Parameters
idrequired | integer | The link's id. A campaign link has its own. |
Request
curl -X DELETE https://1qrl.app/api/v1/links/4821 \
-H "Authorization: Bearer sk_live_your_key_here"
Response
{
"data": {
"id": 4821,
"deleted": true
}
}
Statuses
200The link is deleted.400Something in the request is missing or malformed. The message names it.401No valid API key or session.404No such link in your account, or it was deleted.429A daily limit was reached: API requests, or new links (campaign links included), each counted per account and reset at midnight UTC.
Campaign links
Children of a link that share its destination and add UTM parameters, so each channel's clicks are counted apart.
Create a campaign link
POST/api/v1/links/{id}/children
A new link with its own slug that redirects to the parent's destination with these UTM parameters added. Give at least one. A campaign link can't have children of its own, and it counts toward the daily limit on new links.
Parameters
idrequired | integer | The link's id. A campaign link has its own. |
Body
utmrequired | object | |
utm.source | string | |
utm.medium | string | |
utm.campaign | string | |
Request
curl -X POST https://1qrl.app/api/v1/links/4821/children \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{"utm":{"source":"poster","medium":"print","campaign":"spring"}}'
Response
{
"data": {
"id": 4822,
"slug": "k7m3pq",
"domain": "1qrl.cc",
"shortUrl": "https://1qrl.cc/k7m3pq",
"longUrl": "https://example.com/menus/spring",
"title": null,
"qrConfig": {
"fg": "#1B1A20",
"bg": "#FFFFFF",
"dotStyle": "rounded",
"finderStyle": "rounded",
"logo": null
},
"parentId": 4821,
"utm": {
"source": "poster",
"medium": "print",
"campaign": "spring"
},
"createdAt": "2026-10-05T14:05:40.000Z"
}
}
Statuses
201The new campaign link.400Something in the request is missing or malformed. The message names it.401No valid API key or session.404No such link in your account, or it was deleted.429A daily limit was reached: API requests, or new links (campaign links included), each counted per account and reset at midnight UTC.500Something failed on our side. Try again later.
Delete a campaign link
DELETE/api/v1/links/{id}/children/{childId}
Deletes one campaign link and retires its slug for good, as deleting a link does.
Parameters
idrequired | integer | The link's id. A campaign link has its own. |
childIdrequired | integer | The campaign link's id. |
Request
curl -X DELETE https://1qrl.app/api/v1/links/4821/children/4822 \
-H "Authorization: Bearer sk_live_your_key_here"
Response
{
"data": {
"id": 4822,
"deleted": true
}
}
Statuses
200The campaign link is deleted.400Something in the request is missing or malformed. The message names it.401No valid API key or session.404No such link in your account, or it was deleted.429A daily limit was reached: API requests, or new links (campaign links included), each counted per account and reset at midnight UTC.
QR codes
A link's QR code as an image, in its saved style, and the logos styles carry.
Get a link's QR code
GET/api/v1/links/{id}/qr
The QR code as SVG, in the link's saved style: the same image the dashboard downloads. It encodes the short URL, never the destination, which is what keeps it sparse enough to scan from a distance. Like a logo, its success response is an image rather than JSON; a logo the style has is drawn into the SVG as its bytes, so the file stands alone. It carries an ETag; send it back as If-None-Match and an unchanged code returns an empty 304.
Parameters
idrequired | integer | The link's id. A campaign link has its own. |
If-None-Match | string | In the header. The ETag of a copy you already have. |
Request
curl https://1qrl.app/api/v1/links/4821/qr \
-H "Authorization: Bearer sk_live_your_key_here" \
-o qr.svg
Statuses
200The QR code.304Unchanged since the ETag you sent.400Something in the request is missing or malformed. The message names it.401No valid API key or session.404No such link in your account, or it was deleted.429A daily limit was reached: API requests, or new links (campaign links included), each counted per account and reset at midnight UTC.500Something failed on our side. Try again later.
Get a logo
GET/api/v1/logos/{key}
No authentication.
A saved logo, the image a qrConfig.logo URL points at. It needs no API key or session, so a page can draw it straight from the URL. The key is a hash of the image, so the image at a URL never changes and a browser can keep it for good. Use the URL exactly as given: a query string or another spelling of it is a 404. Anyone with the URL can fetch it, and it can't be guessed. Once no link uses a logo it is deleted, and its URL stops answering within 24 hours.
Parameters
keyrequired | string | The last part of a qrConfig.logo URL. |
Request
curl https://1qrl.app/api/v1/logos/Ab3_-xYz0123456789ABCD \
-o logo
Statuses
200The logo, as the type its bytes are.404No logo is saved under that key.500Something failed on our side. Try again later.
Get a link's click stats
GET/api/v1/links/{id}/stats
Parameters
idrequired | integer | The link's id. A campaign link has its own. |
Request
curl https://1qrl.app/api/v1/links/4821/stats \
-H "Authorization: Bearer sk_live_your_key_here"
Response
{
"data": {
"total": 312,
"daily": [
{
"day": "2026-09-22",
"clicks": 0
},
{
"day": "2026-09-23",
"clicks": 14
}
],
"byCountry": [
{
"country": "US",
"pct": 71
},
{
"country": "CA",
"pct": 18
}
],
"byDevice": [
{
"device": "mobile",
"pct": 83
},
{
"device": "desktop",
"pct": 17
}
],
"byReferrer": [
{
"referrer": "instagram.com",
"pct": 64
}
]
}
}
Statuses
200The link's clicks.400Something in the request is missing or malformed. The message names it.401No valid API key or session.404No such link in your account, or it was deleted.429A daily limit was reached: API requests, or new links (campaign links included), each counted per account and reset at midnight UTC.500Something failed on our side. Try again later.
Profile
Your account's display name and avatar. The email address is shown but can't be changed.
Get your profile
GET/api/v1/profile
Request
curl https://1qrl.app/api/v1/profile \
-H "Authorization: Bearer sk_live_your_key_here"
Statuses
200Your profile.401No valid API key or session.429A daily limit was reached: API requests, or new links (campaign links included), each counted per account and reset at midnight UTC.500Something failed on our side. Try again later.
Update your profile
PATCH/api/v1/profile
Send either field or both; a field you leave out keeps its value. null clears it, and so does a blank displayName. Sending email is refused with 400 invalid_request: an account's address is permanent.
Body
displayName | string or null | At most 64 characters. |
avatar | string or null | One of finder-indigo, module-coral, ring-teal, pair-amber, grid-blue, cross-rose, corner-green, bars-violet, diamond-slate, tail-ink, initials-indigo, initials-coral, initials-teal, initials-amber, initials-blue, initials-rose, initials-green, initials-violet, initials-slate, initials-ink, null. |
Request
curl -X PATCH https://1qrl.app/api/v1/profile \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{"displayName":"Ada Lovelace","avatar":"finder-indigo"}'
Statuses
200Your profile, as saved.400Something in the request is missing or malformed. The message names it.401No valid API key or session.429A daily limit was reached: API requests, or new links (campaign links included), each counted per account and reset at midnight UTC.500Something failed on our side. Try again later.
API keys
Manage the keys themselves. These routes need a signed-in session and refuse API keys, so a leaked key can't mint more.
List your API keys
GET/api/v1/keys
Signed-in session only: API keys get a 403.
Every key the account has made, revoked ones included. Secrets are never returned.
Statuses
200Your keys.401No valid API key or session.403An API key was sent to a route that needs a signed-in session.
Create an API key
POST/api/v1/keys
Signed-in session only: API keys get a 403.
The secret is in data.key in this response and never again; store it now. An account holds up to 10 active keys, and revoking one frees its place.
Body
namerequired | string | 1 to 64 characters. |
Statuses
201The new key, with its secret.400Something in the request is missing or malformed. The message names it.401No valid API key or session.403An API key was sent to a route that needs a signed-in session.409The account already holds the most active keys.500Something failed on our side. Try again later.
Revoke an API key
DELETE/api/v1/keys/{id}
Signed-in session only: API keys get a 403.
The key stops working at once. Revoking can't be undone.
Parameters
idrequired | string | The key's id. |
Statuses
200The key is revoked.401No valid API key or session.403An API key was sent to a route that needs a signed-in session.404No such link in your account, or it was deleted.
Spec
This document, as JSON.
Get this spec
GET/api/v1/openapi.json
No authentication.
This document as OpenAPI 3.1 JSON, for client generators and tools.
Request
curl https://1qrl.app/api/v1/openapi.json