Payment product guide

Balance inquiry, transfers, transaction inquiry, SWIFT messages, fiat accounts, and credit notifications. See the API reference for endpoint details.

Overview

The Stabo Payment OpenAPI is for corporate customers. With this API, you can:

  • Account management: Query account balance and status in real time
  • Fund transfers: Make intra-bank transfers, MEPS+ local transfers, and SWIFT cross-border remittances
  • Transaction inquiry: Query transaction history and transfer status
  • Fiat accounts: Open fiat collection accounts, and query the opening progress and collection details
  • Message retrieval: Retrieve the original MT/MX SWIFT messages
  • Real-time notifications: Receive credit and other event notifications through Webhooks

Version: v1.7

For onboarding, environment information, and authentication, see Quickstart and Authentication. For the protocol and the unified response format, see Conventions.

API overview

ModuleEndpointDescription
AccountGET /openapi/v1/payments/balanceBalance inquiry
TransferPOST /openapi/v1/payments/transfer/intra-bankIntra-bank transfer
TransferGET /openapi/v1/payments/transfer/intra-bank/{referenceNo}Intra-bank transfer result inquiry
TransferPOST /openapi/v1/payments/transfer/mepsMEPS+ transfer
TransferGET /openapi/v1/payments/transfer/meps/{referenceNo}MEPS+ transfer result inquiry
TransferPOST /openapi/v1/payments/transfer/swiftCross-border remittance
TransferGET /openapi/v1/payments/transfer/swift/{referenceNo}Cross-border remittance result inquiry
TransactionGET /openapi/v1/payments/transaction/historyTransaction history inquiry
TransactionGET /openapi/v1/payments/message/mtmxMT/MX message inquiry
FeePOST /openapi/v1/payments/fees/estimateEstimate the transfer fee
Provider KYCGET /openapi/v1/customers/provider-kyc/requirementsGet KYC requirements
Provider KYCPOST /openapi/v1/customers/provider-kycSubmit KYB
Provider KYCGET /openapi/v1/customers/provider-kycGet KYC status
Provider KYCGET /openapi/v1/customers/provider-kyc/rfisList RFIs
Provider KYCGET /openapi/v1/customers/provider-kyc/rfis/{rfiId}Get an RFI
Provider KYCPOST /openapi/v1/customers/provider-kyc/rfis/{rfiId}/questions/{questionId}/idv-sessionCreate an IDV session
Provider KYCPOST /openapi/v1/customers/provider-kyc/rfis/{rfiId}/answersSubmit RFI answers
Fiat accountGET /openapi/v1/fiat-accounts/opening-optionsList opening options
Fiat accountPOST /openapi/v1/fiat-accountsCreate a fiat account
Fiat accountGET /openapi/v1/fiat-accounts/{accountId}Retrieve an account
WebhookHTTPS URL configured by the merchantReceives the credit notification va.credit.create
WebhookHTTPS URL configured by the merchantLEGEND fiat account terminal events fiat.account.activated, fiat.account.failed, and fiat.account.closed. The payload carries only a few fields. Call Retrieve an account for the payment details
WebhookHTTPS URL that you configureProvider KYC status provider.kyc.submitted, provider.kyc.approved, provider.kyc.rejected, and provider.kyc.action_required. See Provider KYC and account opening

For the parameters, responses, and online testing of each endpoint, see the API reference.

Request headers

In addition to the request headers in Conventions, Payment endpoints require X-Channel-Provider.

HeaderDescriptionRequired
AuthorizationBearer {accessToken}Yes
Content-Typeapplication/json. GLDB create fiat account, submit KYB, and submit RFI answers use multipart/form-dataYes, for POST requests
Idempotency-KeyUnique request identifier that prevents duplicate operations (UUID recommended). For the rules, see IdempotencyYes. Required for the three transfer POST endpoints, LEGEND create fiat account, create digital wallet, generate a deposit address, submit KYB, and submit RFI answers. Do not send it for GLDB create fiat account
X-Channel-ProviderProvider identifier, such as GLDBYes

