WallentoDevelopers
GuideReferenceGet an API key

REST API - v1

Build on Wallento

Connect a POS system, an e-shop, or any other system to a tenant's loyalty program: look up customers, record stamps and points, manage the program and locations, and receive webhooks as things happen.

Get startedJump to reference

one stamp at the till

curl -X POST https://app.wallento.cz/api/v1/transactions \
  -H "Authorization: Bearer wlt_sk_..." \
  -H "Idempotency-Key: receipt-4172" \
  -d '{"cardToken": "wlt_9tPq...", "type": "stamp"}'

201 Created

{
  "transaction": { "type": "stamp", "amount": 1 },
  "customer": {
    "name": "Jana Novakova",
    "loyalty": { "currentStamps": 7, "pendingRewards": 0 }
  },
  "rewardEarned": false
}
Base URL
app.wallento.cz/api
Format
JSON over HTTPS
Auth
Bearer API key
Versioning
/v1, additive only

Guide

  • Getting started
  • Authentication
  • Idempotency
  • Errors
  • Rate limits
  • Webhooks
Reference
  • Customers

    • POST/customers/lookup
    • GET/customers
    • POST/customers
    • GET/customers/{cid}
    • PATCH/customers/{cid}
    • DELETE/customers/{cid}
    • GET/customers/{cid}/transactions
  • Transactions

    • GET/transactions
    • POST/transactions
    • GET/transactions/{txid}
    • POST/transactions/reverse
  • Program

    • GET/program
    • PATCH/program
  • Locations

    • GET/locations
    • POST/locations
    • PATCH/locations/{lid}
  • Reports

    • GET/reports/monthly
  • Broadcasts

    • POST/broadcasts
  • Webhooks

    • GET/webhooks
    • POST/webhooks
    • DELETE/webhooks/{whid}
    • POST/webhooks/{whid}/test
  • API keys

    • GET/apikeys
    • POST/apikeys
    • DELETE/apikeys/{akid}
  • Users

    • GET/users
    • POST/users
    • GET/users/{uid}
    • PATCH/users/{uid}
    • DELETE/users/{uid}
    • POST/users/{uid}/login-link
  • Schemas
On this page
  • Getting started
  • Authentication
  • Idempotency
  • Errors
  • Rate limits
  • Webhooks

Customers

  • POST/customers/lookup
  • GET/customers
  • POST/customers
  • GET/customers/{cid}
  • PATCH/customers/{cid}
  • DELETE/customers/{cid}
  • GET/customers/{cid}/transactions

Transactions

  • GET/transactions
  • POST/transactions
  • GET/transactions/{txid}
  • POST/transactions/reverse

Program

  • GET/program
  • PATCH/program

Locations

  • GET/locations
  • POST/locations
  • PATCH/locations/{lid}

Reports

  • GET/reports/monthly

Broadcasts

  • POST/broadcasts

Webhooks

  • GET/webhooks
  • POST/webhooks
  • DELETE/webhooks/{whid}
  • POST/webhooks/{whid}/test

API keys

  • GET/apikeys
  • POST/apikeys
  • DELETE/apikeys/{akid}

Users

  • GET/users
  • POST/users
  • GET/users/{uid}
  • PATCH/users/{uid}
  • DELETE/users/{uid}
  • POST/users/{uid}/login-link

Getting started

All API access happens over HTTPS from a single base URL. Every endpoint in this reference is relative to it, versioned under /v1.

Base URL

https://app.wallento.cz/api

Create an API key

Every request needs a key. Sign in as a tenant admin, open Settings > API keys, and create one: give it a name and pick the scopes it needs, or bind it to a single location - a register token for one till, which restricts it to the pos scope and stamps every transaction it writes with that location automatically. The plaintext key (wlt_sk_...) is shown exactly once, at creation. If you lose it, revoke it and create a new one - it cannot be shown again.

Make your first request

The primary POS entry point resolves a customer's current loyalty state from a phone number or a wallet card token:

shell

curl https://app.wallento.cz/api/v1/customers/lookup \
  -H "Authorization: Bearer wlt_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"phone": "+420777123456"}'

From here: Authentication covers the full scope list, or jump straight to the API reference for every endpoint.

Authentication

Every request carries a bearer token:

HTTP header

Authorization: Bearer wlt_sk_...

The key determines the tenant and the scopes available to the request - the tenant is never sent anywhere else, there is nowhere to put one. An operation succeeds as soon as the key carries at least one of the scopes it requires.

ScopeGrants
posCustomer lookup, writing transactions, reading one customer and their own history, and reading the program. The only scope a checkout terminal needs.
customers:readList and read customers
customers:writeCreate, update and delete customers
program:readRead the loyalty program configuration
program:writeChange the loyalty program configuration
locations:readList locations
locations:writeCreate and update locations
reports:readRead transactions (for reconciliation) and the monthly stats report
broadcast:sendSend a broadcast push to every customer with a synced wallet pass
webhooks:manageRegister, list, delete and test webhook endpoints

Location-bound keys

A key can optionally be bound to a single location - a register token for one till or POS terminal. A location-bound key is restricted to exactly the pos scope, and every transaction it writes is stamped with that location automatically; sending a different locationId in the body of POST /v1/transactions is rejected. Revoking one register never affects any other key.

Idempotency

POST /v1/customers and POST /v1/transactions require an Idempotency-Key header (1 to 255 characters, chosen by you). Every other POST accepts one but does not require it.

Replay the same key with the exact same request body, and the API returns the original response verbatim - with an Idempotent-Replay: true header - instead of repeating the side effect. The same key with a different body, or a key whose original request is still in flight, returns409 IDEMPOTENCY_CONFLICT. Keys are held for 24 hours.

Validation always runs before a key is claimed. A malformed Idempotency-Key (present but not 1-255 characters) always returns 400 VALIDATION_ERROR, not 409, on every POST that accepts the header; on POST /v1/customers and POST /v1/transactions, a missing one returns the same 400. Either way, fixing the mistake and retrying with the same key is never locked out by the earlier failed attempt.

POST /v1/transactions layers a second, unrelated safety net on top: only the customer's immediately preceding transaction is checked, and when it is of the same resulting kind and was recorded less than 180 seconds ago, the request is rejected as a likely double-scan (409 DUPLICATE_TRANSACTION) unlessforce: true is set - an intervening transaction of a different kind (for example a purchase recorded between two stamps) clears the guard. That guard applies even when every request already carries its own fresh idempotency key - it protects against a cashier pressing the same button twice, not against a network retry.

Errors

Every error response has the same shape:

JSON

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "details": { }
  }
}

code is a stable, machine-readable identifier - integrate against code, never against message, which is meant for logs and may reword between releases. details, when present, is specific to code - the raw validation issues onVALIDATION_ERROR, or { existingCustomerId } on CUSTOMER_EXISTS.

HTTPCode
400
VALIDATION_ERROR
401
UNAUTHORIZED
403
FORBIDDEN_SCOPETENANT_ARCHIVED
404
NOT_FOUND

Also returned for a resource that belongs to a different tenant - the API never confirms another tenant's data exists with a 403.

409
DUPLICATE_TRANSACTIONIDEMPOTENCY_CONFLICTCUSTOMER_EXISTSPROGRAM_LOCKEDWEBHOOK_LIMITCONFLICT

The last three are endpoint-specific - see each operation in the reference for exactly which it can return.

422
NO_PENDING_REWARDINSUFFICIENT_POINTSUNKNOWN_REWARDNEGATIVE_BALANCE

Loyalty-engine rejections, only on POST /v1/transactions - always a genuine business rule, never worth retrying as-is.

429
RATE_LIMITED

Rate limits

Limits are enforced per API key, on a sliding window:

BudgetLimit
Reads600 / minute
Writes300 / minute
POST /v1/broadcasts2 / hour, per tenant
GET /v1/customers?email=

On top of the general read budget - this filter scans the tenant's entire customer set for an exact match.

20 / minute, per tenant
Failed authentication attempts

Per client IP, not per key - once an IP crosses this ceiling, further bad or wrong-scope bearer tokens get a 429 in place of the usual 401/403. A request that authenticates successfully never counts against it, however many valid keys share the IP.

1200 / minute, per IP

Authenticated responses and every 429 carry these headers - a 401 or 403 never does:

HeaderMeaning
X-RateLimit-LimitRequests allowed in the current window.
X-RateLimit-RemainingRequests remaining in the current window.
X-RateLimit-ResetUnix timestamp, in seconds, when the window resets.
Retry-AfterSeconds to wait before retrying. Present only on a 429.

A 401 or 403 itself never carries these headers - but a key that fails to resolve can still trip the failed-authentication budget above first, which returns a header-bearing 429 in its place.

Webhooks

Register an HTTPS endpoint with POST /v1/webhooks (webhooks:manage scope) to receive events as they happen:

EventFires whenPayload data
customer.createdA customer is registered - through this API, the public join page, or POS registration{ customer }
customer.updatedA customer's identity fields change (PATCH /v1/customers/{id}){ customer }
customer.deletedA customer is deleted{ customerId }
transaction.createdAny transaction is recorded - this API, the staff scanner app, or an admin adjustment{ transaction, customer, rewardEarned, levelUp }
broadcast.sentA broadcast push finishes fanning out{ message, queued, failed }
pingOnly sent by POST /v1/webhooks/{id}/test{ }

