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.1

OpenAPI 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": "…" } }
HTTPcode
400/422invalid_input
401unauthorized
403insufficient_scope · forbidden · quota_exceeded
404not_found
409conflict · idempotency_conflict · idempotency_in_progress
413invalid_input
429rate_limited (Retry-After)
503configuration_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)
Verify a webhook signature (Node.js)
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: list QR codes
curl -sS "https://tn.tc/api/v1/resources?kind=qr&limit=5" \
  -H "Authorization: Bearer $TNTC_API_KEY"
cURL: retry-safe QR create
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"}'
TypeScript
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' }),
});