Common data fields

The data object of every business response contains the following common fields:

FieldDescriptionTypeExample
providerStatusCodeProvider response codeString000000
providerMessageProvider response messageStringok
providerRequestSerialNoProvider request serial numberString795883203712778240
providerResponseSerialNoProvider response serial numberString785957885476737024
providerTimestampProvider request timestampString2023-05-17 21:03:48

Error responses

Use the HTTP status code to determine whether a request succeeded. For the rules, see Errors. When the provider returns an error, the HTTP status code is 400, code in the response body is 500, and data contains the provider* fields returned by the provider:

{
  "code": 500,
  "message": "Operation failed",
  "traceId": "trace-uuid-string",
  "data": {
    "providerStatusCode": "100019",
    "providerMessage": "Version number is empty",
    "providerRequestSerialNo": "795883203712778240",
    "providerResponseSerialNo": "785957885476737024",
    "providerTimestamp": "2023-05-17 21:03:48"
  }
}

Provider response codes

The provider determines the values of providerStatusCode.

GLDB response codes

CodeDescription
000000Success
100019Version number is empty
100020Provider serial number is empty
100021Timestamp is empty
101119Timestamp mismatch (more than 2 minutes)
100111Field length exceeds the limit
100017Endpoint does not exist or is in an abnormal status
100004Signature verification failed
100067Payload decryption failed
100098Developer application is in an abnormal status
100060Developer certificate not found
100099Error sending to the downstream system
999999Unknown error
199999Downstream system returned an error

Transaction status

StatusDescription
SUCCESSSucceeded
PROCESSINGProcessing
SENT_TO_BANKSent to the bank
FAILEDFailed

Idempotency

The Stabo API supports idempotency to prevent duplicate operations (such as duplicate debits) caused by network interruptions or similar issues.

How it works:

  • The three transfer POST endpoints, LEGEND create fiat account, create digital wallet, and generate a deposit address must include Idempotency-Key in the request header
  • GLDB create fiat account does not support Idempotency-Key. A request with it is rejected. See Fiat accounts
  • Submit KYB and submit RFI answers also require Idempotency-Key, but a repeat request is not rejected. If the provider already accepted it, the current status is returned. See Provider KYC and account opening
  • Stabo processes requests that use the same Idempotency-Key only once
  • The client generates the Idempotency-Key (UUID recommended). It is valid for 24 hours
  • GET requests are idempotent by nature and do not need this header

Each request above must use a unique Idempotency-Key. Never reuse the same key for different operations.

If you reuse an Idempotency-Key within its validity period, the request is rejected (HTTP 400, message is Duplicate request detected). The result of the first request is not returned. A duplicate request is also rejected while the first request is still processing. The key is taken as soon as the request is accepted, and it is not released even if the first request fails. If the request times out or you receive no response, do not resubmit. For a transfer, query the result with referenceNo. For account or wallet creation, query by the returned ID. For address generation, query the address list.

Balance inquiry

Query the available balance of an account. accountNumber and currency are required.

curl -G "https://api.test.stabofx.com/openapi/v1/payments/balance" \
  -H "Authorization: Bearer {accessToken}" \
  -H "X-Channel-Provider: GLDB" \
  --data-urlencode "accountNumber=10000000001" \
  --data-urlencode "currency=USD"

Response example:

{
  "code": 200,
  "message": "OK",
  "traceId": "a1b2c3d4-e5f6-7890",
  "data": {
    "providerStatusCode": "000000",
    "providerMessage": "ok",
    "providerRequestSerialNo": "795883203712778240",
    "providerResponseSerialNo": "785957885476737024",
    "providerTimestamp": "2023-05-17 21:03:48",
    "balance": "1000000.00",
    "currency": "USD",
    "status": "ACTIVE"
  }
}

Values of status: ACTIVE / CLOSED / INACTIVE / DORMANT.

Intra-bank transfer

Transfer funds between accounts within the bank. All currencies are supported. Use it to move funds between different accounts at the same bank.

