Getting Started

API Reference v1

The ExactOnce API gives you a single primitive: an action that can be consumed exactly once — or not at all. Every action has a defined lifecycle, an expiry window, and an immutable audit trail.

All requests are authenticated via client-id and client-secret headers. All responses are JSON. All timestamps are ISO 8601 UTC.

⚛️
Exactly-once
Atomic conditional writes. One request wins. All others get a typed error.
Time-bounded
Every action has an active_at + expires_at window enforced at the storage layer.
📋
Auditable
Creation and consumption timestamps recorded. Terminal states are immutable.
Base URL https://api.exactonce.com/v1
🕹️ Postman: Download the Postman Collection + Environment bundle here.
Authentication

Header-based auth

Every request must include both a client-id and client-secret header. These are issued when you register for the beta. Treat your client-secret like a password — never expose it in client-side code or commit it to version control.

Server-side only. Your client-secret should only be used from your backend. For client-side token consumption (e.g. a user clicking a link), pass the actionId to your backend and consume there.
Error Handling

Typed, deterministic errors

ExactOnce never returns an ambiguous error. Every failure has a specific HTTP status code and a machine-readable error field. Handle errors by type, not by message string.

200 OK Request succeeded. Action consumed or retrieved.
201 Created Action created successfully.
401 client_auth_required Missing client-id or client-secret.
403 client_auth_failed Invalid client credentials.
401 invalid_pin Wrong PIN. Action remains active unless burned.
404 action_not_found Action ID does not exist or was never created.
409 already_used Action was already consumed.
409 not_active Before the active_at window.
410 expired TTL elapsed. Action window permanently closed.
410 canceled Action was canceled before it could be consumed.
429 monthly_quota_exceeded Monthly action quota exceeded. Try again next month or contact support.
500 server_error Something went wrong on our side. Retry with exponential backoff.
Actions

POST Create Action

POST /v1/actions

Creates a new single-use action with an optional activation window and expiry deadline. Returns an actionId that uniquely identifies this action. The action is immediately consumable unless active_at is set in the future.

Headers
client-id string required Your client identifier.
client-secret string required Your client secret. Keep server-side only.
Content-Type string required Must be application/json.
Body Parameters
payload object optional Arbitrary key-value data attached to this action. Returned on successful consumption. Keep sensitive data as reference IDs, not raw values.
active_at string optional ISO 8601 UTC. Action becomes consumable at this time. Attempts before this return 409 not_active. Defaults to immediately.
expires_at string optional ISO 8601 UTC. Action expires at this time. Attempts after this return 410 expired. Defaults to a 30-day TTL if omitted.
Returns

201 Created with actionId, activeAt, and expiresAt.

429 monthly_quota_exceeded when the monthly action quota is reached.

Request
curl
Node.js
Python
curl -X POST https://api.exactonce.com/v1/actions \
  -H "client-id: cli_abc123" \
  -H "client-secret: sk_live_••••••••" \
  -H "Content-Type: application/json" \
  -d '{
    "payload": {
      "action": "password_reset",
      "user_id": "usr_abc123"
    },
    "active_at":  "2026-02-19T00:00:00Z",
    "expires_at": "2026-02-19T01:00:00Z"
  }'
const res = await fetch(
  'https://api.exactonce.com/v1/actions',
  {
    method: 'POST',
    headers: {
      'client-id':     'cli_abc123',
      'client-secret': process.env.EO_SECRET,
      'Content-Type':  'application/json',
    },
    body: JSON.stringify({
      payload:    { action: 'password_reset', user_id: 'usr_abc123' },
      active_at:  '2026-02-19T00:00:00Z',
      expires_at: '2026-02-19T01:00:00Z',
    }),
  }
);
const { actionId } = await res.json();
import requests

res = requests.post(
  "https://api.exactonce.com/v1/actions",
  headers={
    "client-id":     "cli_abc123",
    "client-secret": os.environ["EO_SECRET"],
  },
  json={
    "payload":    { "action": "password_reset" },
    "expires_at": "2026-02-19T01:00:00Z",
  }
)
action_id = res.json()["actionId"]
Response · 201 Created
{
  "actionId":  "act_7f3k9mZn4pQ",
  "activeAt": "2026-02-19T00:00:00Z",
  "expiresAt": "2026-02-19T01:00:00Z"
}
Actions

POST Create Action with PIN

POST /v1/actions

Identical to Create Action but includes a pin field. The PIN is hashed server-side and never returned in any response. Consumers must supply the correct PIN when calling Consume. A wrong PIN returns 401 invalid_pin and does not consume the action.

