SPEI
Integration Guide: SPEI Payments on OrkestaPay
This guide covers the complete flow for integrating bank transfer payments via SPEI (Sistema de Pagos Electrónicos Interbancarios — Mexico's interbank electronic payment network) using the OrkestaPay API.
Sandbox environment: All examples use the sandbox endpoint
https://api.sand.orkestapay.com. For production, replace the domain with the corresponding production endpoint.
Flow overview
1. Authentication → Obtain access_token
2. Create payment method → Obtain payment_method_id
3. Create order → Obtain order_id
4. Register payment → Obtain CLABE + reference for the buyer
Tip: Persist the IDs returned at each step (
payment_method_id,order_id,payment_id) — you will need them in subsequent steps and for transaction tracking.
Step 1 — Authentication
Obtain an access_token by calling OrkestaPay's authentication service with your API credentials. This token is passed as a Bearer token on all other endpoints.
📘 Docs: https://orkestapay-en.readme.io/docs/autenticación
Tokens expire. Implement refresh or re-authentication logic as described in the official documentation.
Step 2 — Create a SPEI payment method
Register a SPEI payment intent. This generates a payment_method_id that you will reference when registering the payment.
Request
curl --request POST \
--url https://api.sand.orkestapay.com/v1/payment-methods \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer {ACCESS_TOKEN}' \
--data '{
"type": "SPEI",
"alias": "SPEI payment method"
}'| Field | Type | Description |
|---|---|---|
type | string | Must be "SPEI" for Mexican bank transfer payments |
alias | string | Descriptive label to identify this payment method |
Response
{
"payment_method_id": "pym_d8dc1499cd6f47ad9fb352ae35690c5c",
"alias": "SPEI payment method",
"type": "SPEI",
"status": "ACTIVE",
"created_at": "1781841077671",
"updated_at": "1781841077671"
}Save the
payment_method_id— you will need it in Step 4.
Note on
created_at/updated_at: Timestamps are Unix Epoch in milliseconds (ms). To convert to a human-readable date:new Date(1781841077671)in JavaScript, ordatetime.fromtimestamp(1781841077671 / 1000)in Python.
📘 Docs: https://orkestapay-en.readme.io/reference/create-payment-method
Step 3 — Create an order
Register the purchase details: line items, amounts, and customer data. This represents the transaction's checkout.
Request
curl --request POST \
--url https://api.sand.orkestapay.com/v1/orders \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer {ACCESS_TOKEN}' \
--data '{
"country_code": "MX",
"merchant_order_id": "1366656595193",
"currency": "MXN",
"subtotal_amount": 1000,
"total_amount": 990,
"discounts": [
{
"amount": 10
}
],
"products": [
{
"product_id": "7197",
"name": "Pantalla TCL Smart TV Serie A3 A343 HD Android TV 40",
"quantity": 1,
"unit_price": 1000
}
],
"customer": {
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]"
}
}'Request body fields:
| Field | Type | Description |
|---|---|---|
country_code | string | ISO country code. For Mexico: "MX" |
merchant_order_id | string | Unique ID from your own system for this order (used to prevent duplicates) |
currency | string | ISO 4217 currency code. For Mexican pesos: "MXN" |
subtotal_amount | number | Sum of all product prices before discounts |
total_amount | number | Final amount to charge (subtotal_amount - sum(discounts)) |
discounts | array | List of applied discounts. The sum must match subtotal_amount - total_amount |
products | array | Line items included in the order |
customer | object | Buyer information |
Response
{
"order_id": "ord_466e5680ca3d44fa83045bd113d7a1df",
"status": "CREATED",
"expires_at": "1781927907513",
"merchant_order_id": "1366656595193",
"customer": {
"source": "ORDER",
"customer_id": "cus_21fa0cf02d1c4a73baf5b4f1c712c557",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"created_at": "1781841507423",
"updated_at": "1781841507423"
},
"placed_at": "1781841507513",
"country": "México",
"country_code": "MX",
"currency": "MXN",
"subtotal_amount": 1000,
"discounts": [
{
"amount": 10.0
}
],
"total_amount": 990,
"products": [
{
"product_id": "7197",
"quantity": 1,
"unit_price": 1000.0,
"name": "Pantalla TCL Smart TV Serie A3 A343 HD Android TV 40"
}
],
"order_type": "STANDARD"
}Save the
order_id— you will need it in Step 4.
Note on
expires_at: Orders have a limited validity window. If the buyer does not complete the transfer before this timestamp, the order will expire and you will need to create a new one. Display this deadline clearly in your UI.
📘 Docs: https://orkestapay-en.readme.io/reference/create-order
Step 4 — Register payment
Using the payment_method_id and order_id obtained in the previous steps, register the payment. The response will include the CLABE and reference number that the buyer must use to complete the bank transfer from their online banking.
Request
curl --request POST \
--url https://api.sand.orkestapay.com/v1/payments \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer {ACCESS_TOKEN}' \
--header 'Idempotency-Key: {UNIQUE_UUID_PER_ATTEMPT}' \
--data '{
"payment_source": {
"type": "SPEI",
"payment_method_id": "{PAYMENT_METHOD_ID}"
},
"order_id": "{ORDER_ID}"
}'| Header | Description |
|---|---|
Idempotency-Key | A unique UUID per new payment attempt. Reusing the same value on a retry prevents duplicate charges. |
Note on
Idempotency-Key: Generate a fresh UUID for each distinct payment attempt (e.g.,crypto.randomUUID()in Node.js oruuid.uuid4()in Python). If you are retrying the exact same request due to a network error, reuse the same key to guarantee idempotency.
Response
{
"payment_id": "pay_bcd1d0cd28964567af02825088cd1bc2",
"order_id": "ord_466e5680ca3d44fa83045bd113d7a1df",
"status": "PAYMENT_ACTION_REQUIRED",
"payment_source": {
"type": "SPEI",
"payment_method_id": "pym_d8dc1499cd6f47ad9fb352ae35690c5c"
},
"amount": {
"requested": 102.0,
"currency": "MXN"
},
"user_action_required": {
"type": "OFFLINE_PAYMENT",
"offline_payment_provider": {
"reference": "1282938",
"bank": "Finco Pay",
"clabe": "734180000066931684",
"url_payment_receipt": "https://api.sand.orkestapay.com/public/v1/offline-payments/bank-transfers/0e5264367c474b0481fe777bd2e32587/receipt"
}
},
"transactions": [
{
"type": "REGISTER",
"transaction_id": "079d36ffe02b4979bb1754b560f837a3",
"status": "SUCCESS",
"amount": 102.0,
"provider": {
"merchant_provider_id": "mpv_c102dd2cef374873b16153616ebaf90c",
"provider_id": "prc_40000c08f7a649b1ab065b6158b2e0dc",
"name": "bank_transfer"
},
"created_at": "1781841416576"
}
],
"created_at": "1781841409679",
"updated_at": "1781841409679"
}Key response fields:
| Field | Description |
|---|---|
status | Always PAYMENT_ACTION_REQUIRED for SPEI — the payment is pending the buyer's bank transfer |
user_action_required.offline_payment_provider.clabe | 18-digit CLABE (interbank account number) the buyer must transfer funds to |
user_action_required.offline_payment_provider.reference | Numeric reference the buyer must include in their transfer |
user_action_required.offline_payment_provider.bank | Name of the receiving bank |
user_action_required.offline_payment_provider.url_payment_receipt | URL to download a receipt with payment instructions for the buyer |
Post-payment flow: The
PAYMENT_ACTION_REQUIREDstatus means the order is active and awaiting the buyer's transfer. OrkestaPay will notify your system of the status change (e.g.,COMPLETEDorFAILED) via webhook once the bank confirms the transaction. Make sure your webhook endpoint is configured in the OrkestaPay dashboard.
📘 Docs: https://orkestapay-en.readme.io/reference/create-payment
IDs to persist
| ID | Obtained at | Purpose |
|---|---|---|
access_token | Step 1 | Authentication header for all API calls |
payment_method_id | Step 2 | Reference the SPEI method when registering the payment |
order_id | Step 3 | Link the order to the payment; query order status |
payment_id | Step 4 | Payment tracking and reconciliation |
clabe + reference | Step 4 | Transfer instructions to display to the buyer |
Common errors
| Scenario | Likely cause |
|---|---|
401 Unauthorized | access_token is expired or invalid — re-authenticate |
| Duplicate payment | The same Idempotency-Key was sent for two different payment attempts |
| Order expired at payment time | Buyer did not transfer before expires_at — create a new order |
