Quickstart
One API for every brand frontend: submit a code with a phone number and consent, attach a receipt where the campaign needs one, and let the consumer check their entries.
From nothing to a first accepted submission. Under five minutes once Docker is running.
Keys live on your server, never in a browser
@utc/gateway-client refuses to start in a browser for this reason.1. Get a test key
On your machine. From the repository root, with Docker running and Node 22:
pnpm install
pnpm dev:apipnpm dev:api starts Postgres and the storage stand-in, applies the migrations, seeds the test campaigns and prints a test key for the campaign test-native. The key is also kept in ./.env.local as UTC_TEST_KEY_TEST_NATIVE. It is safe to run again. Then start the API in a second terminal:
pnpm --filter @utc/manager devOn staging or production. Ask ops for a test key for your campaign and use that deployment's address as UTC_API_URL. A test key works on every deployment. It never reaches Seebait or TRA, it redeems only test codes, and its rows are kept apart from live ones.
2. Set two variables
Every sample in these docs uses them. In the terminal you will run the curls from, at the repository root:
export UTC_API_URL=http://localhost:3100
export UTC_API_KEY="$(grep '^UTC_TEST_KEY_TEST_NATIVE=' .env.local | cut -d= -f2)"Or paste the key pnpm dev:api printed: export UTC_API_KEY=utc_test_…
3. Two curls
The campaign the key belongs to: its window, what it needs from your page, and every message in the consumer's language.
curl -s "$UTC_API_URL/api/v1/campaign" \
-H "Authorization: Bearer $UTC_API_KEY"A submission. TEST-0-ENTERED is a sentinel: with a test key it is always accepted.
curl -s -X POST "$UTC_API_URL/api/v1/submissions" \
-H "Authorization: Bearer $UTC_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"phone":"0712345678","code":"TEST-0-ENTERED","consent":true}'{
"id": "3f2b8f0e-7c1a-4d55-9a53-0d7e2f5c9b11",
"ref": "K7M2Q9",
"state": "accepted",
"display_status": "entered",
"message_key": "entered",
"message": "You're in. Good luck!",
"entries": 1,
"terminal": true
}That is an accepted submission. Change the code to TEST-0-INVALID and the reply is still HTTP 200, with state: "rejected" and message_key: "invalid": an outcome about the consumer is never an HTTP error. Every sentinel is on Test sentinels.
4. The same with the client
@utc/gateway-client is the typed client for your server. It sets the headers, retries a timeout with the same Idempotency-Key, and can poll a submission until it settles.
import { createClient } from "@utc/gateway-client"
const utc = createClient({ baseUrl: process.env.UTC_API_URL!, apiKey: process.env.UTC_API_KEY! })
const campaign = await utc.campaign()
// In a real page the browser mints this once per attempt and your server forwards it.
const idempotencyKey = crypto.randomUUID()
const submission = await utc.submit({ phone: "0712345678", code: "TEST-0-ENTERED", consent: true }, { idempotencyKey })
console.log(campaign.status, submission.state, submission.message)The paths it calls are the ones above: /api/v1/campaign and /api/v1/submissions.
Next
- Concepts: states, idempotency and replay, test and live.
- Code only or Code and receipt: the guide for your campaign.
- Outcomes: every reply your page can meet, and what to do with each.