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.
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.
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.
POST Create Action
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.
| 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. |
| 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. |
201 Created with actionId, activeAt, and expiresAt.
429 monthly_quota_exceeded when the monthly action quota is reached.
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"]
{
"actionId": "act_7f3k9mZn4pQ",
"activeAt": "2026-02-19T00:00:00Z",
"expiresAt": "2026-02-19T01:00:00Z"
}
POST Create Action with PIN
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.
| 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. |
{
"payload": {
"fancy": "cat",
"hoot": "woot"
},
"pin": "847291",
"active_at": "2026-02-01T14:00:00Z",
"expires_at": "2026-02-28T14:00:00Z"
}
{
"actionId": "act_9mZn4pQ7f3k",
"activeAt": "2026-02-01T14:00:00Z",
"expiresAt": "2026-02-28T14:00:00Z"
}
POST Create Action Batch
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.
| Content-Type | string | required | Must be text/csv for batch creation. |
| 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. |
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 -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
{
"total": 3,
"created": 3,
"failed": 0,
"results": [
{ "row": 2, "status": "created", "actionId": "act_7f3k9mZn4pQ" }
]
}
GET Verify Action
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.
| actionId | string | required | The ID of the action to retrieve. |
curl https://api.exactonce.com/v1/actions/act_7f3k9mZn4pQ \ -H "client-id: cli_abc123" \ -H "client-secret: sk_live_••••••••"
{
"actionId": "act_7f3k9mZn4pQ",
"state": "active",
"activeAt": "2026-02-19T00:00:00Z",
"expiresAt": "2026-02-19T01:00:00Z",
"pinRequired": false
}
GET List 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.
| 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. |
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_••••••••"
{
"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"
}
POST Consume Action
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.
| actionId | string | required | The ID of the action to consume. |
curl -X POST \ https://api.exactonce.com/v1/actions/act_7f3k9mZn4pQ/consume \ -H "client-id: cli_abc123" \ -H "client-secret: sk_live_••••••••"
{
"actionId": "act_7f3k9mZn4pQ",
"state": "consumed",
"payload": {
"action": "password_reset",
"user_id": "usr_abc123"
},
"consumedAt": "2026-02-19T00:14:33Z"
}
// 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" }
POST Consume Action with PIN
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.
| pin | string | required | The PIN code set during action creation. Must match exactly. |
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": "invalid_pin",
"message": "Invalid PIN",
"attemptsRemaining": 2
}
// Action state is unchanged — still "active"
// After 3 failed attempts, action is burned
DEL Cancel Action
Cancels an unconsumed action. Any subsequent consume attempt returns 410 canceled. The endpoint is idempotent — canceling an already-canceled action returns success.
curl -X DELETE \ https://api.exactonce.com/v1/actions/act_7f3k9mZn4pQ \ -H "client-id: cli_abc123" \ -H "client-secret: sk_live_••••••••"
// Empty response body
GET Get Stats
Returns aggregate usage statistics for your account. Includes counts by outcome and burned PIN actions.
curl https://api.exactonce.com/v1/stats \ -H "client-id: cli_abc123" \ -H "client-secret: sk_live_••••••••"
{
"derived": false,
"stats": {
"created": 14823,
"consumed": 11204,
"expired": 2891,
"canceled": 728,
"invalid_pin_burned": 12
}
}
The Action Object
List endpoints return action summaries in this shape. Some fields may be null or omitted depending on the endpoint.