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"
     }'
FieldTypeDescription
typestringMust be "SPEI" for Mexican bank transfer payments
aliasstringDescriptive 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, or datetime.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:

FieldTypeDescription
country_codestringISO country code. For Mexico: "MX"
merchant_order_idstringUnique ID from your own system for this order (used to prevent duplicates)
currencystringISO 4217 currency code. For Mexican pesos: "MXN"
subtotal_amountnumberSum of all product prices before discounts
total_amountnumberFinal amount to charge (subtotal_amount - sum(discounts))
discountsarrayList of applied discounts. The sum must match subtotal_amount - total_amount
productsarrayLine items included in the order
customerobjectBuyer 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}"
     }'
HeaderDescription
Idempotency-KeyA 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 or uuid.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:

FieldDescription
statusAlways PAYMENT_ACTION_REQUIRED for SPEI — the payment is pending the buyer's bank transfer
user_action_required.offline_payment_provider.clabe18-digit CLABE (interbank account number) the buyer must transfer funds to
user_action_required.offline_payment_provider.referenceNumeric reference the buyer must include in their transfer
user_action_required.offline_payment_provider.bankName of the receiving bank
user_action_required.offline_payment_provider.url_payment_receiptURL to download a receipt with payment instructions for the buyer

Post-payment flow: The PAYMENT_ACTION_REQUIRED status means the order is active and awaiting the buyer's transfer. OrkestaPay will notify your system of the status change (e.g., COMPLETED or FAILED) 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

IDObtained atPurpose
access_tokenStep 1Authentication header for all API calls
payment_method_idStep 2Reference the SPEI method when registering the payment
order_idStep 3Link the order to the payment; query order status
payment_idStep 4Payment tracking and reconciliation
clabe + referenceStep 4Transfer instructions to display to the buyer

Common errors

ScenarioLikely cause
401 Unauthorizedaccess_token is expired or invalid — re-authenticate
Duplicate paymentThe same Idempotency-Key was sent for two different payment attempts
Order expired at payment timeBuyer did not transfer before expires_at — create a new order