oqrely API

The REST API covers the codes of a workspace, their scan analytics, static rendering and webhooks. It is described by an OpenAPI 3.1 document; a Postman collection is built from the same file. Try any operation with your key in the interactive API reference.

An AI assistant that supports remote MCP servers can use the same operations through the oqrely MCP server: you sign in once to allow it, and it works within the same scopes and limits as a key. Works with your AI assistant: setup for Claude, ChatGPT, Cursor, VS Code and more.

Quickstart

Create a key in the app under Developers, keep it in the OQRELY_API_KEY environment variable, then create an editable code. The answer holds its short_url and image links.

curl
curl -X POST https://oqrely.com/v1/codes \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"mode":"editable","type":"url","content":{"url":"https://example.com/"} }'
JavaScript
const res = await fetch('https://oqrely.com/v1/codes', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.OQRELY_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ mode: 'editable', type: 'url', content: { url: 'https://example.com/' } }),
});
const code = await res.json();
console.log(code.short_url);
Python
import os, requests

res = requests.post(
    "https://oqrely.com/v1/codes",
    headers={"Authorization": f"Bearer {os.environ['OQRELY_API_KEY']}"},
    json={"mode": "editable", "type": "url", "content": {"url": "https://example.com/"} },
)
print(res.json()["short_url"])
PHP
<?php
$ch = curl_init('https://oqrely.com/v1/codes');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('OQRELY_API_KEY'), 'Content-Type: application/json'],
  CURLOPT_POSTFIELDS => '{"mode":"editable","type":"url","content":{"url":"https://example.com/"} }',
]);
$code = json_decode(curl_exec($ch), true);
echo $code['short_url'];
Ruby
require 'net/http'
require 'json'

uri = URI('https://oqrely.com/v1/codes')
req = Net::HTTP::Post.new(uri, 'Authorization' => "Bearer #{ENV['OQRELY_API_KEY']}", 'Content-Type' => 'application/json')
req.body = { mode: 'editable', type: 'url', content: { url: 'https://example.com/' } }.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') { |h| h.request(req) }
puts JSON.parse(res.body)['short_url']
Go
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"strings"
)

func main() {
	body := strings.NewReader(`{"mode":"editable","type":"url","content":{"url":"https://example.com/"} }`)
	req, _ := http.NewRequest("POST", "https://oqrely.com/v1/codes", body)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("OQRELY_API_KEY"))
	req.Header.Set("Content-Type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer res.Body.Close()
	var code struct {
		ShortURL string `json:"short_url"`
	}
	if err := json.NewDecoder(res.Body).Decode(&code); err != nil {
		log.Fatal(err)
	}
	fmt.Println(code.ShortURL)
}

Keys and scopes

Send the key in the Authorization: Bearer header on every call. A key is shown once when it is created and stored only as a hash; revoke it in the app at any time. Each key works in one workspace and has scopes, reduced to what the plan of the account includes:

  • render: POST /v1/render static images.
  • read: every GET.
  • write: creating and changing codes, folders, tags and webhook endpoints.
  • raw_events: raw scan events, on Business and above.

After a downgrade a key keeps working with the scopes the new plan includes, so write calls answer 403 until the plan allows them again.

Limits, pagination, idempotency

Each plan has a daily quota for API calls and a separate one for renders, counted per account in UTC days (see the pricing page for the numbers). Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix time of the next reset). The first call over the quota answers 429 with Retry-After in seconds. A key can also carry its own lower daily cap.

Lists return { "object": "list", "data": [...], "has_more", "next_cursor" }. Pass next_cursor as cursor to get the next page and limit (1 to 100) for its size. Cursors stay valid while items are added.

Send an Idempotency-Key header with a POST to retry it safely: for 24 hours a repeat with the same key gets the first response again (with Idempotent-Replayed: true) and creates nothing. The same key with a different body answers 422.

Errors

Every error has one shape with a stable code:

