API & webhooks

Manage dynamic QR codes from your own systems, and hear about scans, card views and leads the moment they happen. Both are part of OneQR Pro.

Authentication

Create a key in Settings → API keys and send it as a bearer token. Keys act on your own workspace; keep them secret and revoke any that leak.

curl https://oneqr.me/api/v1/codes \
  -H "Authorization: Bearer oqr_your_key"

Successful responses are { "data": … }; failures are { "error": { "message": "…" } } with a 4xx or 5xx status. Each key can make 120 requests a minute.

Endpoints

Base URL: https://oneqr.me/api/v1

GET/codesList every dynamic code in your workspace, newest first, with plan usage.
POST/codesCreate a code. `id` is optional — we pick one if you leave it out.
GET/codes/{id}Get one code.
PATCH/codes/{id}Change any fields, including where it points. Printed codes follow.
DELETE/codes/{id}Delete a code and its analytics. Printed copies stop working.
GET/codes/{id}/stats?days=30Scans, unique visitors, countries, devices, browsers and referrers for 7, 30, 90 or 365 days.
GET/codes/{id}/qr?format=png&size=512The code as a PNG or SVG image in its saved colours.

Create a code

curl https://oneqr.me/api/v1/codes \
  -H "Authorization: Bearer oqr_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "spring-menu",
    "type": "url",
    "values": { "url": "https://example.com/menu" },
    "name": "Spring menu",
    "folder": "Restaurant"
  }'

Other fields: status (active or paused), expiresAt (milliseconds or null), password (empty string removes it), rules (ios, android, and countries as [{ "country": "IN", "to": "…" }]) and design (colours and styles). Types are url, whatsapp (phone, message), location and page for hosted pages.

{
  "data": {
    "id": "spring-menu",
    "to": "https://example.com/menu",
    "type": "url",
    "values": { "url": "https://example.com/menu" },
    "name": "Spring menu",
    "folder": "Restaurant",
    "status": "active",
    "scans": 0,
    "hasPassword": false,
    "createdAt": 1791860000000
  }
}

Webhooks

Add an https endpoint in Settings → Webhooks and pick events. We POST JSON within seconds and wait up to 5 seconds for a 2xx reply. A failed delivery is retried with growing gaps for up to 24 hours; after 20 failed attempts in a row the webhook switches off until you turn it back on. Each delivery carries an id, so you can ignore repeats.

  • qr.scanned — A dynamic QR code or hosted page was scanned
  • profile.viewed — Someone opened one of your digital business cards
  • lead.captured — A visitor left their details on your card
  • order.created — A customer placed an order from one of your catalogs
  • contact.created — Someone sent a message through a landing page's contact form
POST /your-endpoint
OneQR-Event: qr.scanned
OneQR-Signature: t=1791860000,v1=5f2b…

{
  "id": "evt_8c1d…",
  "type": "qr.scanned",
  "created_at": "2026-10-13T08:15:00.000Z",
  "data": {
    "code_id": "spring-menu",
    "scanned_at": "2026-10-13T08:15:00.000Z",
    "country": "IN",
    "city": "Mumbai",
    "os": "Android",
    "device": "mobile",
    "browser": "Chrome",
    "referrer": null,
    "unique": true
  }
}

Check the signature

Sign `${t}.${rawBody}` with your webhook's secret using HMAC-SHA256 and compare it with v1. Reject events older than a few minutes.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return v1.length === expected.length && timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}