transaction.created fires for every transaction, however it was written - this API, the staff scanner app, or an admin adjustment - which is what makes it useful for event-driven integrations. POST /v1/webhooks/{id}/test sends a one-off ping event through the exact same delivery path, so you can verify an endpoint and its signature check before relying on it.

Verifying a delivery

Each delivery is a POST of the event JSON with a signature header, computed as hex HMAC-SHA256 of ${t}.${rawBody} using the endpoint's ownwhsec_... secret - shown in full exactly once, at creation; every later response redacts it to the last 4 characters:

HTTP header

Wallento-Signature: t=<unix_ts>,v1=<hex hmac-sha256>

Verify a delivery by recomputing the same HMAC over the raw bytes you received and comparing in constant time, and reject anything whose t is more than 5 minutes from your own clock:

Node.js

const crypto = require('node:crypto')

function verifyWallentoSignature(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map((part) => part.trim().split('='))
  )
  const timestamp = Number(parts.t)
  if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) {
    throw new Error('Signature timestamp missing or outside tolerance')
  }

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex')

  const received = Buffer.from(parts.v1 ?? '', 'hex')
  const expectedBuf = Buffer.from(expected, 'hex')
  if (received.length !== expectedBuf.length || !crypto.timingSafeEqual(received, expectedBuf)) {
    throw new Error('Signature mismatch')
  }
}

// rawBody must be the exact bytes received, before any JSON.parse
verifyWallentoSignature(rawBody, req.headers['wallento-signature'], endpointSecret)

A delivery that fails or exceeds a 10 second timeout is retried on a backoff schedule and eventually moved to a dead letter queue once retries are exhausted. A slow or failing endpoint never blocks or reverts the write that triggered it - a tenant may register at most 5 endpoints.

API reference

Generated from docs/api/openapi.json (version 1.0.0) - every path and method below has a matching route handler, checked by an automated contract test on every change. For scopes, idempotency and the full error code table, see the guide above.

Customers

Lookup, registration, profile and history of a tenant's loyalty customers.

POST/v1/customers/lookupIdempotency-Key accepted

Look up a customer by card token or phone

The primary POS entry point: resolves a customer's current loyalty state from a wallet card token (the raw token, or the full card URL - both are accepted) or from a phone number. Scope: pos. Accepts Idempotency-Key but does not require it.

Parameters

NameInTypeDescription
Idempotency-KeyheaderstringOptional on this endpoint. When present, a retried request with the same key and body replays the original response instead of repeating the side effect - see Idempotency in the API description.
1-255 chars

Request body

Provide exactly one of cardToken or phone.
FieldTypeDescription
cardTokenstringThe wallet card token (wlt_...) or the current scan token (scn_...) printed in the card's QR code, either as the bare token or as its full URL - https://app.wallento.cz/c/<token> or https://app.wallento.cz/s/<token>. A POS scanning a live card's QR always sends the scn_ shape; wlt_ still resolves for cards issued before the scan/install token split that have not been reissued yet.
1-300 chars
phonestringAny reasonably formatted phone number; normalized to E.164 server-side.
1-30 chars

Example

{
  "cardToken": "wlt_9pQ2xN4kTf7Lm1Rv"
}

Responses

StatusCodeDescription
200-Customer found.
400
VALIDATION_ERROR
The request failed validation - a malformed body, an out-of-range or malformed query parameter, or a missing/malformed Idempotency-Key header on an endpoint that requires one. `details` holds the underlying validation issues when the failure is body/query shape.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
404
NOT_FOUND
No resource with this id exists for the calling tenant. Also returned - deliberately indistinguishable from a genuinely missing id - when the id belongs to a DIFFERENT tenant: the API never confirms another tenant's data exists with a 403.
409
IDEMPOTENCY_CONFLICT
This Idempotency-Key was already used with a different request body, or the original request is still in flight.
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

409

{
  "error": {
    "code": "IDEMPOTENCY_CONFLICT",
    "message": "Idempotency-Key was already used with a different request body, or the original request is still in flight"
  }
}

Example request

shell

curl -X POST https://app.wallento.cz/api/v1/customers/lookup \
  -H "Authorization: Bearer wlt_sk_..." \
  -H "Idempotency-Key: a-unique-request-id" \
  -H "Content-Type: application/json" \
  -d '{"cardToken":"wlt_9pQ2xN4kTf7Lm1Rv"}'
GET/v1/customers

List customers

Cursor-paginated. phone and email are mutually exclusive filters; either one returns at most the matching customer(s) as a single, unpaginated page (no nextCursor, regardless of limit/cursor). Without a filter, returns a plain cursor-paginated page of every customer, newest first. Scope: customers:read. The email filter is metered by an additional, tighter per-tenant rate budget on top of the general read limit, since it scans the tenant's entire customer set to find an exact, case-insensitive match.

Parameters

NameInTypeDescription
limitqueryinteger
1-200 - default 50
cursorquerystringOpaque pagination cursor taken from a previous response's `nextCursor`. A malformed value returns 400 VALIDATION_ERROR.
phonequerystringExact phone match, normalized server-side before comparison. Mutually exclusive with `email`.
emailquerystringExact, case-insensitive email match. Mutually exclusive with `phone`.

Responses

StatusCodeDescription
200-A page of customers, or the 0/1-item result of a phone/email filter.
400
VALIDATION_ERROR
The request failed validation - a malformed body, an out-of-range or malformed query parameter, or a missing/malformed Idempotency-Key header on an endpoint that requires one. `details` holds the underlying validation issues when the failure is body/query shape.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
404
NOT_FOUND
No resource with this id exists for the calling tenant. Also returned - deliberately indistinguishable from a genuinely missing id - when the id belongs to a DIFFERENT tenant: the API never confirms another tenant's data exists with a 403.
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

Example request

shell

curl https://app.wallento.cz/api/v1/customers \
  -H "Authorization: Bearer wlt_sk_..."
POST/v1/customersIdempotency-Key required

Register a customer

Registers a customer from a POS or e-shop, deduplicated by phone number. Scope: customers:write. Requires Idempotency-Key. When notify is email (the default) and an email address is provided, the wallet card is emailed to the customer using the same flow as the public join page.

Parameters

NameInTypeDescription
Idempotency-Key*headerstringRequired on this endpoint. A caller-chosen string, unique per logical request, used to make retries safe - see Idempotency in the API description.
1-255 chars

Request body

FieldTypeDescription
name*string
2-100 chars
phone*string
1-30 chars
emailstring
up to 200 chars
marketingOptInboolean
default false
notify"email" | "none"When email and an address is provided, the wallet card is emailed to the customer.
default "email"

Example

{
  "name": "Jana Novakova",
  "phone": "+420777123456",
  "email": "jana.novakova@example.com",
  "marketingOptIn": true,
  "notify": "email"
}

Responses

StatusCodeDescription
201-Customer created.
400
VALIDATION_ERROR
The request failed validation - a malformed body, an out-of-range or malformed query parameter, or a missing/malformed Idempotency-Key header on an endpoint that requires one. `details` holds the underlying validation issues when the failure is body/query shape.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
404
NOT_FOUND
No resource with this id exists for the calling tenant. Also returned - deliberately indistinguishable from a genuinely missing id - when the id belongs to a DIFFERENT tenant: the API never confirms another tenant's data exists with a 403.
409
CUSTOMER_EXISTSIDEMPOTENCY_CONFLICT
Either a customer with this phone number already exists, or this Idempotency-Key was already used with a different request body (or is still in flight).
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

409 - Phone already registered

{
  "error": {
    "code": "CUSTOMER_EXISTS",
    "message": "A customer with this phone number already exists",
    "details": {
      "existingCustomerId": "V1StGXR8_Z5j"
    }
  }
}

409 - Idempotency-Key reused with a different body

{
  "error": {
    "code": "IDEMPOTENCY_CONFLICT",
    "message": "Idempotency key was already used with a different request body, or the original request is still in flight"
  }
}

Example request

shell

curl -X POST https://app.wallento.cz/api/v1/customers \
  -H "Authorization: Bearer wlt_sk_..." \
  -H "Idempotency-Key: a-unique-request-id" \
  -H "Content-Type: application/json" \
  -d '{"name":"Jana Novakova","phone":"+420777123456","email":"jana.novakova@example.com","marketingOptIn":true,"notify":"email"}'
GET/v1/customers/{cid}

Get a customer

Scope: pos or customers:read.

Parameters

NameInTypeDescription
cid*pathstringCustomer id.

Responses

StatusCodeDescription
200-Customer found.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
404
NOT_FOUND
No resource with this id exists for the calling tenant. Also returned - deliberately indistinguishable from a genuinely missing id - when the id belongs to a DIFFERENT tenant: the API never confirms another tenant's data exists with a 403.
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

Example request

shell

curl https://app.wallento.cz/api/v1/customers/V1StGXR8_Z5j \
  -H "Authorization: Bearer wlt_sk_..."
PATCH/v1/customers/{cid}

Update a customer's identity fields

Updates name, email and/or marketingOptIn - identity fields only, never loyalty state (stamps, points, level). phone cannot be changed through this API. Scope: customers:write.

Parameters

NameInTypeDescription
cid*pathstringCustomer id.

Request body