Additional Body Parameter
pin string optional A PIN code consumers must provide to consume this action. Hashed on receipt. 4–16 characters recommended. Not returned in any response.
Tip: Use a PIN derived from something the recipient already knows — last 4 digits of phone, partial postcode, or a code sent via a separate channel (SMS). This makes forwarded links useless to strangers.
Request body
{
  "payload": {
    "fancy": "cat",
    "hoot":  "woot"
  },
  "pin":        "847291",
  "active_at":  "2026-02-01T14:00:00Z",
  "expires_at": "2026-02-28T14:00:00Z"
}
Response · 201 Created
{
  "actionId":  "act_9mZn4pQ7f3k",
  "activeAt": "2026-02-01T14:00:00Z",
  "expiresAt": "2026-02-28T14:00:00Z"
}
Actions

POST Create Action Batch

POST /v1/actions

Creates multiple actions in a single request by sending a CSV body. Set Content-Type: text/csv. Each CSV row becomes one action. Provide a payload_json column containing JSON for the action payload. Columns: payload_json, pin, active_at, expires_at.

Headers
Content-Type string required Must be text/csv for batch creation.
CSV Column Reference
payload_json string required JSON payload for the action. Must be valid JSON.
active_at ISO 8601 optional Per-row activation time. Overrides a default if set.
expires_at ISO 8601 optional Per-row expiry. Defaults to a 30-day TTL if omitted.
pin string optional Per-row PIN. Leave empty for no PIN on that row.
Limits: Max 5,000 rows per batch request. Rows with invalid data are reported in the response's results array. Rows over quota return monthly_quota_exceeded.
sample-actions.csv
payload_json,pin,active_at,expires_at
"{\"type\":\"invite\",\"user_id\":\"usr_001\"}",,2026-03-01T00:00:00Z,2026-03-08T00:00:00Z
"{\"type\":\"invite\",\"user_id\":\"usr_002\"}",,2026-03-01T00:00:00Z,2026-03-08T00:00:00Z
"{\"type\":\"promo\",\"user_id\":\"usr_003\",\"discount\":\"20_percent\"}",9823,,2026-02-28T00:00:00Z
curl
curl -X POST https://api.exactonce.com/v1/actions \
  -H "client-id: cli_abc123" \
  -H "client-secret: sk_live_••••••••" \
  -H "Content-Type: text/csv" \
  --data-binary @sample-actions.csv
Response · 200 OK
{
  "total":   3,
  "created": 3,
  "failed":  0,
  "results": [
    { "row": 2, "status": "created", "actionId": "act_7f3k9mZn4pQ" }
  ]
}
Actions

GET Verify Action

GET /v1/actions/{actionId}

Read-only state check. Returns the current state and timing fields for an action. Does not consume the action. Safe to call any number of times. Use this for pre-flight checks, or to power a status page.

Email pre-fetching: Some email clients (Apple Mail, Outlook) pre-fetch links in emails to generate previews. Always use GET /v1/actions/{actionId} to show a confirmation page — require a user action (button click) before calling POST /v1/actions/{actionId}/consume.
Path Parameters
actionId string required The ID of the action to retrieve.
Request
curl https://api.exactonce.com/v1/actions/act_7f3k9mZn4pQ \
  -H "client-id: cli_abc123" \
  -H "client-secret: sk_live_••••••••"
Response · 200 OK
{
  "actionId":        "act_7f3k9mZn4pQ",
  "state":       "active",
  "activeAt":    "2026-02-19T00:00:00Z",
  "expiresAt":   "2026-02-19T01:00:00Z",
  "pinRequired": false
}
Actions

GET List Actions

GET /v1/actions

Returns a paginated list of actions belonging to your account. Filter by state, date range, or consumed reason. Results are cursor-paginated using nextToken.

Query Parameters
limit integer optional Number of results per page. Default 50, max 200.
nextToken string optional Cursor from previous response for pagination.
state string optional Filter by state: pending, active, consumed, expired.
consumed_reason string optional Filter consumed actions by reason. E.g. consumed, invalid_pin_burned.
created_from ISO 8601 optional Return actions created on or after this timestamp.
created_to ISO 8601 optional Return actions created on or before this timestamp.
active_from ISO 8601 optional Return actions with active_at on or after this timestamp.
active_to ISO 8601 optional Return actions with active_at on or before this timestamp.
order_by string optional Sort field: createdAt, activeAt, expiresAt. If omitted, results are unsorted.
order string optional asc or desc. Defaults to desc when order_by is provided.
Request
curl "https://api.exactonce.com/v1/actions?\
  limit=50&\
  state=consumed&\
  created_from=2026-02-01T00:00:00Z&\
  created_to=2026-02-28T23:59:59Z&\
  order_by=createdAt&order=desc" \
  -H "client-id: cli_abc123" \
  -H "client-secret: sk_live_••••••••"
Response · 200 OK
{
  "actions": [
    {
      "actionId":    "act_7f3k9mZn4pQ",
      "state":       "consumed",
      "createdAt":  "2026-02-14T10:00:00Z",
      "activeAt":   "2026-02-14T10:05:00Z",
      "expiresAt":  "2026-02-14T10:30:00Z",
      "pinRequired": false,
      "consumedAt": "2026-02-14T10:22:00Z",
      "consumedReason": "consumed",
      "canceledAt": null
    }
    // ...
  ],
  "nextToken": "eyJsYXN0S2V5IjoiYWN0XzcuLi4ifQ"
}
Actions

