API Endpoints
This section describes the available API endpoints for creating and managing sales through the zazpay API. Authentication is required for all endpoints as described in the auth.md documentation.
Create a Sale
POST /commerce/generate-sale
Creates a new sale transaction.
Input Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
total | number | Yes | Total transaction amount |
clientUsername | string | One of | Username/identifier of the client. Provide either clientUsername or clientId |
clientId | string | One of | Client ID. Provide either clientId or clientUsername |
salesmanIdentifier | string | One of | Salesman reference. Provide to look up an existing salesman by reference |
salesmanId | string | One of | Salesman ID. Provide to look up an existing salesman by ID |
salesman | object | One of | Full salesman payload to auto-create if not found. See Salesman object. Mutually exclusive with salesmanIdentifier/salesmanId |
storeId | string | Yes | Store ID (external or internal) |
comments | string | No | Optional comments/description |
folioExternal | string | Yes | External reference for idempotency and, in sandbox, lifecycle and return-simulation flags |
Notes:
- Exactly one of
clientUsernameorclientIdis required. - Exactly one salesman source is required: either
salesmanIdentifier/salesmanId(lookup existing) or a fullsalesmanobject (auto-create if not found). Sending both or neither returnsSALESMAN-PAYLOAD-INVALID. - Salesmen are scoped by
companyId, not bystoreId— the same salesman can generate sales across multiple stores of the same commerce. - Auto-create is idempotent by
reference + companyId: repeat calls with the samesalesman.referencereuse the existing record, they do not create duplicates. - If the sale fails after the salesman is auto-created (e.g., insufficient credit line), the salesman remains created. Retrying with
salesmanIdentifierequal tosalesman.referencewill reuse it.
Salesman object
| Field | Type | Required | Description |
|---|---|---|---|
reference | string | Yes | External salesman identifier (deduplication key) |
name | string | Yes | First name |
paternalSurname | string | Yes | Paternal surname |
maternalSurname | string | No | Maternal surname |
phoneNumber | string | One of | 10-digit phone number. Whitelist identity — provide phoneNumber or curp. |
curp | string | One of | CURP. Whitelist identity — provide phoneNumber or curp. |
At least one of phoneNumber or curp is required — it is the salesman's whitelist identity. reference is the merchant's own external ID (UUID, employee number, etc.). Bank data (CLABE, debit card, bank) is not accepted here; it is registered from the cashier by a manager, or by the salesman in the vendedores app.
Request examples
Mode 1 — Lookup existing salesman (original behavior, backwards compatible):
{
"total": 1500,
"clientId": "a7b4c8d9-1234-4567-89ab-cdef01234567",
"salesmanIdentifier": "SM-001",
"storeId": "5b946887-04ec-4b92-8076-380b028cba1a",
"folioExternal": "EXT-001",
"comments": "Electronics purchase"
}
Mode 2 — Auto-create salesman (new). If (reference, companyId) doesn't exist, the salesman is created before the sale; if it exists, it is reused:
{
"total": 1500,
"clientId": "a7b4c8d9-1234-4567-89ab-cdef01234567",
"salesman": {
"reference": "SM-001",
"name": "Juan",
"paternalSurname": "Pérez",
"maternalSurname": "López",
"phoneNumber": "5512345678"
},
"storeId": "5b946887-04ec-4b92-8076-380b028cba1a",
"folioExternal": "EXT-001"
}
Response (data)
{
"apiSaleId": "string",
"id": "string",
"folio": 12345,
"folioExternal": "EXT-001",
"status": "IN_PROGRESS",
"store": { "name": "Store name" }
}
Exceptions
| Error Code | Description |
|---|---|
COMPANY-NOT-FOUND | Company not found |
STORE-NOT-FOUND | Store not found |
STORE-NOT-BELONG-TO-COMPANY | Store does not belong to your company |
SALESMAN-NOT-FOUND | Salesman not found |
SALESMAN-PAYLOAD-INVALID | Both or neither salesman source provided (XOR rule violated) |
SALESMAN-IDENTITY-MISSING | Salesman object provided without phoneNumber or curp |
SALESMAN-UPSERT-FAILED | Gateway failed to create or retrieve the salesman |
CLIENT-NOT-FOUND | Client not found |
SALE-CREATION-03 | Minimum amount not reached |
SALE-CREATION-04 | Client credit line insufficient |
TRANSACTION-NOT-FOUND | Transaction not found |
Note: Some gateway-originated errors are passed through with their original statusCode and errorCode (e.g., C-PF-TRANSACTION-1001).
Upsert a Salesman
POST /commerce/salesman
Creates a salesman if no record exists for (reference, companyId), or returns the existing one. Idempotent. Useful when you prefer to register vendors ahead of time rather than embedding the salesman object inside generate-sale.
Input Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
companyId | string | Yes | Gateway company UUID this salesman belongs to |
reference | string | Yes | External salesman identifier (deduplication key) |
name | string | Yes | First name |
paternalSurname | string | Yes | Paternal surname |
maternalSurname | string | No | Maternal surname |
phoneNumber | string | One of | 10-digit phone number. Whitelist identity — provide phoneNumber or curp. |
curp | string | One of | CURP. Whitelist identity — provide phoneNumber or curp. |
At least one of phoneNumber or curp is required — it is the salesman's whitelist identity. reference is the merchant's own external ID (UUID, employee number, etc.). Bank data (CLABE, debit card, bank) is not accepted here; it is registered from the cashier by a manager, or by the salesman in the vendedores app.
Request example
{
"companyId": "d6586426-abeb-40b8-8958-455a020a4a25",
"reference": "SM-001",
"name": "Juan",
"paternalSurname": "Pérez",
"maternalSurname": "López",
"phoneNumber": "5512345678"
}
Response (data)
{
"id": "9ce723f5-79d1-415f-8782-1c12be7c6b09",
"reference": "SM-001",
"name": "Juan",
"paternalSurname": "Pérez",
"maternalSurname": "López",
"phoneNumber": "5512345678",
"curp": null,
"companyId": "d6586426-abeb-40b8-8958-455a020a4a25",
"status": "ACTIVE"
}
Exceptions
| Error Code | Description |
|---|---|
INPUT-0 | Missing or malformed required fields (see message for details) |
SM-322 | Neither phoneNumber nor curp provided (at least one required) |
SM-311 | This phone number is already registered as a salesman for this company |
SM-323 | This CURP is already registered as a salesman for this company |
Retrieve Sale Status
POST /commerce/transaction-status
Retrieves the current status of a sale transaction by folio.
Input Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
folio | number | No | Transaction folio to look up |
folioExternal | string | No | Transaction folio external to look up |
Response (data)
{
"folio": 12345,
"folioExternal": "string",
"createdAt": "2024-01-15T10:30:00.000Z",
"updatedAt": "2024-01-15T10:35:00.000Z",
"salesmanId": "string",
"storeId": "string",
"status": "APPROVED",
"total": 1500,
"clientId": "string",
"comments": "string"
}
Exceptions
| Error Code | Description |
|---|---|
TRANSACTION-NOT-FOUND | Transaction not found |
Cancel a sale
POST /commerce/cancel-transaction
Cancels a sale that hasn't been accepted yet.
Input Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
folio | number | No | The transaction folio to cancel |
folioExternal | string | No | The transaction external folio to cancel |
Response (data)
{
"folio": 12345,
"status": "CANCELED",
"cancelledAt": "2024-01-15T10:40:00.000Z"
}
Exceptions
| Error Code | Description |
|---|---|
SALE-CANCEL-01 | Sale not found |
SALE-CANCEL-02 | Sale already cancelled |
SALE-CANCEL-03 | Sale cannot be cancelled (already processed) |
Mock the client response (sandbox)
POST /commerce/resolve-transaction
In production, creating a sale is only half the flow: the end client receives it in their Zazpay app and responds — they can approve it, reject it, or never act on it at all. In sandbox there is no real client, so this endpoint lets you mock that response and test every branch of your integration:
| Real client behavior | Mock it with status |
|---|---|
| Client approves the sale in the app | APPROVED |
| Client rejects the sale in the app | REJECTED |
| Client never responds and the sale expires | EXPIRED |
| Sale is called off | CANCELED |
| Force a completed sale into a return | RETURNED |
This is the recommended way to drive a sandbox sale to a specific outcome: create the sale with NO_CANCEL in folioExternal (which disables the automatic transition) so it stays IN_PROGRESS, then mock the client's decision with this endpoint. In production this endpoint always fails with SANDBOX-ONLY-ENDPOINT.
Without NO_CANCEL in folioExternal, a sandbox sale auto-transitions about 5 seconds after creation (to APPROVED, or to REJECTED if folioExternal contains REJECTED). Once the auto-transition claims the sale, resolving to APPROVED/REJECTED/EXPIRED/CANCELED fails with TRANSACTION-NOT-IN-PROGRESS.
Authentication uses the same bearer token as the other commerce endpoints.
Input Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
folio | number | One of | Transaction folio. Provide either folio or folioExternal |
folioExternal | string | One of | External reference. Provide either folioExternal or folio |
status | string | Yes | Target status: APPROVED, REJECTED, EXPIRED, CANCELED, or RETURNED |
APPROVED,REJECTED,EXPIRED, andCANCELEDrequire the sale to beIN_PROGRESS.RETURNEDrequires the sale to beAPPROVEDand behaves as a force-return: it fires the sameTRANSACTION_RETURNEDandTRANSACTION_STATUS_CHANGEwebhook events as/commerce/return-transaction, and as a force-tool it intentionally bypasses theNO_RETURNflag (see Return simulation flags).
Request examples
Client approves the sale:
{
"folio": 12345,
"status": "APPROVED"
}
Client rejects the sale:
{
"folio": 12345,
"status": "REJECTED"
}
Response (data)
The response echoes the mocked decision:
{
"folio": 12345,
"status": "APPROVED",
"resolvedAt": "2026-07-01T10:40:00.000Z"
}
Exceptions
| Error Code | HTTP | Description |
|---|---|---|
TRANSACTION-NOT-FOUND | 404 | No folio/folioExternal provided, or no matching sale for your company |
TRANSACTION-NOT-IN-PROGRESS | 400 | Target is APPROVED/REJECTED/EXPIRED/CANCELED but the sale is not IN_PROGRESS |
TRANSACTION-NOT-RETURNABLE | 400 | Target is RETURNED but the sale is not APPROVED |
SANDBOX-ONLY-ENDPOINT | 403 | Called in production |
A successful resolve emits the TRANSACTION_STATUS_CHANGE webhook event carrying the mocked status (plus TRANSACTION_RETURNED when resolving to RETURNED) — your webhook receives exactly what it would receive in production when the real client acts, so mocking a rejection also exercises your rejection handling end to end. See Webhooks.
Return a sale (devolución)
POST /commerce/return-transaction
Returns an already APPROVED sale. Cancellation of an IN_PROGRESS sale must go through /commerce/cancel-transaction instead. Only full returns are supported — the entire sale total is always reverted.
The refund to the end client is handled asynchronously by Zazpay and is never exposed through this endpoint.
Input Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
folio | number | One of | Transaction folio. Provide either folio or folioExternal |
folioExternal | string | One of | External reference. Provide either folioExternal or folio |
returnReason | string | No | Free-form reason for the return (max 500 characters) |
Response (data)
The endpoint responds 200 OK (both on the first call and on idempotent replays).
{
"folio": 12345,
"folioExternal": "ORD-789",
"status": "RETURNED",
"returnedAmount": 1500,
"settlementImpact": "NOT_YET_SETTLED",
"returnedAt": "2026-07-01T10:40:00.000Z"
}
settlementImpactisNOT_YET_SETTLEDwhen the commerce has not been paid for the sale yet (the sale is reverted), orDISCOUNT_NEXT_SETTLEMENTwhen the commerce was already paid (the amount is discounted from upcoming settlements).
Exceptions
| Error Code | HTTP | Description |
|---|---|---|
TRANSACTION-NOT-FOUND | 404 | No folio/folioExternal provided or sale not found |
TRANSACTION-IN-PROGRESS-USE-CANCEL | 400 | Sale is still IN_PROGRESS — use cancel-transaction |
TRANSACTION-NOT-RETURNABLE | 400 | Sale is REJECTED/EXPIRED/CANCELED (message has status) |
RETURN-DECLINED | 409 | The return was declined |
GATEWAY-ERROR | 502 | Upstream processing error — retry later |
Transient network failures reaching the upstream may surface as a generic 400 with code GATEWAY-ERROR; treat both as retryable.
Idempotency
Calling return-transaction on a sale that is already RETURNED replays the same 200 response body (no new webhooks, no state changes).
A return also emits the TRANSACTION_RETURNED and TRANSACTION_STATUS_CHANGE webhook events — see Webhooks.
Parameter reference
- folioExternal: External identifier supplied by your system. Used for idempotency and, in sandbox, to control the sale lifecycle (
REJECTED,NO_CANCEL) and simulate return outcomes. - ticket: Optional POS receipt/reference. Free-form string (recommended max 64 chars).
Sandbox behavior and test patterns
In the sandbox environment there is no real end client, so you drive each sale's outcome yourself. Two mechanisms are available:
- Mock the client response (recommended) — create the sale with
NO_CANCELinfolioExternalto disable the automatic transition, then call/commerce/resolve-transactionwith the client's decision (APPROVED,REJECTED,EXPIRED, orCANCELED), or cancel the sale via/commerce/cancel-transaction. Deterministic and race-free. - Automatic transitions (alternative) — leave
folioExternalwithout flags and the sale auto-resolves ~5 seconds after creation. See Automatic transitions viafolioExternal.
Without NO_CANCEL you have roughly 5 seconds before the automatic transition claims the sale; after that, resolving to APPROVED/REJECTED/EXPIRED/CANCELED fails with TRANSACTION-NOT-IN-PROGRESS.
Recommended flow — resolve explicitly
POST /commerce/generate-sale { folioExternal: "ORDER-001-NO_CANCEL", ... } → IN_PROGRESS (stays)
POST /commerce/resolve-transaction { folio: 12345, status: "APPROVED" } → APPROVED
POST /commerce/resolve-transaction { folio: 12346, status: "REJECTED" } → REJECTED
POST /commerce/cancel-transaction { folio: 12347 } → CANCELED
Automatic transitions via folioExternal
Alternatively, keywords in folioExternal control what happens to the sale after creation:
Keyword in folioExternal | Resulting status | Description |
|---|---|---|
| (none) | APPROVED | Sale automatically transitions to APPROVED after ~5 seconds |
REJECTED | REJECTED | Sale automatically transitions to REJECTED after ~5 seconds |
NO_CANCEL | IN_PROGRESS | Automatic transition disabled — the sale stays IN_PROGRESS until you resolve it explicitly or cancel it |
folioExternal: "ORDER-001" → APPROVED (~5s)
folioExternal: "ORDER-001-REJECTED" → REJECTED (~5s)
folioExternal: "ORDER-001-NO_CANCEL" → IN_PROGRESS (stays until resolved/canceled)
Return simulation flags
For /commerce/return-transaction, these additional keywords in folioExternal control the return outcome:
Keyword in folioExternal | Return outcome |
|---|---|
| (none) | 200 with settlementImpact: NOT_YET_SETTLED |
COMMERCE_SETTLED | 200 with settlementImpact: DISCOUNT_NEXT_SETTLEMENT (simulates a commerce already paid) |
WITH_PAYMENTS | 200 identical response; the emitted mock event carries clientRefundPending: true in its metadata (simulates a client with payments to refund) |
NO_RETURN | 409 RETURN-DECLINED |
folioExternal: "ORDER-001-COMMERCE_SETTLED" → RETURNED, DISCOUNT_NEXT_SETTLEMENT
folioExternal: "ORDER-001-WITH_PAYMENTS" → RETURNED, NOT_YET_SETTLED
folioExternal: "ORDER-001-NO_RETURN" → 409 RETURN-DECLINED
A ready-to-run Postman collection covering the whole return matrix (happy path, replay idempotency, every flag, every error code, webhook setup) is available: zazpay-returns.postman_collection.json.
Combine these patterns with the predefined sandbox test clients to cover all integration scenarios.