Error shape
{ "error": { "code": "validation_failed", "message": "The request is not valid.", "details": { "issues": [{ "path": "content.url", "message": "Required" }] } } }

Common codes: unauthorized, invalid_key, key_revoked, key_expired (401); forbidden_scope, plan_required, step_up_required (403); not_found (404); conflict, slug_taken, invalid_transition (409); validation_failed (422); rate_limited (429).

Sandbox keys

Keys starting with oqr_test_ create sandbox codes. They behave like other codes in the API, but their short links never answer scans, and sandbox keys see only sandbox codes (live keys only live ones). Use them in tests and CI.

Webhooks

On Pro and above, add an https endpoint and choose events; scan.created needs Business or above and arrives in batches, either sampled (about one scan in ten) or full. The signing secret is shown once.

EventWhen
code.createdA fixed or editable code was created.
code.updatedContent, design, rules, link settings, name, folder or tags of a code changed.
code.deletedA code was moved to the trash, or purged from it (`data.permanent`).
code.state_changedA code was turned off or on, archived, restored, blocked, or changed state on schedule.
destination.changedThe destination of an editable code changed.
destination.brokenA destination check found the target unreachable.
code.flaggedA safety check blocked a code.
form.submittedA visitor submitted a form on a hosted page.
page.publishedA hosted page was published.
domain.status_changedA custom domain changed setup, TLS or health status.
bulk.completedA bulk job finished.
billing.state_changedThe plan or billing state of the account changed.
scan.createdScans of the workspace codes, in batches (sampled or full).

Each delivery is a POST with a JSON body { "id", "object": "event", "type", "created", "workspace_id", "data" } and the headers Oqrely-Event, Oqrely-Event-Id, Oqrely-Delivery and Oqrely-Signature: t=<unix time>,v1=<hex>. The signature is HMAC-SHA256 with the endpoint secret over the timestamp, a dot and the raw body. Check it before parsing the body, and reject timestamps older than five minutes:

Verify a signature (Node)
import { createHmac, timingSafeEqual } from 'node:crypto';

// header: the Oqrely-Signature header, rawBody: the request body as received (a string)
export function verify(secret, header, rawBody, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const t = Number(parts.t);
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  return parts.v1?.length === expected.length && timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}

Answer with any 2xx status within 10 seconds. Other answers, timeouts and network errors are retried 7 times, after 1 min, 4 min, 16 min, 1.1 h, 4.3 h, 17.1 h, 24 h. Every attempt is in the delivery log, and any delivery can be sent again from the log or with POST /v1/webhooks/{id}/deliveries/{delivery}/replay (the same body and event id). An endpoint that fails every attempt for a day is turned off and the account owner gets one e-mail; turn it back on in the app or with PATCH /v1/webhooks/{id}.

Reference

Base URL: https://oqrely.com/v1. Request and response fields are in the OpenAPI document.

codes19 endpoints

GET /v1/codes

List codes. Scope: read.

Newest first. Filters combine. Sandbox keys list only sandbox codes.

curl
curl 'https://oqrely.com/v1/codes?limit=10&mode=editable' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/codes

Create a fixed or editable code. Scope: write.

Editable codes get a short link on the main short domain unless `domain_id` names another. Send an `Idempotency-Key` header to retry safely.

curl
curl -X POST 'https://oqrely.com/v1/codes' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"mode":"editable","type":"url","name":"Spring flyer","content":{"url":"https://example.com/spring"},"tags":["print"]}'

GET /v1/codes/{id}

Get a code. Scope: read.

curl
curl 'https://oqrely.com/v1/codes/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

PATCH /v1/codes/{id}

Change content, design, rules, link settings, name or folder. Scope: write.

Live on the next scan. Fields are applied in this order: content, design, rules, link, name, folder; the first refusal stops the rest. A destination change on a high-traffic code needs confirmation in the app (403 step_up_required).