At least one of name, email or marketingOptIn must be provided. phone cannot be changed through this API.
FieldTypeDescription
namestring
2-100 chars
emailstringA valid email address, or an empty string to clear it.
up to 200 chars
marketingOptInboolean

Example

{
  "email": "new-address@example.com"
}

Responses

StatusCodeDescription
200-Customer updated.
400
VALIDATION_ERROR
The request failed validation - a malformed body, an out-of-range or malformed query parameter, or a missing/malformed Idempotency-Key header on an endpoint that requires one. `details` holds the underlying validation issues when the failure is body/query shape.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
404
NOT_FOUND
No resource with this id exists for the calling tenant. Also returned - deliberately indistinguishable from a genuinely missing id - when the id belongs to a DIFFERENT tenant: the API never confirms another tenant's data exists with a 403.
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

Example request

shell

curl -X PATCH https://app.wallento.cz/api/v1/customers/V1StGXR8_Z5j \
  -H "Authorization: Bearer wlt_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"email":"new-address@example.com"}'
DELETE/v1/customers/{cid}

Delete a customer

Erases the customer (GDPR deletion) and best-effort revokes their wallet pass. Scope: customers:write. Irreversible.

Parameters

NameInTypeDescription
cid*pathstringCustomer id.

Responses

StatusCodeDescription
200-Customer deleted.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
404
NOT_FOUND
No resource with this id exists for the calling tenant. Also returned - deliberately indistinguishable from a genuinely missing id - when the id belongs to a DIFFERENT tenant: the API never confirms another tenant's data exists with a 403.
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

Example request

shell

curl -X DELETE https://app.wallento.cz/api/v1/customers/V1StGXR8_Z5j \
  -H "Authorization: Bearer wlt_sk_..."
GET/v1/customers/{cid}/transactions

List a customer's transaction history

Cursor-paginated, newest first. Scope: pos or customers:read.

Parameters

NameInTypeDescription
cid*pathstringCustomer id.
limitqueryinteger
1-200 - default 50
cursorquerystringOpaque pagination cursor taken from a previous response's `nextCursor`. A malformed value returns 400 VALIDATION_ERROR.

Responses

StatusCodeDescription
200-A page of the customer's transactions.
400
VALIDATION_ERROR
The request failed validation - a malformed body, an out-of-range or malformed query parameter, or a missing/malformed Idempotency-Key header on an endpoint that requires one. `details` holds the underlying validation issues when the failure is body/query shape.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
404
NOT_FOUND
No resource with this id exists for the calling tenant. Also returned - deliberately indistinguishable from a genuinely missing id - when the id belongs to a DIFFERENT tenant: the API never confirms another tenant's data exists with a 403.
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

Example request

shell

curl https://app.wallento.cz/api/v1/customers/V1StGXR8_Z5j/transactions \
  -H "Authorization: Bearer wlt_sk_..."

Transactions

The single write endpoint for loyalty activity, plus reconciliation reads.

GET/v1/transactions

List transactions for reconciliation

Every transaction for the tenant, cursor-paginated newest first, with optional date-range, location and type filters. Scope: reports:read.

from/to filter on the underlying score-sorted index server-side; locationId and type filter client-side against the already-fetched page. Because of this, a heavily filtered page can legitimately contain fewer than limit items while nextCursor is still present - keep paging until nextCursor is absent, do not stop merely because a page came back short.

Note type here is the STORED transaction kind (stamp, points_earn, redeem, adjust), not the action name POST /v1/transactions accepts: a spend action produces points_earn, and both redeem_stamp and redeem_points produce redeem.

Parameters

NameInTypeDescription
fromquerystringInclusive lower bound, ISO 8601. Omit for no lower bound.
toquerystringInclusive upper bound, ISO 8601. Omit for no upper bound.
locationIdquerystring
typequery"stamp" | "points_earn" | "redeem" | "adjust"Filters by the stored transaction kind - see the operation description for how this maps from the write endpoint's action names.
limitqueryinteger
1-200 - default 50
cursorquerystringOpaque pagination cursor taken from a previous response's `nextCursor`. A malformed value returns 400 VALIDATION_ERROR.

Responses

StatusCodeDescription
200-A page of transactions.
400
VALIDATION_ERROR
The request failed validation - a malformed body, an out-of-range or malformed query parameter, or a missing/malformed Idempotency-Key header on an endpoint that requires one. `details` holds the underlying validation issues when the failure is body/query shape.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

Example request

shell

curl https://app.wallento.cz/api/v1/transactions \
  -H "Authorization: Bearer wlt_sk_..."
POST/v1/transactionsIdempotency-Key required

Record a stamp, spend or redemption

The API's single write endpoint - a thin facade over the same optimistic-concurrency commit, loyalty-engine dispatch and wallet-pass sync the staff scanner app uses. Scope: pos. Requires Idempotency-Key.

Resolve the customer with exactly one of customerId or cardToken. spentCzk is required when type is spend; rewardId is required when type is redeem_points. type must also match the tenant's own program - stamp/redeem_stamp on a points program, or spend/redeem_points on a stamps program, is rejected with 400 VALIDATION_ERROR rather than silently recording a no-op transaction. Only the customer's immediately preceding transaction is checked for a duplicate: when it is of the same resulting kind and was recorded less than 180 seconds ago, the request is rejected as a likely double-scan (409 DUPLICATE_TRANSACTION) unless force: true is set - an intervening transaction of a different kind (for example a purchase recorded between two stamps) clears the guard. This is a separate concern from the Idempotency-Key envelope above it, which protects against network retries; this guard protects against a cashier pressing the same button twice, each press carrying its own fresh key.

A location-bound key stamps its own locationId automatically; an unbound key may send any locationId belonging to its own tenant.

Parameters

NameInTypeDescription
Idempotency-Key*headerstringRequired on this endpoint. A caller-chosen string, unique per logical request, used to make retries safe - see Idempotency in the API description.
1-255 chars

Request body

Provide exactly one of customerId or cardToken. spentCzk is required when type is spend; rewardId is required when type is redeem_points.
FieldTypeDescription
customerIdstring
1-64 chars
cardTokenstringThe wallet card token (wlt_...) or the current scan token (scn_...) printed in the card's QR code, either as the bare token or as its full URL - https://app.wallento.cz/c/<token> or https://app.wallento.cz/s/<token>. Same lookup rules as CustomerLookupRequest.cardToken.
1-300 chars
type*"stamp" | "spend" | "redeem_stamp" | "redeem_points"Must match the tenant's own program: stamp/redeem_stamp on a stamps program, spend/redeem_points on a points program. A mismatch returns 400 VALIDATION_ERROR.
spentCzkintegerRequired when type is spend.
1-1000000
rewardIdstringRequired when type is redeem_points; the id of one of the program's configured point rewards.
at least 1 chars
externalRefstringPartner-supplied receipt/order id, echoed back on the transaction.
1-64 chars
locationIdstringOnly meaningful for an unbound key that wants to attribute this transaction to a specific location. A location-bound key stamps its own location automatically; sending a different id here is rejected.
1-64 chars
forcebooleanBypasses the 180 second duplicate-transaction guard.
default false

Example

{
  "customerId": "V1StGXR8_Z5j",
  "type": "stamp",
  "externalRef": "receipt-10245"
}

Responses

StatusCodeDescription
201-Transaction recorded.
400
VALIDATION_ERROR
The request failed validation - a malformed body, an out-of-range or malformed query parameter, or a missing/malformed Idempotency-Key header on an endpoint that requires one. `details` holds the underlying validation issues when the failure is body/query shape.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
404
NOT_FOUND
No resource with this id exists for the calling tenant. Also returned - deliberately indistinguishable from a genuinely missing id - when the id belongs to a DIFFERENT tenant: the API never confirms another tenant's data exists with a 403.
409
DUPLICATE_TRANSACTIONIDEMPOTENCY_CONFLICTCONFLICT
The customer's immediately preceding transaction was of the same kind and was recorded very recently, and `force` was not set; this Idempotency-Key was already used with a different body (or is still in flight); or a rare optimistic-concurrency conflict that a straight retry resolves.
422
NO_PENDING_REWARDINSUFFICIENT_POINTSUNKNOWN_REWARDNEGATIVE_BALANCE
The loyalty engine rejected this transaction - always a genuine business rule, never a transient condition worth retrying as-is.
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

201 - Stamp awarded, card completed

{
  "transaction": {
    "id": "Qx8mLp2Kz9Rt",
    "customerId": "V1StGXR8_Z5j",
    "type": "stamp",
    "amount": 1,
    "spentCzk": null,
    "rewardId": null,
    "locationId": "Nk4pXz9Lm2Qr",
    "actor": "api",
    "externalRef": "receipt-10245",
    "note": null,
    "createdAt": "2026-08-09T14:32:10.000Z"
  },
  "customer": {
    "id": "V1StGXR8_Z5j",
    "name": "Jana Novakova",
    "phone": "+420777123456",
    "email": "jana.novakova@example.com",
    "marketingOptIn": true,
    "cardToken": "wlt_9pQ2xN4kTf7Lm1Rv",
    "cardUrl": "https://app.wallento.cz/c/wlt_9pQ2xN4kTf7Lm1Rv",
    "passState": "pending",
    "visits": 9,
    "firstTxAt": "2026-06-02T08:12:00.000Z",
    "lastTxAt": "2026-08-12T09:41:02.000Z",
    "originLocationId": "Nk4pXz9Lm2Qr",
    "signupRef": "U008-1043",
    "loyalty": {
      "programType": "stamps",
      "currentStamps": 0,
      "pendingRewards": 1,
      "pointsBalance": 0,
      "lifetimeStamps": 9,
      "lifetimePoints": 0,
      "level": 1,
      "levelName": "Gold"
    },
    "createdAt": "2026-06-01T09:00:00.000Z",
    "registeredAt": "2026-06-01T09:00:00.000Z"
  },
  "rewardEarned": true,
  "earnedPoints": null,
  "levelUp": {
    "name": "Gold",
    "threshold": 9
  }
}

