TN.TC API
Manage QR codes, links, Box, Chat, Bell, sites and Hub through the API. Every request follows your account’s permissions and usage limits.
https://tn.tc/api/v1OpenAPI 3.1OpenAPI 3.1 is the full machine-readable description of the API: import it into Postman, Insomnia or a code generator to build a client. It downloads as a file.
Authentication
Base URL /api/v1. Send Authorization: Bearer tntc_sk_… with every request.
- A key belongs to one workspace and its creator's current role; it can't go beyond its scopes.
- Web sessions and mobile access tokens are separate identities; a key doesn't replace them.
- Never put a key in browser code, a mobile app, a URL or logs. If it leaks, revoke or rotate it immediately.
Scopes
Each endpoint requires a scope; resource endpoints (/resources) check qr, links, box or sites depending on the record's kind. Every operation in the OpenAPI document carries x-tntc-key-scope.
Account and usage
profile:read- Profile, workspaces, team list
profile:write- Profile and notification read state
usage:read- Plan capacity and usage
billing:read- Read membership status
QR
qr:read- Read QR codes, fetch SVG
qr:write- Create, edit, archive QR codes
analytics:read- Scan and visit statistics
Links and addresses
links:read- Read links and address availability
links:write- Create, edit, archive links
Box and files
box:read- Read Boxes, policies and submissions
box:write- Create Boxes, manage policy and review
files:read- File metadata and permitted downloads
files:write- Start/finish uploads, manage files
Chat and Bell
chat:read- Conversations, messages, live stream
chat:write- Send, edit, react to messages
bell:read- Bell orders and their status
bell:write- Bell offer settings and starting orders
Mini sites
sites:read- Read sites, menus, templates and revisions
sites:write- Create sites/menus; edit social links, images and design
sites:publish- Publish
leads:read- Read and export consented contacts (owner/admin)
leads:write- Withdraw consent or erase contacts (owner/admin)
Products & courses
commerce:read- Read products, permitted orders and purchased content
commerce:write- Manage products and courses (no checkout or refund authority)
UGC
ugc:read- Read creators, campaigns, applications
ugc:write- Campaign and application actions
earnings:read- Read earnings (no payouts)
Never available to keys: account deletion and export, starting payments, payouts, team management, key and webhook management, platform administration.
Errors
Errors return a stable machine code and a requestId you can quote to support.
{ "error": { "code": "insufficient_scope", "requestId": "…" } }| HTTP | code |
|---|---|
| 400/422 | invalid_input |
| 401 | unauthorized |
| 403 | insufficient_scope · forbidden · quota_exceeded |
| 404 | not_found |
| 409 | conflict · idempotency_conflict · idempotency_in_progress |
| 413 | invalid_input |
| 429 | rate_limited (Retry-After) |
| 503 | configuration_missing |
Retry-safe requests
Add Idempotency-Key to POST, PATCH and DELETE. The same key and body return the first response for 24 hours (Idempotent-Replayed: true); a different body gets 409 idempotency_conflict; while the first is running you get 409 idempotency_in_progress.
Limits
- Free
- 10/ min
- Pro
- 60/ min
Requests per minute per key: Free 10, Pro 60. Beyond that you get 429 with Retry-After. What you create counts against your plan's resource, storage and Box quotas.
The rate limit is not an uptime guarantee.
Webhooks
Body: id, type, schemaVersion, occurredAt, workspaceId, data. Headers: TNTC-Event-Id, TNTC-Event-Type, TNTC-Delivery-Attempt, TNTC-Signature (t=timestamp, v1=HMAC-SHA256(secret, t + '.' + raw body)). Reject signatures older than five minutes.
resource.created- A QR, link, Box or site was created
resource.updated- A resource changed (version goes up)
resource.archived- A resource was archived
qr.import.completed- A QR import finished (counts and job ID only)
links.import.completed- A link import finished (counts and job ID only)
box.submission.completed- A file arrived in a Box request
file.ready- A file is ready or has a new version
chat.message.created- New message (ids only; no content)
chat.conversation.updated- Group/channel settings or member management changed
bell.order.updated- A Bell order changed status
site.published- A mini site was published
menu.published- A menu was published (price/item changes)
quote.received- A quote request arrived from a site (ids only)
quote.updated- A quote request status or brief Box changed
commerce.product.updated- Product changed
commerce.order.updated- Order changed (ID and action only)
ugc.application.updated- A UGC application changed
lead.confirmed- An email opt-in was confirmed (IDs only)
lead.unsubscribed- A contact withdrew email consent (IDs only)
import { createHmac, timingSafeEqual } from 'node:crypto';
// header: TNTC-Signature: t=<unix seconds>,v1=<hex>[,v1=<hex during rotation>]
export function verifyTntcSignature(secret: string, header: string, rawBody: string) {
const parts = header.split(',');
const t = Number(parts.find((p) => p.startsWith('t='))?.slice(2));
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
return parts
.filter((p) => p.startsWith('v1='))
.some((p) => {
const given = Buffer.from(p.slice(3), 'hex');
return given.length === expected.length && timingSafeEqual(given, expected);
});
}
// Deliveries are at least once: store TNTC-Event-Id and ignore repeats.Examples
curl -sS "https://tn.tc/api/v1/resources?kind=qr&limit=5" \
-H "Authorization: Bearer $TNTC_API_KEY"curl -sS -X POST "https://tn.tc/api/v1/resources" \
-H "Authorization: Bearer $TNTC_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: menu-qr-2026-09-25" \
-d '{"kind":"qr","name":"Menu QR","qrType":"website","targetUrl":"https://example.com/menu"}'const api = 'https://tn.tc/api/v1';
const headers = {
Authorization: `Bearer ${process.env.TNTC_API_KEY}`,
'Content-Type': 'application/json',
};
// Retry-safe create: the same Idempotency-Key returns the first result.
const created = await fetch(`${api}/resources`, {
method: 'POST',
headers: { ...headers, 'Idempotency-Key': crypto.randomUUID() },
body: JSON.stringify({ kind: 'qr', name: 'Menu QR', qrType: 'website', targetUrl: 'https://example.com/menu' }),
});
if (!created.ok) throw new Error((await created.json()).error.code);
const qr = await created.json();
// Change the destination later; the printed QR keeps working.
await fetch(`${api}/resources/${qr.id}`, {
method: 'PATCH',
headers,
body: JSON.stringify({ targetUrl: 'https://example.com/menu-autumn' }),
});