curl
curl -X PATCH 'https://oqrely.com/v1/codes/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content":{"url":"https://example.com/summer"} }'

DELETE /v1/codes/{id}

Move a code to the trash. Scope: write.

The code stops working and can be restored for 30 days (POST /codes/{id}/restore); then it is purged and its short link is retired.

curl
curl -X DELETE 'https://oqrely.com/v1/codes/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/codes/{id}/turn-off

Turn an editable code off. Scope: write.

curl
curl -X POST 'https://oqrely.com/v1/codes/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/turn-off' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/codes/{id}/turn-on

Turn an editable code back on. Scope: write.

curl
curl -X POST 'https://oqrely.com/v1/codes/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/turn-on' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/codes/{id}/archive

Archive a code. Scope: write.

curl
curl -X POST 'https://oqrely.com/v1/codes/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/archive' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/codes/{id}/unarchive

Bring an archived code back. Scope: write.

curl
curl -X POST 'https://oqrely.com/v1/codes/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/unarchive' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/codes/{id}/restore

Restore a code from the trash. Scope: write.

curl
curl -X POST 'https://oqrely.com/v1/codes/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/restore' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

GET /v1/codes/{id}/image

Download the image of a code. Scope: read.

SVG, PNG, PDF or EPS with the code design. Counts against the daily API quota.

curl
curl 'https://oqrely.com/v1/codes/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/image?format=svg' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -o code.svg

POST /v1/codes/{id}/convert

Make an editable copy of a fixed code, or a fixed copy of an editable one. Scope: write.

A new code; the original stays as it is. A fixed copy holds the editable code's current content.

curl
curl -X POST 'https://oqrely.com/v1/codes/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/convert' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/codes/{id}/domain

Move an editable code to another domain. Scope: write.

Its short link moves to that domain. A code that was already downloaded or exported needs `confirm_breaks_printed: true`, because printed copies stop working.

curl
curl -X POST 'https://oqrely.com/v1/codes/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/domain' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain_id":"5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01"}'

GET /v1/codes/{id}/print-record

The note and print runs of a code. Scope: read.

curl
curl 'https://oqrely.com/v1/codes/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/print-record' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

PATCH /v1/codes/{id}/note

Set the note of a code. Scope: write.

Up to 2,000 characters; an empty note removes it.

curl
curl -X PATCH 'https://oqrely.com/v1/codes/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/note' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"note":"Printed for the spring fair."}'

POST /v1/codes/{id}/print-runs

Record a print run of a code. Scope: write.

curl
curl -X POST 'https://oqrely.com/v1/codes/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/print-runs' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"quantity":500,"medium":"Flyer A5","printed_on":"2026-10-01","printer":"City Print"}'

DELETE /v1/codes/{id}/print-runs/{run_id}

Remove a print run from a code. Scope: write.

curl
curl -X DELETE 'https://oqrely.com/v1/codes/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/print-runs/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/codes/{id}/check-again

Ask for a new check of a held code. Scope: write.

When the automatic link check held a code, this runs it again now (10 times a day per code).

curl
curl -X POST 'https://oqrely.com/v1/codes/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/check-again' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

GET /v1/destinations/broken

Editable codes whose destination is down. Scope: read.

Open incidents of the daily destination check, newest first: the code, its URL, the last status or error and since when it fails. A code leaves the list when its destination answers again.

curl
curl 'https://oqrely.com/v1/destinations/broken?limit=20' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

tags3 endpoints

POST /v1/codes/{id}/tags

Add a tag to a code. Scope: write.

Creates the tag in the workspace when it is new.

curl
curl -X POST 'https://oqrely.com/v1/codes/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/tags' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"print"}'

DELETE /v1/codes/{id}/tags/{name}

Remove a tag from a code. Scope: write.

curl
curl -X DELETE 'https://oqrely.com/v1/codes/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/tags/print' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

GET /v1/tags

List tags. Scope: read.