409 - Likely double-scan

{
  "error": {
    "code": "DUPLICATE_TRANSACTION",
    "message": "A transaction of the same type was recorded very recently",
    "details": {
      "lastTxAgoSec": 4
    }
  }
}

409 - Idempotency-Key reused with a different body

{
  "error": {
    "code": "IDEMPOTENCY_CONFLICT",
    "message": "Idempotency-Key was already used with a different request body, or the original request is still in flight"
  }
}

409 - Optimistic-concurrency conflict - safe to retry

{
  "error": {
    "code": "CONFLICT",
    "message": "Conflict, please retry"
  }
}

422 - redeem_stamp with nothing to redeem

{
  "error": {
    "code": "NO_PENDING_REWARD",
    "message": "No pending reward to redeem"
  }
}

422 - redeem_points, not enough balance

{
  "error": {
    "code": "INSUFFICIENT_POINTS",
    "message": "Not enough points for this reward"
  }
}

422 - rewardId does not exist on this program

{
  "error": {
    "code": "UNKNOWN_REWARD",
    "message": "Unknown reward"
  }
}

422 - Would drive the points balance negative

{
  "error": {
    "code": "NEGATIVE_BALANCE",
    "message": "Resulting balance would be negative"
  }
}

Example request

shell

curl -X POST https://app.wallento.cz/api/v1/transactions \
  -H "Authorization: Bearer wlt_sk_..." \
  -H "Idempotency-Key: a-unique-request-id" \
  -H "Content-Type: application/json" \
  -d '{"customerId":"V1StGXR8_Z5j","type":"stamp","externalRef":"receipt-10245"}'
GET/v1/transactions/{txid}

Get a transaction

Scope: reports:read.

Parameters

NameInTypeDescription
txid*pathstringTransaction id.

Responses

StatusCodeDescription
200-Transaction found.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
404
NOT_FOUND
No resource with this id exists for the calling tenant. Also returned - deliberately indistinguishable from a genuinely missing id - when the id belongs to a DIFFERENT tenant: the API never confirms another tenant's data exists with a 403.
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

Example request

shell

curl https://app.wallento.cz/api/v1/transactions/Qx8mLp2Kz9Rt \
  -H "Authorization: Bearer wlt_sk_..."
POST/v1/transactions/reverseIdempotency-Key required

Undo the loyalty effect of a voided receipt

Takes back what one receipt did to a customer's balance, after the till voided that receipt. Scope: pos. Requires Idempotency-Key.

The receipt is identified by the externalRef its transactions were recorded with, not by transaction id - one call undoes everything that receipt did, both the reward redemption and the points earned on it. Records exactly ONE adjust transaction carrying the net inverse, with a mandatory note: deliberately the same instrument a tenant admin uses by hand in the admin UI, because that hand correction is what this endpoint automates. That means it inherits the same semantics: the CURRENT balance moves, lifetimePoints and level do not, and the monthly report still counts the voided receipt as a visit.

Only transactions the customer still has outstanding are reversed. A second call for the same receipt returns 200 with alreadyReversed: true and changes nothing - a store-and-forward till whose queue outlives the 24 hour Idempotency-Key window must not be told a correction failed when it had in fact already landed.

If the customer has since spent the points, the correction is CLAMPED at a zero balance rather than rejected: by the time this call is made the money has already gone back over the counter. requestedPoints and appliedPoints differ in that case, clamped is true, and the shortfall is stated in the transaction's note.

Points programs only - on a stamps program the inverse of a redemption is a pending reward, which an adjustment cannot give back, so the request is rejected with 400 VALIDATION_ERROR instead of silently corrupting the stamp card. The lookup scans this customer's most recent transactions; a receipt older than that window returns 404 NOT_FOUND with details.scanned rather than a silent no-op.

Parameters

NameInTypeDescription
Idempotency-Key*headerstringRequired on this endpoint. A caller-chosen string, unique per logical request, used to make retries safe - see Idempotency in the API description.
1-255 chars

Request body

FieldTypeDescription
customerId*string
1-64 chars
reversesRef*stringThe externalRef the receipt's transactions were recorded with.
1-64 chars
notestringShown to the tenant on the resulting adjustment. Defaults to "Reversal of <reversesRef>".
1-300 chars

Example

{
  "customerId": "V1StGXR8_Z5j",
  "reversesRef": "receipt-10246",
  "note": "Storno uctenky c. 10246"
}

Responses

StatusCodeDescription
200-Every transaction on this receipt had already been reversed; nothing changed.
201-Reversal recorded.
400
VALIDATION_ERROR
The request failed validation - a malformed body, an out-of-range or malformed query parameter, or a missing/malformed Idempotency-Key header on an endpoint that requires one. `details` holds the underlying validation issues when the failure is body/query shape.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
404
NOT_FOUND
Unknown customer, or no reversible transaction with this `externalRef` within the scanned window.
409-This Idempotency-Key was already used with a different body (or is still in flight), or a rare optimistic-concurrency conflict that a straight retry resolves.
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

201 - Receipt earned 27 points, all of them taken back

{
  "transaction": {
    "id": "Zt4nWq8Bx2Cf",
    "customerId": "V1StGXR8_Z5j",
    "type": "adjust",
    "amount": -27,
    "spentCzk": null,
    "rewardId": null,
    "locationId": "Nk4pXz9Lm2Qr",
    "actor": "api",
    "externalRef": "receipt-10246",
    "note": "Storno uctenky c. 10246",
    "createdAt": "2026-08-12T09:41:02.000Z"
  },
  "customer": {
    "id": "V1StGXR8_Z5j",
    "name": "Jana Novakova",
    "phone": "+420777123456",
    "email": "jana.novakova@example.com",
    "marketingOptIn": true,
    "cardToken": "wlt_9pQ2xN4kTf7Lm1Rv",
    "cardUrl": "https://app.wallento.cz/c/wlt_9pQ2xN4kTf7Lm1Rv",
    "passState": "pending",
    "visits": 14,
    "firstTxAt": "2026-06-02T08:12:00.000Z",
    "lastTxAt": "2026-08-12T09:41:02.000Z",
    "originLocationId": "Nk4pXz9Lm2Qr",
    "signupRef": "U008-1043",
    "loyalty": {
      "programType": "points",
      "currentStamps": 0,
      "pendingRewards": 0,
      "pointsBalance": 156,
      "lifetimeStamps": 0,
      "lifetimePoints": 183,
      "level": 0,
      "levelName": "Bronz"
    },
    "createdAt": "2026-06-01T09:00:00.000Z",
    "registeredAt": "2026-06-01T09:00:00.000Z"
  },
  "reversed": [
    {
      "id": "Qx8mLp2Kz9Rt",
      "customerId": "V1StGXR8_Z5j",
      "type": "points_earn",
      "amount": 27,
      "spentCzk": 270,
      "rewardId": null,
      "locationId": "Nk4pXz9Lm2Qr",
      "actor": "api",
      "externalRef": "receipt-10246",
      "note": null,
      "createdAt": "2026-08-12T09:12:44.000Z"
    }
  ],
  "requestedPoints": -27,
  "appliedPoints": -27,
  "clamped": false,
  "alreadyReversed": false
}

404 - Receipt not found in this customer's recent history

{
  "error": {
    "code": "NOT_FOUND",
    "message": "No reversible transaction with this externalRef for this customer",
    "details": {
      "scanned": 200
    }
  }
}

Example request

shell

curl -X POST https://app.wallento.cz/api/v1/transactions/reverse \
  -H "Authorization: Bearer wlt_sk_..." \
  -H "Idempotency-Key: a-unique-request-id" \
  -H "Content-Type: application/json" \
  -d '{"customerId":"V1StGXR8_Z5j","reversesRef":"receipt-10246","note":"Storno uctenky c. 10246"}'

Program

The tenant's loyalty program configuration - stamps or points, rewards, levels.

GET/v1/program

Get the loyalty program configuration

Scope: pos or program:read.

Responses

StatusCodeDescription
200-Program configuration.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
404
NOT_FOUND
No resource with this id exists for the calling tenant. Also returned - deliberately indistinguishable from a genuinely missing id - when the id belongs to a DIFFERENT tenant: the API never confirms another tenant's data exists with a 403.
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

Example request

shell

curl https://app.wallento.cz/api/v1/program \
  -H "Authorization: Bearer wlt_sk_..."
PATCH/v1/program

Update the loyalty program configuration

All fields optional; an unspecified field keeps its current value. Rewards are merged by id - omit id to mint a new reward, include an existing one to update it in place; sending rewards replaces the full array (any existing reward not included is dropped). levels, when sent, replaces the full array and thresholds must be strictly increasing. Scope: program:write.