curl -X POST "https://api.test.stabofx.com/openapi/v1/payments/transfer/intra-bank" \
  -H "Authorization: Bearer {accessToken}" \
  -H "Content-Type: application/json" \
  -H "X-Channel-Provider: GLDB" \
  -H "Idempotency-Key: STABO20231121001" \
  -d '{
    "referenceNo": "STABO20231121001",
    "fromAccount": "10000000001",
    "beneficiaryAccount": "10000000002",
    "currency": "SGD",
    "amount": "200.00",
    "remarks": "Payment for services"
  }'

Response example:

{
  "code": 200,
  "message": "OK",
  "traceId": "a1b2c3d4-e5f6-7890",
  "data": {
    "providerStatusCode": "000000",
    "providerMessage": "ok",
    "providerRequestSerialNo": "795883203712778240",
    "providerResponseSerialNo": "785957885476737024",
    "providerTimestamp": "2023-05-17 21:03:48",
    "channelSerialNo": "742465990302056448",
    "status": "SUCCESS",
    "coreSerialNo": "FT23131QQQ2P",
    "poboAccountNo": "10000000003",
    "poboAccountName": "EXAMPLE PTE. LTD."
  }
}

Notes

  • remarks accepts only letters, digits, spaces, and the special characters / - ? : ( ) . , ' +
  • If the response status is PROCESSING, call the result inquiry endpoint after 5 minutes to confirm the final status
  • For orders paid out through POBO, the response returns poboAccountNo / poboAccountName. When you call the result inquiry endpoint, we recommend that you pass accountNumber=poboAccountNo to specify the POBO payout account explicitly. If you omit accountNumber, the system matches the POBO payout account automatically by referenceNo.

Transfer result inquiry

Each of the three transfer types has a result inquiry endpoint, with the same parameters and response. If a transfer returns PROCESSING, call the result inquiry endpoint to get the final result.

# Standard orders / POBO payout orders
GET /openapi/v1/payments/transfer/intra-bank/STABO20231121001

# For POBO payout orders, you can also specify the payout account explicitly
GET /openapi/v1/payments/transfer/intra-bank/STABO20231121001?accountNumber=10000000003

MEPS+ transfer

Make real-time local transfers in Singapore through MEPS+ (MAS Electronic Payment System).

Features:

  • SGD only
  • Real-time settlement
  • Suitable for high-value local transfers

Request example:

{
  "referenceNo": "STABO20231121002",
  "fromAccount": "10000000001",
  "beneficiaryAccount": "10000000004",
  "beneficiaryName": "Example Company",
  "beneficiaryAddress": "1 Example Road, Singapore 000000",
  "beneficiaryCountryCode": "SG",
  "currency": "SGD",
  "amount": "100.00",
  "beneficiaryBankCode": "EXMPSGS0XXX",
  "beneficiaryBankName": "EXAMPLE BANK LTD",
  "purposeCode": "BEXP",
  "chargeBearer": "SHA"
}

For the values of purposeCode and chargeBearer, see the Reference.

Notes

  • beneficiaryName, beneficiaryAddress, and remarks accept only letters, digits, spaces, and the special characters / - ? : ( ) . , ' +
  • If the beneficiary bank is not a MEPS+ participant, provide intermediary bank information
  • For orders paid out through POBO, the response returns poboAccountNo / poboAccountName. When you call the result inquiry endpoint, we recommend that you pass accountNumber=poboAccountNo to specify the POBO payout account explicitly. If you omit accountNumber, the system matches the POBO payout account automatically by referenceNo.

Cross-border remittance

Make cross-border remittances through the SWIFT network. Note: The GLDB provider does not support cross-border remittances in SGD.

Request example:

{
  "referenceNo": "STABO20231121003",
  "fromAccount": "10000000001",
  "beneficiaryAccount": "10000000005",
  "beneficiaryName": "Example Company",
  "beneficiaryAddress": "1 Example Road",
  "beneficiaryCountryCode": "SG",
  "beneficiaryCountryName": "Singapore",
  "currency": "USD",
  "amount": "1001.64",
  "beneficiaryBankCode": "EXMPSGS0XXX",
  "beneficiaryBankName": "EXAMPLE BANK LTD",
  "chargeBearer": "SHA",
  "purposeCode": "BEXP",
  "remarks": "open transfer test"
}