With the number of codes (outside the trash) carrying each tag.

curl
curl 'https://oqrely.com/v1/tags?limit=50' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

analytics6 endpoints

GET /v1/codes/{id}/scans

Scan counts of a code. Scope: read.

Human scans per UTC day, or summed per country or device code, over a range of up to 366 days.

curl
curl 'https://oqrely.com/v1/codes/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/scans?group_by=country' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

GET /v1/codes/{id}/scans/events

Raw scan events of a code. Scope: raw_events.

Newest first, one row per human scan, without any visitor identifier. Needs the raw_events scope (Business and up).

curl
curl 'https://oqrely.com/v1/codes/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/scans/events?limit=50' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/codes/{id}/counter/reset

Set the scan counter of an editable code back to 0. Scope: write.

The analytics history stays; only the counter (and scan limits that read it) starts again.

curl
curl -X POST 'https://oqrely.com/v1/codes/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/counter/reset' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

GET /v1/scans/export

Export the scans of the workspace. Scope: read.

CSV or XLSX for the range (default 30 days). Small exports come back at once (200); larger ones are prepared in the background (202 with the export to fetch).

curl
curl 'https://oqrely.com/v1/scans/export?format=csv&range=30d' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -o code.svg

GET /v1/scans/exports/{id}

Download a prepared scan export. Scope: read.

404 until it is ready, and after it expires.

curl
curl 'https://oqrely.com/v1/scans/exports/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -o code.svg

GET /v1/scans/realtime

Scans of the last 60 minutes. Scope: read.

Human scans per minute and the latest scans, for the workspace or one code.

curl
curl 'https://oqrely.com/v1/scans/realtime' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

render1 endpoint

POST /v1/render

Render a static code image. Scope: render.

Encodes `text` as given, or builds the text from a typed `payload` (the same types as the generator). Returns the image bytes. Daily quota: the plan `api.render` value.

curl
curl -X POST 'https://oqrely.com/v1/render' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"payload":{"type":"url","url":"https://example.com/menu"},"format":"svg"}' \
  -o qr.svg

folders4 endpoints

GET /v1/folders

List folders. Scope: read.

curl
curl 'https://oqrely.com/v1/folders?limit=50' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/folders

Create a folder. Scope: write.

curl
curl -X POST 'https://oqrely.com/v1/folders' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Spring campaign"}'

DELETE /v1/folders/{id}

Delete a folder. Scope: write.

Codes in it move to the top level; nothing is deleted with it but its subfolders.

curl
curl -X DELETE 'https://oqrely.com/v1/folders/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/folders/{id}/restore

Bring back a deleted folder. Scope: write.

The folder, its subfolders and the codes that were in them, unless they moved since.

curl
curl -X POST 'https://oqrely.com/v1/folders/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/restore' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

domains9 endpoints

GET /v1/domains

List domains codes can use. Scope: read.

The oqrely short domains and the custom domains of the workspace. Add and verify custom domains in the app.

curl
curl 'https://oqrely.com/v1/domains?limit=20' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/domains

Add a custom domain. Scope: write.

Returns the DNS record to set. The first check is queued at once; call POST /domains/{id}/check to check again. Apex domains need a provider that flattens CNAMEs (send `flattening_confirmed: true`).

curl
curl -X POST 'https://oqrely.com/v1/domains' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"hostname":"qr.example.com"}'

GET /v1/domains/{id}

Get a custom domain with its setup status. Scope: read.

curl
curl 'https://oqrely.com/v1/domains/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/domains/{id}/check

Check DNS, TLS or health now. Scope: write.

One check for the current setup step, as the setup screen runs while you wait.

curl
curl -X POST 'https://oqrely.com/v1/domains/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/check' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/domains/{id}/default

Make a domain the default for new codes. Scope: write.

Send `unset: true` to go back to the main short domain.

curl
curl -X POST 'https://oqrely.com/v1/domains/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/default' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"unset":false}'