Request body

All fields optional; an unspecified field keeps its current value. Changing programType while the tenant has any customers returns 409 PROGRAM_LOCKED.
FieldTypeDescription
programType"stamps" | "points"
stampsRequiredinteger
3-20
rewardTextstring
1-120 chars
pointsPerCzknumber
0.01-10
rewardsarray of RewardInput
0-200 items
levelsarray of LevelDefReplaces the full levels array. Thresholds must be strictly increasing.
0-20 items

Example

{
  "stampsRequired": 10,
  "rewardText": "Free coffee"
}

Responses

StatusCodeDescription
200-Program updated.
400
VALIDATION_ERROR
The request failed validation - a malformed body, an out-of-range or malformed query parameter, or a missing/malformed Idempotency-Key header on an endpoint that requires one. `details` holds the underlying validation issues when the failure is body/query shape.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
404
NOT_FOUND
No resource with this id exists for the calling tenant. Also returned - deliberately indistinguishable from a genuinely missing id - when the id belongs to a DIFFERENT tenant: the API never confirms another tenant's data exists with a 403.
409
PROGRAM_LOCKED
Attempted to change `programType` while the tenant already has customers.
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

409

{
  "error": {
    "code": "PROGRAM_LOCKED",
    "message": "Cannot change program type when customers exist"
  }
}

Example request

shell

curl -X PATCH https://app.wallento.cz/api/v1/program \
  -H "Authorization: Bearer wlt_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"stampsRequired":10,"rewardText":"Free coffee"}'

Locations

Physical locations (branches/tills) a tenant operates.

GET/v1/locations

List locations

Every location for the tenant, unpaginated. Scope: locations:read - deliberately not accepted with pos: the response includes each location's staff login code, and a location-bound key must stay unable to enumerate every other branch's login code.

Responses

StatusCodeDescription
200-All locations.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

Example request

shell

curl https://app.wallento.cz/api/v1/locations \
  -H "Authorization: Bearer wlt_sk_..."
POST/v1/locationsIdempotency-Key accepted

Create a location

Scope: locations:write. Accepts Idempotency-Key but does not require it. A unique staff login code is minted server-side.

Parameters

NameInTypeDescription
Idempotency-KeyheaderstringOptional on this endpoint. When present, a retried request with the same key and body replays the original response instead of repeating the side effect - see Idempotency in the API description.
1-255 chars

Request body

Breaking change on 2026-08-14: `address` is now required. Existing POST /v1/locations clients must provide it; prior to that date, it was optional.
FieldTypeDescription
name*string
1-80 chars
pin*string4 to 6 digit PIN staff use with the location's login code at /scan/{code}. Never returned by the API.
pattern ^\d{4,6}$
address*stringPostal address. The server geocodes it via Google Geocoding API to derive `lat`/`lng`/`geoConfidence` in the response. Normalization (trimming whitespace) is server-side. If geocoding fails or `GOOGLE_GEOCODING_API_KEY` is unconfigured, the location is still created but without coordinates - the admin UI flags it for manual follow-up.
5-200 chars

Example

{
  "name": "Vinohrady",
  "pin": "4821",
  "address": "Korunni 15, 120 00 Praha 2"
}

Responses

StatusCodeDescription
201-Location created.
400
VALIDATION_ERROR
The request failed validation - a malformed body, an out-of-range or malformed query parameter, or a missing/malformed Idempotency-Key header on an endpoint that requires one. `details` holds the underlying validation issues when the failure is body/query shape.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
409
CONFLICTIDEMPOTENCY_CONFLICT
Either the generated location code collided after several attempts (retry), or this Idempotency-Key was already used with a different body (or is still in flight).
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

409 - Code collision, retry

{
  "error": {
    "code": "CONFLICT",
    "message": "Location code already exists"
  }
}

409 - Idempotency-Key reused with a different body

{
  "error": {
    "code": "IDEMPOTENCY_CONFLICT",
    "message": "Idempotency-Key was already used with a different request body, or the original request is still in flight"
  }
}

Example request

shell

curl -X POST https://app.wallento.cz/api/v1/locations \
  -H "Authorization: Bearer wlt_sk_..." \
  -H "Idempotency-Key: a-unique-request-id" \
  -H "Content-Type: application/json" \
  -d '{"name":"Vinohrady","pin":"4821","address":"Korunni 15, 120 00 Praha 2"}'
PATCH/v1/locations/{lid}

Update a location

At least one of name, address, or pin must be provided. Changing the PIN immediately revokes every active staff session for this location. Changing the address re-geocodes it server-side. Scope: locations:write.

Parameters

NameInTypeDescription
lid*pathstringLocation id.

Request body

At least one of name, address or pin must be provided. Changing the PIN revokes every active staff session for this location. Changing the address re-geocodes it server-side.
FieldTypeDescription
namestring
1-80 chars
addressstringPostal address. When provided, the server re-geocodes it - even if it is byte-identical to the address already on file, as long as the location has no coordinates yet. If geocoding fails or `GOOGLE_GEOCODING_API_KEY` is unconfigured, the location is still updated but any previously derived lat/lng/geoConfidence are cleared (returned as null) rather than left pointing at the address being replaced.
5-200 chars
pinstring
pattern ^\d{4,6}$

Example

{
  "name": "Vinohrady - Korunni"
}

Responses

StatusCodeDescription
200-Location updated.
400
VALIDATION_ERROR
The request failed validation - a malformed body, an out-of-range or malformed query parameter, or a missing/malformed Idempotency-Key header on an endpoint that requires one. `details` holds the underlying validation issues when the failure is body/query shape.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
404
NOT_FOUND
No resource with this id exists for the calling tenant. Also returned - deliberately indistinguishable from a genuinely missing id - when the id belongs to a DIFFERENT tenant: the API never confirms another tenant's data exists with a 403.
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

Example request

shell

curl -X PATCH https://app.wallento.cz/api/v1/locations/Nk4pXz9Lm2Qr \
  -H "Authorization: Bearer wlt_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"name":"Vinohrady - Korunni"}'

Reports

Aggregate monthly statistics.

GET/v1/reports/monthly

Get a tenant's monthly stats

Scope: reports:read.

Parameters

NameInTypeDescription
monthquerystringYYYY-MM. Defaults to the current month.
pattern ^\d{4}-(0[1-9]|1[0-2])$

Responses

StatusCodeDescription
200-Stats for the requested month (zeros for a month with no recorded activity).
400
VALIDATION_ERROR
The request failed validation - a malformed body, an out-of-range or malformed query parameter, or a missing/malformed Idempotency-Key header on an endpoint that requires one. `details` holds the underlying validation issues when the failure is body/query shape.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

Example request

shell

curl https://app.wallento.cz/api/v1/reports/monthly \
  -H "Authorization: Bearer wlt_sk_..."

Broadcasts

Push a message to every customer with a synced wallet pass.

POST/v1/broadcastsIdempotency-Key accepted

Send a broadcast push

Pushes a message (with an automatic date prefix) to every customer with a synced wallet pass. Scope: broadcast:send. Hard limit: 2 per hour per tenant, enforced in addition to the general per-key write limit - either budget being exhausted returns 429 RATE_LIMITED. Accepts Idempotency-Key but does not require it; when provided, a rate-limited attempt does not consume it, so the same key can be retried once the tenant's budget has headroom again.

Parameters

NameInTypeDescription
Idempotency-KeyheaderstringOptional on this endpoint. When present, a retried request with the same key and body replays the original response instead of repeating the side effect - see Idempotency in the API description.
1-255 chars

Request body

FieldTypeDescription
message*stringFree text. A date prefix is added automatically.
1-180 chars

Example

{
  "message": "New seasonal menu just dropped! Come try it this week."
}

Responses

StatusCodeDescription
200-Broadcast fanned out. `failed` counts individual delivery-queue failures - the request as a whole still succeeds.
400
VALIDATION_ERROR
The request failed validation - a malformed body, an out-of-range or malformed query parameter, or a missing/malformed Idempotency-Key header on an endpoint that requires one. `details` holds the underlying validation issues when the failure is body/query shape.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
409
IDEMPOTENCY_CONFLICT
This Idempotency-Key was already used with a different request body, or the original request is still in flight.
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

409

{
  "error": {
    "code": "IDEMPOTENCY_CONFLICT",
    "message": "Idempotency-Key was already used with a different request body, or the original request is still in flight"
  }
}

Example request

shell

curl -X POST https://app.wallento.cz/api/v1/broadcasts \
  -H "Authorization: Bearer wlt_sk_..." \
  -H "Idempotency-Key: a-unique-request-id" \
  -H "Content-Type: application/json" \
  -d '{"message":"New seasonal menu just dropped! Come try it this week."}'

Webhooks

Register endpoints to receive domain events.

GET/v1/webhooks

List webhook endpoints

Scope: webhooks:manage. secret is always redacted here (whsec_... plus the last 4 characters) - the full value is only ever returned once, by the create call below.

Responses

StatusCodeDescription
200-All webhook endpoints for the tenant.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

Example request

shell

curl https://app.wallento.cz/api/v1/webhooks \
  -H "Authorization: Bearer wlt_sk_..."
POST/v1/webhooksIdempotency-Key accepted

Register a webhook endpoint