Notes (GLDB provider)

  • SGD is not supported
  • If a BIC code has fewer than 11 characters, pad it with X
  • beneficiaryName, beneficiaryAddress, and remarks accept only letters, digits, spaces, and the special characters / - ? : ( ) . , ' +
  • For orders paid out through POBO, the response returns poboAccountNo / poboAccountName. When you call the result inquiry endpoint, we recommend that you pass accountNumber=poboAccountNo to specify the POBO payout account explicitly. If you omit accountNumber, the system matches the POBO payout account automatically by referenceNo.

Transaction history inquiry

Query the transaction history of an account, with pagination. Use it for reconciliation and financial reporting.

GET /openapi/v1/payments/transaction/history?accountNumber=10000000001&currency=SGD&startDate=20240213&endDate=20240426&currentPage=1&pageSize=20

A positive amount is a credit (incoming), and a negative amount is a debit (outgoing). The maximum pageSize is 100. For the values of transactionType, see Transaction type codes.

Estimate the transfer fee

Calculate the estimated fee based on the transfer type and amount. The request body is JSON.

Request example:

{
  "transferType": "SWIFT",
  "amount": "100.00",
  "currency": "USD",
  "usdEquivalentAmount": "100.00",
  "chargeOption": "SHA"
}

fee has two decimal places and is rounded up. For example, 1.221 becomes 1.23. The fee currency is SGD for INTRA_BANK, MEPS, and SWIFT SGD, and USD for non-SGD SWIFT. The current fee estimation logic does not depend on chargeOption.

MT/MX message inquiry

Query the original MT/MX SWIFT message associated with a transfer. Use it for compliance audits and troubleshooting. paymentReference comes from the transfer result inquiry endpoint.

GET /openapi/v1/payments/message/mtmx?paymentReference=SGL24241GG0LJHCB&direction=OUTGOING

SWIFT message examples

The following examples show the content of the messageContent field in different scenarios. The institutions, account numbers, and reference numbers in the examples are placeholders.

Credit notification (va.credit.create)

When an account receives incoming funds, Stabo sends a va.credit.create event through a Webhook.

The notification mechanism, retry policy, request headers, event object structure, and signature verification are the same as for other products. See Webhooks. This section describes only the payload of this event. Webhook-Event-Type is va.credit.create.

Best practice: When you receive a Webhook notification, return a 2xx response first and then process the business logic asynchronously. This prevents duplicate deliveries caused by processing timeouts.

data.object fields

FieldTypeRequiredDescription
transactionReferenceStringYTransaction reference number
tradeDateStringYTransaction date, in the bank's original format yyyyMMdd, such as 20260528
tradeTimeStringYTransaction time, in the bank's original format HHmmssSSS. For example, 101135517 means 10:11:35.517
valueDateStringYValue date, in the bank's original format yyyyMMdd, such as 20260528
currencyStringYCurrency
amountStringYAmount
senderAccountStringYPayer account number
senderNameStringYPayer name
senderBankCodeStringNPayer bank code
receiverAccountStringYBeneficiary account number
receiverNameStringYBeneficiary name
receiverBankCodeStringNBeneficiary bank code
remarksStringNRemarks
directionStringYDirection (INCOMING/OUTGOING)
paymentReferenceStringNPayment reference number
sourceStringYSource (SWIFT/SWIFTMX/FAST/MEPS/INTERNAL)
eventReferenceStringYEvent reference number
accountNumberStringYAccount number
senderBankNameStringNPayer bank name
masterAccountNoStringNMaster account number. Applies only to multi-currency accounts; single-currency messages do not include this field

Example payload