PATCH /v1/domains/{id}/site

Set what the domain root and unknown links answer. Scope: write.

Modes: redirect (to `site_url`), link_list (a page with up to 20 links) or not_found.

curl
curl -X PATCH 'https://oqrely.com/v1/domains/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/site' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"root_mode":"redirect","site_url":"https://example.com"}'

POST /v1/domains/{id}/release-slugs

Release links a previous owner retired on this domain. Scope: write.

Needs a DNS check that verified the domain since you added it, and `confirm: true`: scanners of those old printed codes then reach your links.

curl
curl -X POST 'https://oqrely.com/v1/domains/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/release-slugs' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"confirm":true}'

DELETE /v1/domains/{id}

Remove a custom domain. Scope: write.

Printed codes on it stop working. Send the hostname as `confirm` to say you mean it.

curl
curl -X DELETE 'https://oqrely.com/v1/domains/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01?confirm=qr.example.com' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/destination-domains/verify

Prove you control a destination domain. Scope: write.

Checks the DNS TXT record or the meta tag (`oqrely-verify`) on the destination site. A verified domain lifts the trial rule on new destination domains.

curl
curl -X POST 'https://oqrely.com/v1/destination-domains/verify' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain":"example.com"}'

workspaces14 endpoints

GET /v1/workspaces

List the workspaces of the account. Scope: read.

A key works in one workspace; this lists every workspace of the same account.

curl
curl 'https://oqrely.com/v1/workspaces?limit=20' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

GET /v1/workspaces/current

The workspace of this key. Scope: read.

curl
curl 'https://oqrely.com/v1/workspaces/current' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

PATCH /v1/workspaces/current

Rename the workspace of this key. Scope: write.

curl
curl -X PATCH 'https://oqrely.com/v1/workspaces/current' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Marketing"}'

POST /v1/workspaces

Create a workspace. Scope: write.

Within the plan's workspaces. The key keeps working in its own workspace; create a key in the new one to use it.

curl
curl -X POST 'https://oqrely.com/v1/workspaces' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Events"}'

GET /v1/clients

List client workspaces. Scope: read.

Agency plans: the client workspaces of the account with their codes, scans and members.

curl
curl 'https://oqrely.com/v1/clients' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/clients

Create a client workspace. Scope: write.

curl
curl -X POST 'https://oqrely.com/v1/clients' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Corner Bakery"}'

POST /v1/clients/{pub_id}/invites

Invite someone to a client workspace. Scope: write.

Returns the invitation link once.

curl
curl -X POST 'https://oqrely.com/v1/clients/abc123/invites' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","role":"viewer"}'

POST /v1/clients/{pub_id}/brand-kit

Choose the brand kit of a client workspace. Scope: write.

Its colours and logo brand what the client's scanners see. Leave `brand_kit_id` out to clear it.

curl
curl -X POST 'https://oqrely.com/v1/clients/abc123/brand-kit' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"brand_kit_id":null}'

POST /v1/clients/{pub_id}/copy

Copy codes between your workspace and a client workspace. Scope: write.

Up to 50 codes per call. Copies get new short links; the originals stay.

curl
curl -X POST 'https://oqrely.com/v1/clients/abc123/copy' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"direction":"to_client","code_ids":["5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01"]}'

GET /v1/branding

The branding scanners see. Scope: read.

curl
curl 'https://oqrely.com/v1/branding' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

PATCH /v1/branding

Change the branding scanners see. Scope: write.

Display name, logo (an uploaded image file), accent colour or a brand kit, on plans with custom branding.

curl
curl -X PATCH 'https://oqrely.com/v1/branding' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"display_name":"Corner Bakery","accent":"#0B6E4F"}'

GET /v1/integrations/ga4

The GA4 connection of the workspace. Scope: read.

Whether every human scan is sent to a GA4 web stream as a `qr_scan` event. The API secret is never shown.