Scope: webhooks:manage. Accepts Idempotency-Key but does not require it - a retried registration must not silently mint a second endpoint, so pass one if your client may retry. url must use https in production and must not resolve to a loopback, link-local, or private-range host. ping is not a subscribable event; use POST /v1/webhooks/{id}/test to send one. A tenant may register at most 5 endpoints.

Parameters

NameInTypeDescription
Idempotency-KeyheaderstringOptional on this endpoint. When present, a retried request with the same key and body replays the original response instead of repeating the side effect - see Idempotency in the API description.
1-255 chars

Request body

FieldTypeDescription
url*stringMust use https in production; must not resolve to a loopback, link-local, or private-range host.
up to 500 chars
events*array of SubscribableWebhookEventType
1-unlimited items

Example

{
  "url": "https://example.com/webhooks/wallento",
  "events": [
    "transaction.created",
    "customer.created"
  ]
}

Responses

StatusCodeDescription
201-Webhook registered. This is the ONLY response that ever includes the full `secret` - store it now, it cannot be retrieved again.
400
VALIDATION_ERROR
The request failed validation - a malformed body, an out-of-range or malformed query parameter, or a missing/malformed Idempotency-Key header on an endpoint that requires one. `details` holds the underlying validation issues when the failure is body/query shape.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
409
WEBHOOK_LIMITIDEMPOTENCY_CONFLICT
Either the tenant is already at the 5-endpoint limit, or this Idempotency-Key was already used with a different request body (or is still in flight).
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

201 - Full secret, shown once

{
  "webhook": {
    "id": "wh_3n7k2q9p1x8m",
    "url": "https://example.com/webhooks/wallento",
    "events": [
      "transaction.created",
      "customer.created"
    ],
    "secret": "whsec_k3n9p2q7x1m8v4t6r5w0y2z3a1b2c3d4",
    "createdAt": "2026-08-09T14:30:00.000Z"
  }
}

409 - Endpoint limit reached

{
  "error": {
    "code": "WEBHOOK_LIMIT",
    "message": "A tenant may register at most 5 webhook endpoints"
  }
}

409 - Idempotency-Key reused with a different body

{
  "error": {
    "code": "IDEMPOTENCY_CONFLICT",
    "message": "Idempotency-Key was already used with a different request body, or the original request is still in flight"
  }
}

Example request

shell

curl -X POST https://app.wallento.cz/api/v1/webhooks \
  -H "Authorization: Bearer wlt_sk_..." \
  -H "Idempotency-Key: a-unique-request-id" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/webhooks/wallento","events":["transaction.created","customer.created"]}'
DELETE/v1/webhooks/{whid}

Delete a webhook endpoint

Scope: webhooks:manage. Irreversible.

Parameters

NameInTypeDescription
whid*pathstringWebhook endpoint id.

Responses

StatusCodeDescription
200-Webhook deleted.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
404
NOT_FOUND
No resource with this id exists for the calling tenant. Also returned - deliberately indistinguishable from a genuinely missing id - when the id belongs to a DIFFERENT tenant: the API never confirms another tenant's data exists with a 403.
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

Example request

shell

curl -X DELETE https://app.wallento.cz/api/v1/webhooks/wh_3n7k2q9p1x8m \
  -H "Authorization: Bearer wlt_sk_..."
POST/v1/webhooks/{whid}/testIdempotency-Key accepted

Send a test ping event

Enqueues a single ping event through the exact same signed-delivery path as a real domain event, so you can verify an endpoint and its signature check before relying on it. Scope: webhooks:manage. Accepts Idempotency-Key but does not require it - without one, a retried request after a lost response enqueues a second ping.

Parameters

NameInTypeDescription
whid*pathstringWebhook endpoint id.
Idempotency-KeyheaderstringOptional on this endpoint. When present, a retried request with the same key and body replays the original response instead of repeating the side effect - see Idempotency in the API description.
1-255 chars

Responses

StatusCodeDescription
200-The ping was accepted for delivery. `enqueued: false` means the delivery queue itself rejected the job (rare, an infrastructure issue on our side, not a signal about your endpoint) - the event was not attempted.
400
VALIDATION_ERROR
The request failed validation - a malformed body, an out-of-range or malformed query parameter, or a missing/malformed Idempotency-Key header on an endpoint that requires one. `details` holds the underlying validation issues when the failure is body/query shape.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
404
NOT_FOUND
No resource with this id exists for the calling tenant. Also returned - deliberately indistinguishable from a genuinely missing id - when the id belongs to a DIFFERENT tenant: the API never confirms another tenant's data exists with a 403.
409
IDEMPOTENCY_CONFLICT
This Idempotency-Key was already used with a different request body, or the original request is still in flight.
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

409

{
  "error": {
    "code": "IDEMPOTENCY_CONFLICT",
    "message": "Idempotency-Key was already used with a different request body, or the original request is still in flight"
  }
}

Example request

shell

curl -X POST https://app.wallento.cz/api/v1/webhooks/wh_3n7k2q9p1x8m/test \
  -H "Authorization: Bearer wlt_sk_..." \
  -H "Idempotency-Key: a-unique-request-id"

API keys

Provisioning of register tokens - the location-bound `pos` keys a till authenticates with. Only ever register tokens: this surface can neither mint nor revoke a key with admin-shaped scopes.

GET/v1/apikeys

List register tokens

Scope: apikeys:manage. Lists only location-bound pos keys, revoked ones included (audit trail) - the tenant's admin-shaped keys are never visible here. Secrets are never returned; match a key you already hold by its last6.

Parameters

NameInTypeDescription
locationIdquerystringReturn only the tokens bound to this location.

Responses

StatusCodeDescription
200-Register tokens, newest first.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

Example request

shell

curl https://app.wallento.cz/api/v1/apikeys \
  -H "Authorization: Bearer wlt_sk_..."
POST/v1/apikeys

Mint a register token for a location

Scope: apikeys:manage. Mints exactly one pos key bound to locationId. You cannot choose scopes and you cannot mint a tenant-wide key here - the shape is fixed so a leaked provisioning key can never escalate itself into an admin-shaped one.

Idempotency-Key is REFUSED on this endpoint (400), unlike every other v1 POST: replay works by storing the original response body, and this one carries the key plaintext - the single value Wallento never persists. Repeat safety comes instead from the one-live-token-per-location rule: a second POST for the same location answers 409 KEY_EXISTS until you pass "rotate": true.

Request body

Scopes are deliberately NOT part of this request - every key minted here is exactly a `pos` key bound to `locationId`.
FieldTypeDescription
name*stringHuman label shown in the tenant admin's key list.
up to 60 chars
locationId*stringThe location this till stands in. Must belong to your tenant.
up to 64 chars
rotatebooleanfalse: a location that already has a live register token answers 409 `KEY_EXISTS`. true: revoke every live token bound to that location first, then mint a fresh one - the old tokens stop working immediately.
default false

Example

{
  "name": "Tillo - Praha 1",
  "locationId": "Xsf5V5hNMhiy"
}

Responses

StatusCodeDescription
201-Token minted. This is the ONLY response that ever contains the secret - store it now, it cannot be retrieved again.
400
VALIDATION_ERROR
The request failed validation - a malformed body, an out-of-range or malformed query parameter, or a missing/malformed Idempotency-Key header on an endpoint that requires one. `details` holds the underlying validation issues when the failure is body/query shape.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
404
NOT_FOUND
No resource with this id exists for the calling tenant. Also returned - deliberately indistinguishable from a genuinely missing id - when the id belongs to a DIFFERENT tenant: the API never confirms another tenant's data exists with a 403.
409
KEY_EXISTS
`KEY_EXISTS` - the location already has a live token (repeat with `"rotate": true` to replace it); or `LIMIT_REACHED` - the tenant is at its live-key ceiling.
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

409

{
  "error": {
    "code": "KEY_EXISTS",
    "message": "This location already has a live register token. Send \"rotate\": true to replace it."
  }
}

Example request

shell

curl -X POST https://app.wallento.cz/api/v1/apikeys \
  -H "Authorization: Bearer wlt_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"name":"Tillo - Praha 1","locationId":"Xsf5V5hNMhiy"}'
DELETE/v1/apikeys/{akid}

Revoke a register token

Scope: apikeys:manage. Takes effect immediately and is irreversible; the record stays listed for audit with revokedAt set. Only location-bound tokens can be revoked here - an id belonging to an admin-shaped key answers 404, so a leaked provisioning key cannot switch off your other integrations. Repeating the call on an already-revoked token is a safe no-op.

Parameters

NameInTypeDescription
akid*pathstringRegister token id (`ak_...`), as returned when the key was minted.

Responses

StatusCodeDescription
200-Token revoked.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
404
NOT_FOUND
No resource with this id exists for the calling tenant. Also returned - deliberately indistinguishable from a genuinely missing id - when the id belongs to a DIFFERENT tenant: the API never confirms another tenant's data exists with a 403.
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

Example request

shell

curl -X DELETE https://app.wallento.cz/api/v1/apikeys/akid \
  -H "Authorization: Bearer wlt_sk_..."

Users

Provisioning and role management for the /t-facing admin accounts (owner, franchisee, manager) and staff profiles a partner backend manages, plus one-time login links. Scope: `users:manage` - never available to a location-bound `pos` key.

GET/v1/users

List users

