Skip to content

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

All requestshttp
https://app.quberoute.com/api/v1

Authentication

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.

Every requestbash
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.

Request and responsebash
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.

Asking for an app that is not yoursbash
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.

FieldRules
webFallbackUrlRequired. Absolute, http or https. Where somebody without the app installed ends up.
deepLinkDataA JSON object, handed to your app when the link opens it. Up to 8,000 characters once serialised.
aliasLetters, 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, feature200 characters each.
tagsUp to 25, each 60 characters. Anything beyond that is dropped silently — you get a 201 and fewer tags than you sent.
expiresAtISO 8601, for example 2026-12-31T23:59:59Z.
iosFallbackUrlOptional, for when iOS should go somewhere other than webFallbackUrl. There is no Android equivalent — Android uses webFallbackUrl.
Creating a linkbash
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"
  }
}
The host of the returned url is your app's subdomainKey — 71k8c above, from GET /api/v1/app — and never your key. In the test environment it carries a -test suffix: 71k8c-test.qbrt.app.

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.

Listing linksbash
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 }
}
Each link carries the same fields as the create response, plus clickCount. Abridged here.

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.

  • 400 invalid_body — the request body is not valid JSON, or is not a JSON object. Nothing was read.
  • 401 unauthorised — the key is missing, malformed, unknown or revoked. All four give the same answer, deliberately.
  • 404 not_found — the thing does not exist, or it is not yours. These are the same response, deliberately.
  • 409 conflict — the change conflicts with the link’s current state; archiving one that is already archived, for instance.
  • 422 invalid_request — the body was read and rejected. It carries a fields object naming each field that failed, so a form can show the errors beside the inputs rather than as one sentence.
    A 422 with no fields, 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.
  • 429 rate_limited — too many link creations. Honour the Retry-After header; 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].

← All documentation