curl
curl 'https://oqrely.com/v1/integrations/ga4' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/integrations/ga4

Send scans to GA4. Scope: write.

A measurement id (G-...) and a Measurement Protocol API secret from your GA4 web stream.

curl
curl -X POST 'https://oqrely.com/v1/integrations/ga4' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"measurement_id":"G-ABCD1234","api_secret":"secret-from-ga4"}'

DELETE /v1/integrations/ga4

Stop sending scans to GA4. Scope: write.

curl
curl -X DELETE 'https://oqrely.com/v1/integrations/ga4' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

templates1 endpoint

GET /v1/templates

List templates. Scope: read.

Published oqrely templates and the templates of the workspace.

curl
curl 'https://oqrely.com/v1/templates?kind=design' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

webhooks7 endpoints

GET /v1/webhooks

List webhook endpoints. Scope: read.

curl
curl 'https://oqrely.com/v1/webhooks' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/webhooks

Add a webhook endpoint. Scope: write.

The response carries the signing secret once; store it. URLs must use https and reach a public address.

curl
curl -X POST 'https://oqrely.com/v1/webhooks' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://hooks.example.com/oqrely","events":["code.created","code.updated"]}'

GET /v1/webhooks/{id}

Get a webhook endpoint. Scope: read.

curl
curl 'https://oqrely.com/v1/webhooks/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

PATCH /v1/webhooks/{id}

Change a webhook endpoint. Scope: write.

Send `enabled: true` to turn an endpoint back on after it was turned off for failing.

curl
curl -X PATCH 'https://oqrely.com/v1/webhooks/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"events":["code.created","code.deleted"]}'

DELETE /v1/webhooks/{id}

Delete a webhook endpoint. Scope: write.

Its delivery log goes with it.

curl
curl -X DELETE 'https://oqrely.com/v1/webhooks/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

GET /v1/webhooks/{id}/deliveries

Delivery log of an endpoint. Scope: read.

Newest first, each delivery with every attempt (time, status code, duration, error). Kept 30 days.

curl
curl 'https://oqrely.com/v1/webhooks/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/deliveries?limit=20' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/webhooks/{id}/deliveries/{delivery}/replay

Send a delivery again. Scope: write.

Queues the identical body (same event id) as a new delivery, signed with a fresh timestamp.

curl
curl -X POST 'https://oqrely.com/v1/webhooks/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/deliveries/1/replay' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

pages11 endpoints

GET /v1/pages

List hosted pages. Scope: read.

Newest change first, with whether each is published.

curl
curl 'https://oqrely.com/v1/pages?limit=20' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

GET /v1/pages/{id}

Get a hosted page with its draft blocks. Scope: read.

curl
curl 'https://oqrely.com/v1/pages/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/pages

Create a hosted page from structured fields. Scope: write.

A new editable code with a draft page: a title, an optional subtitle and text, and up to 20 links. Publish it with POST /pages/{id}/publish.

curl
curl -X POST 'https://oqrely.com/v1/pages' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"Spring menu","text":"Fresh every morning.","links":[{"label":"Book a table","url":"https://example.com/book"}]}'

PATCH /v1/pages/{id}

Save the draft blocks of a page. Scope: write.

Adding, changing, moving and removing blocks: send the whole block list. Scanners keep the published version until you publish.

curl
curl -X PATCH 'https://oqrely.com/v1/pages/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"blocks":[{"id":"hd","type":"header","title":"Spring menu"}]}'

POST /v1/pages/{id}/publish

Publish the draft of a page. Scope: write.

Live on the next scan. Refused with 409 and the issues while the draft has publishing errors.

curl
curl -X POST 'https://oqrely.com/v1/pages/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/publish' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/pages/{id}/unpublish

Take a page offline. Scope: write.

curl
curl -X POST 'https://oqrely.com/v1/pages/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/unpublish' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/pages/{id}/revert

Bring back an earlier published version. Scope: write.