Scope: users:manage. Cursor-paginated (see Pagination in the API description). externalRef and email are mutually exclusive filters, each returning a 0/1-item page with no nextCursor - same shape as the customer lookups (GET /v1/customers?phone=/?email=). email matches against the GLOBAL email index (an email belongs to exactly one User across every tenant); a match belonging to a different tenant is indistinguishable from no match.

Parameters

NameInTypeDescription
limitqueryinteger
1-200 - default 50
cursorquerystringOpaque pagination cursor taken from a previous response's `nextCursor`. A malformed value returns 400 VALIDATION_ERROR.
externalRefquerystringReturn only the user with this externalRef, if any.
emailquerystringReturn only the user with this email, if any (and it belongs to your tenant).

Responses

StatusCodeDescription
200-Users, newest first.
400
VALIDATION_ERROR
The request failed validation - a malformed body, an out-of-range or malformed query parameter, or a missing/malformed Idempotency-Key header on an endpoint that requires one. `details` holds the underlying validation issues when the failure is body/query shape.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

Example request

shell

curl https://app.wallento.cz/api/v1/users \
  -H "Authorization: Bearer wlt_sk_..."
POST/v1/usersIdempotency-Key required

Provision a user (create, or upsert by externalRef)

Scope: users:manage. Requires Idempotency-Key (see Idempotency in the API description). When externalRef is given and already matches an existing user of your tenant, this behaves like PATCH /v1/users/{uid} instead of creating a second record (200, not 201) - the natural key a re-run provisioning sync upserts on, independent of the Idempotency-Key mechanism (which only protects a single retried request, not a daily re-sync that mints a fresh key every time). email must not already belong to a user of a DIFFERENT tenant (409 EMAIL_TAKEN); every locationId must belong to your tenant (422 VALIDATION_ERROR).

Parameters

NameInTypeDescription
Idempotency-Key*headerstringRequired on this endpoint. A caller-chosen string, unique per logical request, used to make retries safe - see Idempotency in the API description.
1-255 chars

Request body

FieldTypeDescription
email*string
up to 200 chars
name*string
1-100 chars
role*UserRole
locationIdsarray of stringOmit or send [] for every location of the tenant.
0-500 items
externalRefstringYour own id for this user. When it already matches an existing user of your tenant, this call upserts that user instead of creating a second one.
1-128 chars

Example

{
  "email": "jana@example.com",
  "name": "Jana Novakova",
  "role": "manager",
  "locationIds": [
    "Xsf5V5hNMhiy"
  ],
  "externalRef": "employee-482"
}

Responses

StatusCodeDescription
200-Upserted an existing user matched by externalRef.
201-A new user was created.
400
VALIDATION_ERROR
The request failed validation - a malformed body, an out-of-range or malformed query parameter, or a missing/malformed Idempotency-Key header on an endpoint that requires one. `details` holds the underlying validation issues when the failure is body/query shape.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
409
EMAIL_TAKENLAST_OWNERIDEMPOTENCY_CONFLICT
`EMAIL_TAKEN` - the email belongs to a user of a different tenant; `LAST_OWNER` - an externalRef upsert would re-role or otherwise remove the tenant's last active owner; `REF_TAKEN` - a rare concurrent-create race on a brand-new externalRef; or this Idempotency-Key was already used with a different body (or is still in flight).
422
VALIDATION_ERROR
One or more `locationIds` do not belong to your tenant.
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

409 - Email belongs to another tenant's user

{
  "error": {
    "code": "EMAIL_TAKEN",
    "message": "A user with this email already exists"
  }
}

409 - Would remove the tenant's last active owner

{
  "error": {
    "code": "LAST_OWNER",
    "message": "This is the tenant's last active owner and cannot lose owner access through this call"
  }
}

409 - Idempotency-Key reused with a different body

{
  "error": {
    "code": "IDEMPOTENCY_CONFLICT",
    "message": "Idempotency-Key was already used with a different request body, or the original request is still in flight"
  }
}

422

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "One or more locationIds do not belong to this tenant"
  }
}

Example request

shell

curl -X POST https://app.wallento.cz/api/v1/users \
  -H "Authorization: Bearer wlt_sk_..." \
  -H "Idempotency-Key: a-unique-request-id" \
  -H "Content-Type: application/json" \
  -d '{"email":"jana@example.com","name":"Jana Novakova","role":"manager","locationIds":["Xsf5V5hNMhiy"],"externalRef":"employee-482"}'
GET/v1/users/{uid}

Get a user

Scope: users:manage.

Parameters

NameInTypeDescription
uid*pathstringUser id.

Responses

StatusCodeDescription
200-The user.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
404
NOT_FOUND
No resource with this id exists for the calling tenant. Also returned - deliberately indistinguishable from a genuinely missing id - when the id belongs to a DIFFERENT tenant: the API never confirms another tenant's data exists with a 403.
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

Example request

shell

curl https://app.wallento.cz/api/v1/users/uid \
  -H "Authorization: Bearer wlt_sk_..."
PATCH/v1/users/{uid}

Update a user

Scope: users:manage. At least one of email, name, role, locationIds, status or externalRef must be provided. externalRef is normally set once at creation (POST /v1/users) and used as the provisioning natural key - this endpoint only accepts it to ADOPT a user that doesn't have one yet (created by hand, or via POST without externalRef); once set, sending a different value is rejected (409 REF_IMMUTABLE). Setting status: "disabled" here has the same effect as DELETE /v1/users/{uid} (soft-disable; a live session for this user is cut off on its next revalidation). email must not already belong to a user of a different tenant (409 EMAIL_TAKEN); a patch that would leave the tenant with no active owner is rejected (409 LAST_OWNER); every locationId must belong to your tenant (422 VALIDATION_ERROR).

Parameters

NameInTypeDescription
uid*pathstringUser id.

Request body

At least one field must be provided. externalRef is only writable through this endpoint while the user does not have one yet - it lets you adopt a user that was created without one (by hand in /t/settings, or via POST without externalRef) so it can be found by GET /v1/users?externalRef= from then on. Once a user has a ref, sending the same value back is a harmless no-op but sending a different one is rejected (409 REF_IMMUTABLE) - reassign by deleting and re-provisioning instead.
FieldTypeDescription
emailstring
up to 200 chars
namestring
1-100 chars
roleUserRole
locationIdsarray of string
0-500 items
status"active" | "disabled"Setting disabled has the same effect as DELETE /v1/users/{uid}.
externalRefstringAttaches this ref to a user that doesn't have one yet (adoption). Rejected with 409 REF_IMMUTABLE if the user already has a different ref, and with 409 REF_TAKEN if another user of your tenant already has this one.
1-128 chars

Example

{
  "locationIds": [
    "Xsf5V5hNMhiy"
  ]
}

Responses

StatusCodeDescription
200-User updated.
400
VALIDATION_ERROR
The request failed validation - a malformed body, an out-of-range or malformed query parameter, or a missing/malformed Idempotency-Key header on an endpoint that requires one. `details` holds the underlying validation issues when the failure is body/query shape.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
404
NOT_FOUND
No resource with this id exists for the calling tenant. Also returned - deliberately indistinguishable from a genuinely missing id - when the id belongs to a DIFFERENT tenant: the API never confirms another tenant's data exists with a 403.
409
EMAIL_TAKENLAST_OWNERREF_IMMUTABLEREF_TAKEN
`EMAIL_TAKEN` - the email belongs to a user of a different tenant; `LAST_OWNER` - this patch would leave the tenant with no active owner; `REF_IMMUTABLE` - the user already has a different externalRef; or `REF_TAKEN` - the given externalRef already belongs to another user of your tenant.
422
VALIDATION_ERROR
One or more `locationIds` do not belong to your tenant.
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

409 - Email belongs to another tenant's user

{
  "error": {
    "code": "EMAIL_TAKEN",
    "message": "A user with this email already exists"
  }
}

409 - Would leave the tenant with no active owner

{
  "error": {
    "code": "LAST_OWNER",
    "message": "This is the tenant's last active owner and cannot lose owner access through this call"
  }
}

409 - User already has a different externalRef

{
  "error": {
    "code": "REF_IMMUTABLE",
    "message": "externalRef is already set for this user and cannot be reassigned through this call"
  }
}

409 - externalRef already used by another user of this tenant

{
  "error": {
    "code": "REF_TAKEN",
    "message": "externalRef already used by another user of this tenant"
  }
}

422

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "One or more locationIds do not belong to this tenant"
  }
}

Example request

shell

curl -X PATCH https://app.wallento.cz/api/v1/users/uid \
  -H "Authorization: Bearer wlt_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"locationIds":["Xsf5V5hNMhiy"]}'
DELETE/v1/users/{uid}

Disable a user

Scope: users:manage. Soft delete: sets status to disabled - never removes the record, and it stays visible in GET /v1/users/GET /v1/users/{uid}. A live session for this user is cut off on its next revalidation. The tenant's last active owner cannot be disabled this way (409 LAST_OWNER), so a partner integration can never lock a tenant out of its own admin.

Parameters

NameInTypeDescription
uid*pathstringUser id.

Responses

StatusCodeDescription
200-User disabled.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
404
NOT_FOUND
No resource with this id exists for the calling tenant. Also returned - deliberately indistinguishable from a genuinely missing id - when the id belongs to a DIFFERENT tenant: the API never confirms another tenant's data exists with a 403.
409
LAST_OWNER
`LAST_OWNER` - this is the tenant's last active owner.
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

