The system of record for vouchers
Whether a voucher exists, which channel owns it, and who redeemed it for how much: UVS is the authority.
x-uvs-access-key: ak_live_7f3c1a2e
x-uvs-timestamp: 1785072041
x-uvs-nonce: 9d41b0
x-uvs-signature: HMAC-SHA256(...)
idempotency-key: req_lock_001
{
"request_id": "req_lock_001",
"coupon": "164TGDA8HAQ6XS1K",
"merchant": "mch_abc123",
"order_amount": 100,
"currency": "CNY"
}
200 OK
{
"ok": true,
"redemption": "red_lock_998877",
"discount_amount": 15.5
}
Boundaries matter more than capabilities
UVS owns the full lifecycle of the voucher itself. Money, delivery and payment each live in their own systems. Wherever that boundary stays vague, it shows up later as the rows that will not match at reconciliation time.
Inside UVS
- Voucher templates, batch issuing, code security, stock
- Allocation, quota transfer and channel attribution
- Redemption: two-phase, idempotent, concurrency-guarded, reversible
- Risk control: anomaly detection, anti-forgery, anti-enumeration
- Ledger, reconciliation files and outbound event callbacks
Outside UVS
- Settlement and revenue split. UVS never touches funds
- Voucher delivery: SMS, QR codes, claim pages
- Payment collection, run by the merchant's own stack
- Merchant onboarding review and approval
Allocation is inside UVS. Delivery is outside. Handing ten thousand vouchers to channel A, deducting its quota and permanently stamping ownership is allocation, and reconciliation depends on it. How channel A then reaches whom, UVS does not know and does not need to.
Status is not a field you update by hand
A voucher's status is the materialized result of its event ledger. Every transition is a guarded conditional update, so under concurrency it either succeeds exactly once or fails. Two requests never redeem the same voucher.
Three kinds of integrator, one contract
Authentication by Party, authorization by role. Under a single contract, makers, distributors and acceptance sides each get the endpoints their role covers.
Merchant
POS and online checkout start the redemption. Lock first, confirm second, with cancel and reverse for the exception paths.
- POST /v1/redemptions/lock
- POST /v1/redemptions/confirm
- POST /v1/redemptions/cancel
- POST /v1/redemptions/redeem
- POST /v1/redemptions/reverse
- POST /v1/redemptions/void
Maker
Submit a batch, track its progress, release batch quota to a channel.
- POST /v1/batches
- GET /v1/batches/{batchId}
- POST /v1/batches/allocate
Distributor
Pull events incrementally to compensate, fetch daily statements and discrepancy lists.
- GET /v1/events
- GET /v1/reconciliations
- GET /v1/reconciliations/{date}/{kind}
Three guarantees you build on
- Every write is idempotent
-
Write endpoints require an idempotency key. A retry carrying the
same
request_idreplays the first response instead of executing again, and does not error. That is what keeps a checkout from redeeming twice when the network drops. - Callbacks are at-least-once
-
Lifecycle events go out through a transactional outbox, signed
with HMAC, guaranteed to arrive at least once. Subscribers
deduplicate by event ID. Anything missed can be recovered
through
GET /v1/events. - The ledger is append-only
- State changes land in the event ledger first. The status column on the business table is a materialized view of that ledger, and the current state can be rebuilt from it at any time. This is the precondition for reconciliation to balance.
Ready to integrate
Tell us which kind of integrator you are, plus the voucher types and volume you expect. We will send back an AppKey and everything the integration needs.