Skip to the page
Sections

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.

Note:

Keys live on your server, never in a browser

The key is a secret. It goes in your server's environment and is sent from your server. The browser calls your server; your server calls this API. @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:

Shell
pnpm install
pnpm dev:api

pnpm 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:

Shell
pnpm --filter @utc/manager dev

On 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:

Shell
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
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
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}'
The reply, trimmed
{
  "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.

On your server
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