409

{
  "error": {
    "code": "LAST_OWNER",
    "message": "This is the tenant's last active owner and cannot be deactivated through this call"
  }
}

Example request

shell

curl -X DELETE https://app.wallento.cz/api/v1/users/uid \
  -H "Authorization: Bearer wlt_sk_..."
POST/v1/users/{uid}/login-link

Mint a one-time login link

Scope: users:manage. Mints a single-use URL that signs the target user directly into /t, no password or email round-trip required - meant for a partner backend's own "log in as this employee" action. The token expires after 60 seconds and is consumed on first use (a second visit to the same URL fails and redirects to the sign-in page). Rejected for a staff user (422 STAFF_NO_LOGIN - staff never signs in to /t) and for a disabled user (422 USER_DISABLED). Tenant-scoped budget: 60 mints/minute, in addition to the general per-key write limit - see Rate limits in the API description. Does not accept Idempotency-Key - there is no request body, and every call is meant to mint a fresh token, never replay a previous one. The URL itself is a bearer credential for the 60 seconds it lives: never log it, and hand it only to the already-authenticated operator it was minted for.

Parameters

NameInTypeDescription
uid*pathstringUser id.

Responses

StatusCodeDescription
200-Login link minted.
401
UNAUTHORIZED
The bearer token is missing, malformed, or does not match a live (non-revoked) API key. Carries no X-RateLimit-* headers - no key has been resolved yet.
403
FORBIDDEN_SCOPETENANT_ARCHIVED
The key is valid but either lacks every scope this operation requires, or belongs to a tenant that has been archived. Carries no X-RateLimit-* headers - both checks happen before a per-key limiter is consulted.
404
NOT_FOUND
No resource with this id exists for the calling tenant. Also returned - deliberately indistinguishable from a genuinely missing id - when the id belongs to a DIFFERENT tenant: the API never confirms another tenant's data exists with a 403.
422
STAFF_NO_LOGINUSER_DISABLED
`STAFF_NO_LOGIN` - staff users never sign in to `/t`; or `USER_DISABLED` - this user is disabled.
429
RATE_LIMITED
Too many requests. Back off and retry after `Retry-After` seconds. See Rate limits in the API description for which budget applies to this endpoint.

422 - Staff never signs in to /t

{
  "error": {
    "code": "STAFF_NO_LOGIN",
    "message": "Staff users cannot receive a login link - staff never signs in to /t"
  }
}

422 - User is disabled

{
  "error": {
    "code": "USER_DISABLED",
    "message": "This user is disabled"
  }
}

Example request

shell

curl -X POST https://app.wallento.cz/api/v1/users/uid/login-link \
  -H "Authorization: Bearer wlt_sk_..."

Schemas

Shared object shapes referenced from the tables above.

Customer

FieldTypeDescription
id*string
name*string
phone*stringE.164 format, for example +420777123456.
email*stringEmpty string when the customer has no email on file.
marketingOptIn*boolean
cardToken*stringOpaque token identifying the customer's wallet card.
cardUrl*stringPublic URL of the customer's wallet card (Add to Apple/Google Wallet).
passState*"ok" | "pending" | "error"Wallet pass sync state: pending briefly after creation or a state change, error if the wallet provider rejected the last sync.
visits*integerCount of stamp/points_earn transactions - a redeem or an admin adjust does not count as a new visit. 0 for a customer with none yet.
at least 0
firstTxAt*string or nullTimestamp of this customer's first transaction of any kind, null if none yet.
lastTxAt*string or nullTimestamp of this customer's most recent transaction of any kind, null if none yet.
originLocationId*string or nullThe location whose join QR issued this card, captured once at signup - provenance, not ownership. Null when the card was issued through the tenant-wide print page's QR, or predates this field.
signupRef*string or nullReceipt attribution code the till printed on the receipt whose QR led to this signup, shape "KODPRODEJNY-CISLOUCTENKY" (e.g. "U008-1043"), captured once at signup and never rewritten afterwards. Null when the signup came from a QR carrying no ref (tenant-wide print page, a location's own QR without a receipt behind it), or predates this field.
loyalty*Loyalty
createdAt*string
registeredAt*stringSame instant as createdAt - a stable alias partner integrations can rely on independent of internal naming.

Loyalty

FieldTypeDescription
programType*"stamps" | "points"
currentStamps*integerStamps toward the customer's next reward, on a stamps program.
at least 0
pendingRewards*integerEarned-but-not-yet-redeemed stamp-card rewards.
at least 0
pointsBalance*integerCurrent spendable points balance, on a points program.
at least 0
lifetimeStamps*integer
at least 0
lifetimePoints*integer
at least 0
level*integerIndex into the program's `levels` array. -1 when the customer has not reached any level yet - whether because the tenant has no levels configured, or because lifetime progress is still below the first threshold.
levelName*string or nullName of the customer's current level, or null when `level` is -1 (no level reached yet).

Transaction

FieldTypeDescription
id*string
customerId*string
type*"stamp" | "points_earn" | "redeem" | "adjust"The stored transaction kind - distinct from the action names POST /v1/transactions accepts (stamp, spend, redeem_stamp, redeem_points): spend produces points_earn; both redeem_stamp and redeem_points produce redeem; adjust is admin-only and never written by this API.
amount*integerSigned magnitude of the change: +1 for a stamp, +points for points_earn, -1 or -points for redeem, positive or negative for an admin adjust.
spentCzk*integer or nullAmount spent in CZK, present only on a points_earn transaction created from a spend action.
rewardId*string or nullThe redeemed reward's id, present only on a points-reward redemption.
locationId*string or null
actor*"staff" | "admin" | "system" | "api"Who made this write: the staff scanner app, the tenant admin, an internal system process, or this partner API.
externalRef*string or nullPartner-supplied receipt/order id, echoed back verbatim when this transaction was created via this API with externalRef set.
note*string or null
createdAt*string

Program

FieldTypeDescription
programType*"stamps" | "points"
stamps*StampsConfig or null
points*PointsConfig or null
levels*array of LevelDef

StampsConfig

FieldTypeDescription
stampsRequired*integer
3-20
rewardText*string

PointsConfig

FieldTypeDescription
pointsPerCzk*number
0.01-10
rewards*array of Reward

Reward

FieldTypeDescription
id*string
name*string
costPoints*integer
at least 1

RewardInput

FieldTypeDescription
idstringOmit to mint a new reward; include an existing reward's id to update it in place.
up to 40 chars
name*string
1-60 chars
costPoints*integer
1-1000000

LevelDef

FieldTypeDescription
name*string
1-40 chars
threshold*integerLifetime stamps or points required to reach this level.
0-10000000
colorstring
pattern ^#[0-9a-fA-F]{6}$
benefitTextstring
up to 200 chars

Location

FieldTypeDescription
id*string
name*string
code*stringStaff login code used together with the location's PIN at /scan/{code}. Sensitive - anyone holding both can sign in as staff for this location.
createdAt*string
address*string or nullPostal address of this location, geocoded server-side to `lat`/`lng` for Apple Wallet geofence triggers (Etapa 1). The API normalizes and returns the formatted address Google Geocoding provided. null for a location created before this field existed.
lat*number or nullLatitude coordinate derived from `address` by Google Geocoding API. null if address was not geocodable, `GOOGLE_GEOCODING_API_KEY` is not configured, or the location predates this field.
lng*number or nullLongitude coordinate derived from `address`. null if address was not geocodable, `GOOGLE_GEOCODING_API_KEY` is not configured, or the location predates this field.
geoConfidence*"exact" | "approximate" | null'exact' when Google's geocoder matched a rooftop-precision address (geometry.location_type === ROOFTOP), 'approximate' for street/city centroid matches, null alongside lat/lng when neither is set. Guides how carefully an admin should review the coordinate.

Webhook

FieldTypeDescription
id*string
url*string
events*array of SubscribableWebhookEventType
secret*stringwhsec_ prefixed signing secret. The full value is present only in the response to POST /v1/webhooks, shown exactly once. Every other response redacts it to whsec_... followed by the last 4 characters.
createdAt*string

WebhookEvent

FieldTypeDescription
id*stringevt_ prefixed event id, unique per delivery attempt's underlying event.
type*WebhookEventType
createdAt*string
data*objectEvent-specific payload. customer.created/updated: { customer: Customer }. customer.deleted: { customerId }. transaction.created: { transaction: Transaction, customer: Customer, rewardEarned: boolean, levelUp: LevelDef | null }. broadcast.sent: { message: string, queued: integer, failed: integer }. ping: {} (empty).

SubscribableWebhookEventType

Event types a webhook endpoint can subscribe to. ping is not in this list - it is only ever sent by POST /v1/webhooks/{id}/test.

"customer.created""customer.updated""customer.deleted""transaction.created""broadcast.sent"

WebhookEventType

Every event type a delivery's type field can carry, including the test-only ping.

"customer.created""customer.updated""customer.deleted""transaction.created""broadcast.sent""ping"

MonthlyStats

FieldTypeDescription
visits*integer
at least 0
newCustomers*integer
at least 0
redemptions*integer
at least 0
pointsIssued*integer
at least 0

Error

FieldTypeDescription
error*object
Wallento (c) 2026
PrivacyBack to app