The chosen version becomes the live page again.

curl
curl -X POST 'https://oqrely.com/v1/pages/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/revert' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"version":1}'

PATCH /v1/pages/{id}/settings

Change the settings and theme of a page. Scope: write.

Fields you leave out keep their value. `brand_kit_id` applies a brand kit: its colour, typeface and logo.

curl
curl -X PATCH 'https://oqrely.com/v1/pages/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/settings' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"settings":{"title":"Spring menu"},"theme":{"accent":"#0B6E4F"} }'

POST /v1/pages/{id}/languages

Add a language version of a page. Scope: write.

Scanners see the version in their browser language. It starts as a copy of the main language.

curl
curl -X POST 'https://oqrely.com/v1/pages/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/languages' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"lang":"de"}'

PATCH /v1/pages/{id}/languages/{lang}

Set the title and description of a language version. Scope: write.

curl
curl -X PATCH 'https://oqrely.com/v1/pages/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/languages/de' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"Speisekarte","description":"Frisch jeden Morgen."}'

DELETE /v1/pages/{id}/languages/{lang}

Remove a language version of a page. Scope: write.

curl
curl -X DELETE 'https://oqrely.com/v1/pages/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/languages/de' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

submissions1 endpoint

GET /v1/pages/{id}/submissions

List the form responses of a page. Scope: read.

Newest first. Needs the analytics permission in the workspace, like the responses inbox.

curl
curl 'https://oqrely.com/v1/pages/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/submissions?limit=50' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

files4 endpoints

GET /v1/files

List uploaded files. Scope: read.

PDFs and images of the workspace, newest first. Upload new files in the app.

curl
curl 'https://oqrely.com/v1/files?kind=pdf' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

DELETE /v1/files/{id}

Delete a file. Scope: write.

It leaves the file list and the storage meter; pages that still show it keep working until it is purged.

curl
curl -X DELETE 'https://oqrely.com/v1/files/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/files

Upload a PDF or an image. Scope: write.

The file as base64 in JSON. It is checked for malware and type before it can be used (status processing, then ready). Uploading the same bytes again returns the existing file with status 200.

curl
curl -X POST 'https://oqrely.com/v1/files' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"menu.png","content_base64":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGNgYGD4DwABBAEAwS2OUAAAAABJRU5ErkJggg=="}'

members7 endpoints

GET /v1/members

List the members of the workspace. Scope: read.

curl
curl 'https://oqrely.com/v1/members?limit=50' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

GET /v1/members/invites

List open invitations. Scope: read.

curl
curl 'https://oqrely.com/v1/members/invites' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/members/invites

Invite someone to the workspace. Scope: write.

Returns the invitation link once. It works for that e-mail address only, once, for 7 days.

curl
curl -X POST 'https://oqrely.com/v1/members/invites' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","role":"editor"}'

DELETE /v1/members/invites/{id}

Withdraw an invitation. Scope: write.

curl
curl -X DELETE 'https://oqrely.com/v1/members/invites/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

PATCH /v1/members/{user_id}

Change the role of a member. Scope: write.

curl
curl -X PATCH 'https://oqrely.com/v1/members/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"role":"viewer"}'

DELETE /v1/members/{user_id}

Remove a member. Scope: write.

Their codes move to you; their API keys and assistant connections in this workspace stop.

curl
curl -X DELETE 'https://oqrely.com/v1/members/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

PATCH /v1/members/{user_id}/folders

Limit a member to some folders. Scope: write.

An empty list gives the member the whole workspace again.

curl
curl -X PATCH 'https://oqrely.com/v1/members/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/folders' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"folder_ids":[]}'

bulk9 endpoints

GET /v1/bulk

List bulk jobs. Scope: read.

curl
curl 'https://oqrely.com/v1/bulk?limit=20' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/bulk

Create many codes in one job. Scope: write.