{
  "id": "evt_1234567890",
  "object": "event",
  "type": "va.credit.create",
  "created": 1712345678,
  "data": {
    "object": {
      "transactionReference": "TXN0000000001",
      "tradeDate": "20260528",
      "tradeTime": "101135517",
      "valueDate": "20260528",
      "currency": "USD",
      "amount": "20600.00",
      "senderAccount": "123456789",
      "senderName": "EXAMPLE SENDER PTE LTD",
      "senderBankCode": "EXMPHKHHXXX",
      "receiverAccount": "10000000001",
      "receiverName": "EXAMPLE RECEIVER PTE LTD",
      "receiverBankCode": "EXMBSGSGXXX",
      "remarks": "INVOICE-0001",
      "direction": "INCOMING",
      "paymentReference": "SGL0000000000000",
      "source": "SWIFTMX",
      "eventReference": "00000000-0000-4000-8000-000000000001",
      "accountNumber": "10000000001",
      "senderBankName": "EXAMPLE BANK LIMITED",
      "masterAccountNo": "10000000000"
    }
  }
}

Fiat accounts

Fiat accounts use the X-Channel-Provider request header to select the provider. LEGEND and GLDB are supported.

Before a LEGEND account is opened, the customer must complete KYC at the provider and get a providerCustomerId. For the full flow from KYB to account opening with examples, see Provider KYC and account opening.

The two providers differ in request format and opening flow:

ItemLEGENDGLDB
Create request formatapplication/jsonmultipart/form-data, with the opening documents uploaded in the same request
Idempotency-KeyRequiredDo not send it. If you send it, fiat.account.idempotency_key.unsupported is returned
Provider-specific fieldsproviderCustomerId (required), bankCountry, alias, openingOptionIdaccountName, contactName, contactEmail, contactPhone, companyRegistrationDoc (all required), accountOpeningProof, otherDocuments (optional)
Opening flowSynchronous or asynchronous, decided by the providerManual review. Create returns PENDING. After approval, the status is ACTIVE
List opening optionsSupportedNot supported. openapi.channel.unsupported is returned
Retrieve an accountproviderCustomerId is requiredDo not send providerCustomerId. accountId is the application number
Status notificationA Webhook is sent for terminal statusesNo Webhook. Poll the account detail
  • currency is required for both providers. It accepts only one currency.
  • If you send a field that belongs to the other provider, fiat.account.field.unsupported is returned.
  • If a field that this provider requires is missing, fiat.account.field.required is returned.

Some providers open the account synchronously, and the create response is the account detail. Some providers open the account asynchronously, and create returns only PENDING. The account can receive funds only after it is ACTIVE.

List opening options (optional, LEGEND only) → Create a fiat account (required) → Retrieve an account (conditional)
StepEndpointRequiredDescription
1List opening optionsOptional, LEGEND onlyCall it when you need to choose the opening route or confirm the fees in advance. If you do not call it, the provider chooses the route.
2Create a fiat accountRequiredFor LEGEND, submit JSON. If you chose a route in step 1, send back the returned openingOptionId. On synchronous success, the response is already the standard account. For GLDB, submit multipart/form-data with the opening documents. The response is PENDING.
3Retrieve an accountConditionalIf create returns PENDING, poll until a terminal status. For GLDB, you must poll. If create returns fiat.account.channel.detail_unavailable, query with the account ID in the message. Do not open the account again. If create already returned ACTIVE, you do not need to query.

PENDING means the request is accepted but the account cannot receive funds yet. ACTIVE includes at least an account number or an IBAN.

Integration sequence examples

Examples for other providers will be added to this section.

Reference

For charge bearer, purpose codes, transaction type codes, and country codes, see the Payment reference.

FAQ and support

Q: How do I make sure a transfer is not executed twice?

A: Use a unique Idempotency-Key for each transfer request. Each key is processed only once, and resubmitting it within the validity period is rejected. After a network timeout or a missing response, do not resubmit; call the result inquiry endpoint with referenceNo to confirm the status. See Idempotency.

For general questions and how to contact technical support, see the FAQ. For how error responses work, see Errors. Include the traceId when you contact technical support.

On this page