Verify merchants and send payouts
Register merchants with one or more Pago Móvil payout methods, confirm micro-deposit amounts per method, then pay verified destinations with exact VES amounts and item-level idempotency.
Three golden rules
- Payout amounts are always explicit tenant strings with exactly two decimals — the gateway never calculates or converts FX for payouts.
- Idempotency is enforced per externalRef at item level before any bank credit is sent.
- Every CreditoInmediato AC00 response is reconciled through the same ConsultarOperaciones poller used for débito inmediato.
externalRef rules
You always choose externalRef. The API never generates one. Send your own stable id (user id, settlement id, order number) so you can look records up later.
| Rule | Detail |
|---|---|
| Required | Required on POST /v1/merchants, POST /v1/payouts, and each POST /v1/payouts/batch item. |
| Length | 1–64 characters after trim. |
| Scope | Unique per tenant. The same string may exist under another tenant. |
| Ownership | Client-supplied only. Portal Generate is a UI helper for manual testing; production integrations must send their own value in the JSON body. |
| Replay (same payload) | Returns the existing merchant or payout (200) and does not re-hit the bank. |
| Conflict (different payload) | Returns 409 external_ref_conflict — unless the merchant has no payout methods left (e.g. after a BE01 mismatch cleanup or a shell create), in which case POST /v1/merchants with bankCode+phone attaches a new method (200), or POST without them updates the shell identity (200). |
| Recovery | GET /v1/merchants?externalRef=…, GET /v1/merchants/:id, GET /v1/payouts/by-ref/:externalRef, GET /v1/payouts/:id |
Merchant verification
| Step | Endpoint |
|---|---|
| 1. Register merchant | POST /v1/merchants (optional bankCode + phone creates the first payout method; omit both for a shell merchant) |
| 2. Add another method (optional) | POST /v1/merchants/:id/payout-methods |
| 3. Start micro-deposits | POST /v1/merchants/:id/payout-methods/:methodId/verify/start |
| 4. Confirm amounts | POST /v1/merchants/:id/payout-methods/:methodId/verify |
| 5. List methods | GET /v1/merchants/:id/payout-methods |
| 6. Set default method | POST /v1/merchants/:id/payout-methods/:methodId/default |
| 7. Delete a method | DELETE /v1/merchants/:id/payout-methods/:methodId |
| 8. Read status | GET /v1/merchants/:id or GET /v1/merchants?externalRef=… |
| 9. List payout-ready | GET /v1/merchants?status=verified&isActive=true&limit=25 |
| 10. Deactivate / reactivate | PATCH /v1/merchants/:id `{ "isActive": false|true }` |
| 11. Delete (no payouts) | DELETE /v1/merchants/:id — 409 merchant_has_payouts if history exists |
curl --request POST "${API_URL}/v1/merchants" \
--header "content-type: application/json" \
--header "x-api-key: ${VEXPAY_API_KEY}" \
--data '{
"externalRef": "usr_7f3a",
"name": "Maria Perez",
"identification": "V12345678",
"contactEmail": "maria@mail.com",
"contactPhone": "04141234567",
"bankCode": "0134",
"phone": "04145555555"
}'curl --request POST "${API_URL}/v1/merchants" \
--header "content-type: application/json" \
--header "x-api-key: ${VEXPAY_API_KEY}" \
--data '{
"externalRef": "usr_7f3a",
"name": "Maria Perez",
"identification": "V12345678",
"contactEmail": "maria@mail.com",
"contactPhone": "04141234567"
}'
# → payoutMethods: [], message: "No payout method exists…"curl "${API_URL}/v1/merchants/MERCHANT_ID/payout-methods" \
--header "x-api-key: ${VEXPAY_API_KEY}"
curl --request POST "${API_URL}/v1/merchants/MERCHANT_ID/payout-methods" \
--header "content-type: application/json" \
--header "x-api-key: ${VEXPAY_API_KEY}" \
--data '{
"bankCode": "0105",
"phone": "04141234567"
}'curl --request POST "${API_URL}/v1/merchants/MERCHANT_ID/payout-methods/METHOD_ID/verify/start" \
--header "x-api-key: ${VEXPAY_API_KEY}"
curl --request POST "${API_URL}/v1/merchants/MERCHANT_ID/payout-methods/METHOD_ID/verify" \
--header "content-type: application/json" \
--header "x-api-key: ${VEXPAY_API_KEY}" \
--data '{ "amount1": "0.37", "amount2": "0.82" }'curl --request POST "${API_URL}/v1/merchants/MERCHANT_ID/payout-methods/METHOD_ID/default" \
--header "x-api-key: ${VEXPAY_API_KEY}"
curl --request DELETE "${API_URL}/v1/merchants/MERCHANT_ID/payout-methods/METHOD_ID" \
--header "x-api-key: ${VEXPAY_API_KEY}"curl "${API_URL}/v1/merchants?status=verified&isActive=true&limit=25" \
--header "x-api-key: ${VEXPAY_API_KEY}"
curl "${API_URL}/v1/merchants?externalRef=usr_7f3a" \
--header "x-api-key: ${VEXPAY_API_KEY}"
curl "${API_URL}/v1/merchants/MERCHANT_ID" \
--header "x-api-key: ${VEXPAY_API_KEY}"curl --request PATCH "${API_URL}/v1/merchants/MERCHANT_ID" \
--header "content-type: application/json" \
--header "x-api-key: ${VEXPAY_API_KEY}" \
--data '{ "isActive": false }'curl --request DELETE "${API_URL}/v1/merchants/MERCHANT_ID" \
--header "x-api-key: ${VEXPAY_API_KEY}"
# 200 → { "deleted": true, "merchantId": "…", "externalRef": "…" }
# 409 → { "error": "merchant_has_payouts", "payoutCount": N }Delete a merchant
Hard-delete a merchant (and its payout methods) when it has never received a payout. This frees the tenant-scoped externalRef so you can re-register the same seller id. Prefer PATCH isActive:false when payout history exists.
| Outcome | Detail |
|---|---|
| DELETE /v1/merchants/:id | Tenant-scoped by x-api-key. Path id is the gateway merchant UUID. |
| 200 OK | `{ "deleted": true, "merchantId", "externalRef" }` — merchant and payout methods removed; externalRef is free. |
| 404 | Merchant not found for this tenant. |
| 409 merchant_has_payouts | At least one payout exists (`payoutCount`). Deactivate with PATCH `{ "isActive": false }` instead. |
curl --request DELETE "${API_URL}/v1/merchants/00000000-0000-4000-8000-000000000000" \
--header "x-api-key: ${VEXPAY_API_KEY}"{
"deleted": true,
"merchantId": "00000000-0000-4000-8000-000000000000",
"externalRef": "usr_7f3a"
}{
"error": "merchant_has_payouts",
"payoutCount": 3
}Find a merchant by externalRef
Within a tenant, each merchant is uniquely identified by the externalRef you sent on register. Prefer looking up by that ref when your system stores the seller/user id, not the gateway merchantId. Auth is the tenant API key (`x-api-key`) — the lookup is always scoped to that tenant.
| Endpoint | Returns |
|---|---|
| GET /v1/merchants?externalRef={ref} | Single merchant detail (same shape as GET /v1/merchants/:id), including payoutMethods[] |
| GET /v1/merchants/:id | Same detail when you already have the gateway UUID |
| GET /v1/merchants | Paginated list (`items`, `nextCursor`) when externalRef is omitted |
curl "${API_URL}/v1/merchants?externalRef=usr_7f3a" \
--header "x-api-key: ${VEXPAY_API_KEY}"const merchant = await fetch(
`${process.env.API_URL}/v1/merchants?externalRef=${encodeURIComponent('usr_7f3a')}`,
{ headers: { 'x-api-key': process.env.VEXPAY_API_KEY } }
).then((r) => r.json());
// merchant.merchantId, merchant.status, merchant.payoutMethodsSimple and batch payouts
| Endpoint | Purpose |
|---|---|
| POST /v1/payouts | Pay one verified + active merchant (optional payoutMethodId) |
| POST /v1/payouts/batch | Pay many merchants; results are per item |
| GET /v1/payouts | List payouts (optional status filter) |
| GET /v1/payouts/:id | Recover by payout id |
| GET /v1/payouts/by-ref/:externalRef | Recover by externalRef |
curl --request POST "${API_URL}/v1/payouts" \
--header "content-type: application/json" \
--header "x-api-key: ${VEXPAY_API_KEY}" \
--data '{
"merchantId": "00000000-0000-4000-8000-000000000000",
"payoutMethodId": "00000000-0000-4000-8000-000000000001",
"monto": "1523.40",
"concepto": "Liquidacion semana 30",
"externalRef": "po_9c21"
}'POST /v1/payouts/batch pays many verified merchants in one request. Each item is its own payout (own externalRef, status, and webhooks). Failures are per item — they do not roll back siblings.
curl --request POST "${API_URL}/v1/payouts/batch" \
--header "content-type: application/json" \
--header "x-api-key: ${VEXPAY_API_KEY}" \
--data '{
"externalRef": "corte_semana_30",
"items": [
{
"merchantId": "00000000-0000-4000-8000-000000000000",
"payoutMethodId": "00000000-0000-4000-8000-000000000001",
"monto": "1523.40",
"concepto": "Liquidacion semana 30",
"externalRef": "po_9c21_seller"
},
{
"merchantId": "00000000-0000-4000-8000-000000000002",
"monto": "210.00",
"concepto": "Liquidacion semana 30",
"externalRef": "po_9c21_partner"
}
]
}'{
"batchId": "00000000-0000-4000-8000-000000000010",
"items": [
{
"payoutId": "00000000-0000-4000-8000-000000000011",
"status": "completed",
"reference": "12345678",
"failureCode": null,
"externalRef": "po_9c21_seller",
"merchantId": "00000000-0000-4000-8000-000000000000",
"payoutMethodId": "00000000-0000-4000-8000-000000000001",
"merchantName": "Maria Perez",
"monto": "1523.40",
"concepto": "Liquidacion semana 30",
"bankCode": "0134",
"destination": "…5555",
"operationId": null,
"createdAt": "2026-07-28T16:00:00.000Z",
"settledAt": "2026-07-28T16:00:01.000Z"
},
{
"payoutId": "00000000-0000-4000-8000-000000000012",
"status": "pending",
"reference": null,
"failureCode": null,
"externalRef": "po_9c21_partner",
"merchantId": "00000000-0000-4000-8000-000000000002",
"payoutMethodId": "00000000-0000-4000-8000-000000000003",
"merchantName": "Socio CA",
"monto": "210.00",
"concepto": "Liquidacion semana 30",
"bankCode": "0102",
"destination": "…1234",
"operationId": "AC00-OP-EXAMPLE",
"createdAt": "2026-07-28T16:00:00.000Z",
"settledAt": null
}
]
}curl "${API_URL}/v1/payouts?status=pending&limit=25" \
--header "x-api-key: ${VEXPAY_API_KEY}"
curl "${API_URL}/v1/payouts/PAYOUT_ID" \
--header "x-api-key: ${VEXPAY_API_KEY}"Webhook events
- merchant.verified / merchant.rejected
- merchant.deactivated / merchant.reactivated
- payout.completed / payout.failed
- Envelope remains { event, data, timestamp } with X-Webhook-Signature over the exact raw body.
{
"event": "merchant.rejected",
"data": {
"merchantId": "292d478b-a1fb-4d27-ab5c-8701ed14da88",
"externalRef": "47b53d97-489f-4f8e-8843-6c24045e168d",
"payoutMethodId": "0064456b-7a6a-4b7e-b0b5-d9b27b0dfa5c",
"failureCode": "BE01",
"reason": "Datos del cliente no corresponden a la cuenta",
"action": "payout_method_deleted"
},
"timestamp": "2026-07-23T18:12:05.000Z"
}