Documentation
API reference
Authentication, the endpoints that exist today, and the error responses you should expect.
This page covers the endpoints you call with a secret key, from your own server. The endpoints your app calls with a publishable key — resolving a link, reporting an install, and erasing everything held about one installation — are on Native and REST.
Base address
https://app.quberoute.com/api/v1Authentication
A bearer token in the Authorization header. Keys look like qr_sk_live_… or qr_sk_test_… — the environment is in the key itself, so one pasted into the wrong configuration is obvious on sight, and a live key committed to a public repository is recognisable to automated scanners.
curl https://app.quberoute.com/api/v1/app \
-H "Authorization: Bearer qr_sk_live_YOUR_KEY"A key belongs to one app and one environment and cannot be moved between them. The server reads both from the key row it just authenticated, never from the request — which is why the endpoint below takes no parameters at all.
Revocation takes effect on the very next request. Nothing is cached anywhere: not in memory, not in a session, not at the edge.
GET /api/v1/app
Returns the app the presented key belongs to.
curl https://app.quberoute.com/api/v1/app \
-H "Authorization: Bearer qr_sk_live_YOUR_KEY"
200 OK
{
"app": {
"id": "0d6f1a3c-2f4b-4c8e-9a1d-7b2c6e5f0a11",
"name": "Northwind Retail",
"subdomainKey": "71k8c",
"linkHost": "71k8c.qbrt.app"
},
"environment": "live"
}GET /api/v1/apps/{appId}
The same data, addressed by identifier. The identifier in the path is compared against the one on your key, and anything else is a 404.
curl https://app.quberoute.com/api/v1/apps/SOMEONE_ELSES_ID \
-H "Authorization: Bearer qr_sk_live_YOUR_KEY"
404 Not Found
{ "error": "not_found", "message": "No such resource." }404, not 403. “Forbidden” would confirm the app exists and belongs to somebody, which is enough to work out who our customers are. “Not found” says nothing.
POST /api/v1/links
Creates a link. It takes no app or environment parameter: both come off the key you authenticated with, so nothing in a request to this endpoint can reach another app, another customer, or the other environment of the same app.
Only webFallbackUrl is required. Everything else is optional, including deepLinkData — an alias you do not supply is generated for you.
| Field | Rules |
|---|---|
webFallbackUrl | Required. Absolute, http or https. Where somebody without the app installed ends up. |
deepLinkData | A JSON object, handed to your app when the link opens it. Up to 8,000 characters once serialised. |
alias | Letters, digits, - and _, starting with a letter or digit; 128 characters. Unique within the app and environment. Some words are reserved. It cannot be changed afterwards. |
title, campaign, channel, feature | 200 characters each. |
tags | Up to 25, each 60 characters. Anything beyond that is dropped silently — you get a 201 and fewer tags than you sent. |
expiresAt | ISO 8601, for example 2026-12-31T23:59:59Z. |
iosFallbackUrl | Optional, for when iOS should go somewhere other than webFallbackUrl. There is no Android equivalent — Android uses webFallbackUrl. |
curl -X POST https://app.quberoute.com/api/v1/links \
-H "Authorization: Bearer qr_sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"alias": "spring-sale",
"deepLinkData": { "path": "/offers/spring" },
"webFallbackUrl": "https://example.com/offers/spring"
}'
201 Created
{
"link": {
"id": "0f9c…",
"alias": "spring-sale",
"url": "https://71k8c.qbrt.app/spring-sale",
"environment": "live",
"title": null,
"deepLinkData": { "path": "/offers/spring" },
"webFallbackUrl": "https://example.com/offers/spring",
"iosFallbackUrl": null,
"campaign": null,
"channel": null,
"feature": null,
"tags": [],
"expiresAt": null,
"archivedAt": null,
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z"
}
}GET /api/v1/links
Lists this app’s links in this environment, newest first. Query parameters: search, tag, status (active, archived, expired; the default is all of them), sort (newest, oldest, alias, clicks), page and perPage, which defaults to 25 and is capped at 200 — ask for more and you get 200, without an error, so page from pagination rather than from the perPage you asked for.
An unrecognised status or sort falls back to the default rather than failing, so a typo returns results rather than an error.
curl "https://app.quberoute.com/api/v1/links?status=active&perPage=2" \
-H "Authorization: Bearer qr_sk_live_YOUR_KEY"
200 OK
{
"links": [
{
"id": "0f9c…",
"alias": "spring-sale",
"url": "https://71k8c.qbrt.app/spring-sale",
"clickCount": 128
}
],
"pagination": { "page": 1, "perPage": 2, "total": 1, "pageCount": 1 }
}GET /api/v1/links/{linkId}
One link, by identifier. The identifier is matched together with the app and environment from your key, so an id belonging to somebody else — or to the same app’s other environment — matches nothing and the answer is 404.
PATCH /api/v1/links/{linkId}
Edits a link. Send only the fields you are changing; the rest are left as they were. The response is the whole link as it now stands.
alias cannot be changed. It is the address, and it may already be printed on something. Sending a different one is a 422 with fields.alias set. Create a second link instead.
DELETE /api/v1/links/{linkId}
This archives the link. It does not delete it. The verb is DELETE because that is what a REST client expects for “take this out of service”, but the link goes on resolving to its web fallback for ever — it may be printed on something nobody can recall. The response returns the link and a note saying so, rather than a bare 204.
Two consequences follow, and neither can be undone. The alias stays taken — archiving spring-sale does not free it for next spring, and recreating it is a 422. There is no unarchive. Nothing in the API or the dashboard brings an archived link back.
Errors
Every failure is { "error": <slug>, "message": <text> }. Branch on the slug; the message is for a person.
400invalid_body— the request body is not valid JSON, or is not a JSON object. Nothing was read.401unauthorised— the key is missing, malformed, unknown or revoked. All four give the same answer, deliberately.404not_found— the thing does not exist, or it is not yours. These are the same response, deliberately.409conflict— the change conflicts with the link’s current state; archiving one that is already archived, for instance.422invalid_request— the body was read and rejected. It carries afieldsobject naming each field that failed, so a form can show the errors beside the inputs rather than as one sentence.
A422with nofields, and a message about your plan, means you have reached your link allowance. Nothing is wrong with the request — do not go looking for it.429rate_limited— too many link creations. Honour theRetry-Afterheader; see Rate limits below.
400 and 422 are next to each other and mean opposite things: invalid_body is “we could not read it”, invalid_request is “we read it and it is wrong”.
Creating links is limited to 300 an hour per app. Over that, the API answers 429 with a Retry-After header giving the seconds to wait — honour it rather than retrying immediately, and a bulk import of more than that should pace itself. Reads are not limited.
Versioning
The version is in the path. Breaking changes appear in the changelog.
Ask the documentation
It answers from these pages only, and links what it used. If the answer is not here it says so rather than guessing — then email [email protected].