POST Consume Action

POST /v1/actions/{actionId}/consume

Atomically consumes an action. This is the critical path. Exactly one concurrent request can succeed — all others receive a typed error. The state transition is enforced at the database level with a conditional write.

Path Parameters
actionId string required The ID of the action to consume.
Response Codes
200 Action consumed. Returns full action object with payload.
409 already_used — previously consumed.
410 expired — past expires_at.
409 not_active — before active_at.
404 action_not_found — action does not exist.
410 canceled — action was canceled.
401 invalid_pin — wrong PIN for a protected action.
Request
curl -X POST \
  https://api.exactonce.com/v1/actions/act_7f3k9mZn4pQ/consume \
  -H "client-id: cli_abc123" \
  -H "client-secret: sk_live_••••••••"
Response · 200 OK
{
  "actionId":        "act_7f3k9mZn4pQ",
  "state":           "consumed",
  "payload": {
    "action":  "password_reset",
    "user_id": "usr_abc123"
  },
  "consumedAt":     "2026-02-19T00:14:33Z"
}
Error responses
// 409 — already used
{ "error": "already_used" }

// 410 — expired
{ "error": "expired" }

// 409 — not active
{ "error": "not_active" }

// 410 — canceled
{ "error": "canceled" }

// 404 — not found
{ "error": "action_not_found" }
Actions

POST Consume Action with PIN

POST /v1/actions/{actionId}/consume

Same as Consume Action, but for PIN-protected actions. Include a JSON body with the pin field. If the PIN is incorrect, returns 401 invalid_pin. After three failed attempts the action is burned and cannot be consumed.

Body Parameters
pin string required The PIN code set during action creation. Must match exactly.
PIN retries: After three failed PIN attempts, the action is burned and cannot be consumed.
Request
curl -X POST \
  https://api.exactonce.com/v1/actions/act_9mZn4pQ7f3k/consume \
  -H "client-id: cli_abc123" \
  -H "client-secret: sk_live_••••••••" \
  -H "Content-Type: application/json" \
  -d '{"pin": "847291"}'
Error · 401 invalid_pin
{
  "error":   "invalid_pin",
  "message": "Invalid PIN",
  "attemptsRemaining": 2
}

// Action state is unchanged — still "active"
// After 3 failed attempts, action is burned
Actions

DEL Cancel Action

DELETE /v1/actions/{actionId}

Cancels an unconsumed action. Any subsequent consume attempt returns 410 canceled. The endpoint is idempotent — canceling an already-canceled action returns success.

Response Codes
204 Action canceled (or already canceled).
409 already_used — action already consumed.
404 action_not_found — action does not exist.
Use case: Cancel when the underlying resource changes. E.g. if a user changes their email mid-flow, cancel the old verification action and issue a new one for the updated address.
Request
curl -X DELETE \
  https://api.exactonce.com/v1/actions/act_7f3k9mZn4pQ \
  -H "client-id: cli_abc123" \
  -H "client-secret: sk_live_••••••••"
Response · 204 No Content
// Empty response body
Analytics

GET Get Stats

GET /v1/stats

Returns aggregate usage statistics for your account. Includes counts by outcome and burned PIN actions.

Request
curl https://api.exactonce.com/v1/stats \
  -H "client-id: cli_abc123" \
  -H "client-secret: sk_live_••••••••"
Response · 200 OK
{
  "derived": false,
  "stats": {
    "created": 14823,
    "consumed": 11204,
    "expired": 2891,
    "canceled": 728,
    "invalid_pin_burned": 12
  }
}
Reference

The Action Object

List endpoints return action summaries in this shape. Some fields may be null or omitted depending on the endpoint.

Action Object
actionId string Unique identifier for the action. Prefix: act_
state enum pending · active · consumed · expired
payload object Arbitrary key-value data set at creation. Returned only on consume.
pinRequired boolean Whether a PIN is required to consume this action. The PIN itself is never returned.
activeAt string ISO 8601 UTC. When the action becomes consumable.
expiresAt string ISO 8601 UTC. When the action expires. Immutable after creation.
createdAt string ISO 8601 UTC. When the action was created.
consumedAt string | null ISO 8601 UTC. When the action was consumed. Null if not yet consumed.
consumedReason string | null Reason for consumption. consumed or invalid_pin_burned.
canceledAt string | null ISO 8601 UTC. When the action was canceled. Null if not canceled.
Reference

States

pending Created with a future active_at. Not yet consumable. Transitions to active automatically.
active Within the consumable window. A consume call will succeed if the action hasn't been consumed yet.
consumed Successfully consumed exactly once. Terminal state. Immutable.
expired Past expires_at or canceled. Canceled actions return reason: "canceled".