Rows of url, name and tags. A request larger than the plan's bulk rows is refused before anything is created.

curl
curl -X POST 'https://oqrely.com/v1/bulk' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"mode":"editable","rows":[{"url":"https://example.com/a","name":"Table 1"}]}'

GET /v1/bulk/{id}

Get a bulk job with its counts. Scope: read.

curl
curl 'https://oqrely.com/v1/bulk/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/bulk/{id}/cancel

Stop a running bulk job. Scope: write.

Rows already done stay done.

curl
curl -X POST 'https://oqrely.com/v1/bulk/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/cancel' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/bulk/preview

Check rows before a bulk job. Scope: write.

Validates every row against the plan's bulk rows and the code rules, without creating anything. Then POST /bulk (or with `dry_run: true`, a dry run you apply later).

curl
curl -X POST 'https://oqrely.com/v1/bulk/preview' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"mode":"editable","rows":[{"url":"https://example.com/a","name":"Table 1"}]}'

POST /v1/bulk/{id}/apply

Apply a dry run. Scope: write.

Runs the planned rows of a finished dry run for real.

curl
curl -X POST 'https://oqrely.com/v1/bulk/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/apply' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/bulk/{id}/undo

Undo a bulk update. Scope: write.

Puts back what an update job changed, within 24 hours of the run.

curl
curl -X POST 'https://oqrely.com/v1/bulk/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/undo' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

GET /v1/bulk/{id}/report

The rows of a bulk job with their result. Scope: read.

curl
curl 'https://oqrely.com/v1/bulk/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/report?limit=100' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

GET /v1/bulk/{id}/files/{name}

Download the ZIP of codes or the print sheet of a job. Scope: read.

`name` is codes.zip or sheet.pdf, as the job made them.

curl
curl 'https://oqrely.com/v1/bulk/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/files/codes.zip' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -o code.svg

brand_kits5 endpoints

GET /v1/brand-kits

List brand kits. Scope: read.

Colors, logos and style the workspace applies to new codes. Pass a kit's design to codes.create as `design`.

curl
curl 'https://oqrely.com/v1/brand-kits' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/brand-kits

Create a brand kit. Scope: write.

curl
curl -X POST 'https://oqrely.com/v1/brand-kits' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Main brand","colors":{"brand":"#0B6E4F","background":"#FFFFFF","accent":"#F2A541","light":"#E8F5EE"},"frameId":null,"domainId":null}'

PATCH /v1/brand-kits/{id}

Change a brand kit. Scope: write.

Send the whole kit, as for create.

curl
curl -X PATCH 'https://oqrely.com/v1/brand-kits/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01' \
  -H "Authorization: Bearer $OQRELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Main brand","colors":{"brand":"#0B6E4F","background":"#FFFFFF","accent":"#F2A541","light":"#E8F5EE"},"frameId":null,"domainId":null}'

DELETE /v1/brand-kits/{id}

Delete a brand kit. Scope: write.

Codes keep their design.

curl
curl -X DELETE 'https://oqrely.com/v1/brand-kits/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

POST /v1/brand-kits/{id}/default

Make a brand kit the default for new codes. Scope: write.

curl
curl -X POST 'https://oqrely.com/v1/brand-kits/5ca1ab1e-c0de-4a11-8e5e-0d0c5a3b1e01/default' \
  -H "Authorization: Bearer $OQRELY_API_KEY"

Versioning and changelog

The version is part of the path (/v1). New optional fields, parameters and event types are added without a new version, so clients should ignore fields they do not know.

Deprecation policy: A breaking change ships as a new version, with at least 12 months of notice before the old version stops. The notice goes to key owners by e-mail and into this changelog.

Changelog

  • v1.1: custom domains, file uploads, page settings and languages, bulk preview, dry runs and undo, print records, scan exports and the real-time view, client workspaces, branding and GA4.
  • v1.0: codes, analytics, render, folders, tags, domains, workspaces, templates and webhooks.