Skip to main content

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

ParameterTypeRequiredDescription
totalnumberYesTotal transaction amount
clientUsernamestringOne ofUsername/identifier of the client. Provide either clientUsername or clientId
clientIdstringOne ofClient ID. Provide either clientId or clientUsername
salesmanIdentifierstringOne ofSalesman reference. Provide to look up an existing salesman by reference
salesmanIdstringOne ofSalesman ID. Provide to look up an existing salesman by ID
salesmanobjectOne ofFull salesman payload to auto-create if not found. See Salesman object. Mutually exclusive with salesmanIdentifier/salesmanId
storeIdstringYesStore ID (external or internal)
commentsstringNoOptional comments/description
folioExternalstringYesExternal reference for idempotency and, in sandbox, lifecycle and return-simulation flags

Notes:

  • Exactly one of clientUsername or clientId is required.
  • Exactly one salesman source is required: either salesmanIdentifier/salesmanId (lookup existing) or a full salesman object (auto-create if not found). Sending both or neither returns SALESMAN-PAYLOAD-INVALID.
  • Salesmen are scoped by companyId, not by storeId — the same salesman can generate sales across multiple stores of the same commerce.
  • Auto-create is idempotent by reference + companyId: repeat calls with the same salesman.reference reuse 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 salesmanIdentifier equal to salesman.reference will reuse it.

Salesman object

FieldTypeRequiredDescription
referencestringYesExternal salesman identifier (deduplication key)
namestringYesFirst name
paternalSurnamestringYesPaternal surname
maternalSurnamestringNoMaternal surname
phoneNumberstringOne of10-digit phone number. Whitelist identity — provide phoneNumber or curp.
curpstringOne ofCURP. 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 CodeDescription
COMPANY-NOT-FOUNDCompany not found
STORE-NOT-FOUNDStore not found
STORE-NOT-BELONG-TO-COMPANYStore does not belong to your company
SALESMAN-NOT-FOUNDSalesman not found
SALESMAN-PAYLOAD-INVALIDBoth or neither salesman source provided (XOR rule violated)
SALESMAN-IDENTITY-MISSINGSalesman object provided without phoneNumber or curp
SALESMAN-UPSERT-FAILEDGateway failed to create or retrieve the salesman
CLIENT-NOT-FOUNDClient not found
SALE-CREATION-03Minimum amount not reached
SALE-CREATION-04Client credit line insufficient
TRANSACTION-NOT-FOUNDTransaction 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

ParameterTypeRequiredDescription
companyIdstringYesGateway company UUID this salesman belongs to
referencestringYesExternal salesman identifier (deduplication key)
namestringYesFirst name
paternalSurnamestringYesPaternal surname
maternalSurnamestringNoMaternal surname
phoneNumberstringOne of10-digit phone number. Whitelist identity — provide phoneNumber or curp.
curpstringOne ofCURP. 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 CodeDescription
INPUT-0Missing or malformed required fields (see message for details)
SM-322Neither phoneNumber nor curp provided (at least one required)
SM-311This phone number is already registered as a salesman for this company
SM-323This 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

ParameterTypeRequiredDescription
folionumberNoTransaction folio to look up
folioExternalstringNoTransaction 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 CodeDescription
TRANSACTION-NOT-FOUNDTransaction not found

Cancel a sale

POST /commerce/cancel-transaction

Cancels a sale that hasn't been accepted yet.

Input Parameters

ParameterTypeRequiredDescription
folionumberNoThe transaction folio to cancel
folioExternalstringNoThe transaction external folio to cancel

Response (data)

{
"folio": 12345,
"status": "CANCELED",
"cancelledAt": "2024-01-15T10:40:00.000Z"
}

Exceptions

Error CodeDescription
SALE-CANCEL-01Sale not found
SALE-CANCEL-02Sale already cancelled
SALE-CANCEL-03Sale 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 behaviorMock it with status
Client approves the sale in the appAPPROVED
Client rejects the sale in the appREJECTED
Client never responds and the sale expiresEXPIRED
Sale is called offCANCELED
Force a completed sale into a returnRETURNED

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.

caution

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

ParameterTypeRequiredDescription
folionumberOne ofTransaction folio. Provide either folio or folioExternal
folioExternalstringOne ofExternal reference. Provide either folioExternal or folio
statusstringYesTarget status: APPROVED, REJECTED, EXPIRED, CANCELED, or RETURNED
  • APPROVED, REJECTED, EXPIRED, and CANCELED require the sale to be IN_PROGRESS.
  • RETURNED requires the sale to be APPROVED and behaves as a force-return: it fires the same TRANSACTION_RETURNED and TRANSACTION_STATUS_CHANGE webhook events as /commerce/return-transaction, and as a force-tool it intentionally bypasses the NO_RETURN flag (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 CodeHTTPDescription
TRANSACTION-NOT-FOUND404No folio/folioExternal provided, or no matching sale for your company
TRANSACTION-NOT-IN-PROGRESS400Target is APPROVED/REJECTED/EXPIRED/CANCELED but the sale is not IN_PROGRESS
TRANSACTION-NOT-RETURNABLE400Target is RETURNED but the sale is not APPROVED
SANDBOX-ONLY-ENDPOINT403Called 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

ParameterTypeRequiredDescription
folionumberOne ofTransaction folio. Provide either folio or folioExternal
folioExternalstringOne ofExternal reference. Provide either folioExternal or folio
returnReasonstringNoFree-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"
}
  • settlementImpact is NOT_YET_SETTLED when the commerce has not been paid for the sale yet (the sale is reverted), or DISCOUNT_NEXT_SETTLEMENT when the commerce was already paid (the amount is discounted from upcoming settlements).

Exceptions

Error CodeHTTPDescription
TRANSACTION-NOT-FOUND404No folio/folioExternal provided or sale not found
TRANSACTION-IN-PROGRESS-USE-CANCEL400Sale is still IN_PROGRESS — use cancel-transaction
TRANSACTION-NOT-RETURNABLE400Sale is REJECTED/EXPIRED/CANCELED (message has status)
RETURN-DECLINED409The return was declined
GATEWAY-ERROR502Upstream 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:

  1. Mock the client response (recommended) — create the sale with NO_CANCEL in folioExternal to disable the automatic transition, then call /commerce/resolve-transaction with the client's decision (APPROVED, REJECTED, EXPIRED, or CANCELED), or cancel the sale via /commerce/cancel-transaction. Deterministic and race-free.
  2. Automatic transitions (alternative) — leave folioExternal without flags and the sale auto-resolves ~5 seconds after creation. See Automatic transitions via folioExternal.
caution

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.

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 folioExternalResulting statusDescription
(none)APPROVEDSale automatically transitions to APPROVED after ~5 seconds
REJECTEDREJECTEDSale automatically transitions to REJECTED after ~5 seconds
NO_CANCELIN_PROGRESSAutomatic 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 folioExternalReturn outcome
(none)200 with settlementImpact: NOT_YET_SETTLED
COMMERCE_SETTLED200 with settlementImpact: DISCOUNT_NEXT_SETTLEMENT (simulates a commerce already paid)
WITH_PAYMENTS200 identical response; the emitted mock event carries clientRefundPending: true in its metadata (simulates a client with payments to refund)
NO_RETURN409 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.

tip

Combine these patterns with the predefined sandbox test clients to cover all integration scenarios.