> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.crisscross.money/api-reference/collect/payments/payment-initiation/initiate-transaction/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.crisscross.money/_mcp/server. # Initiate Payment (Transaction) POST https://api.crisscross.money/v1/payment Content-Type: application/json Initiates a payment transaction for an existing checkout session. PayOS models a "payment" as a transaction; the primary identifier is `transactionId`. Reference: https://docs.crisscross.money/api-reference/collect/payments/payment-initiation/initiate-transaction ## Request ### Body (application/json) This endpoint expects an InitiateTransactionRequest. - `paymentMethodId` (enum, required) — Payment method identifier. - Allowed values: `card`, `directcharge`, `mobilemoney`, `ieft`, `banktransfer`, `capitecpay`, `cardpresent`, `digitalwallet`, `bnpl` - `sessionId` (string, required) — Unique identifier for the session. - `paymentDetails` (PaymentDetails, required) - `signature` (string, optional) — Optional signature used for hosted checkout verification. - `attributes` (map from string to string, optional) — Optional transaction attributes. ## Response ### 201 Transaction initiated successfully. - `transactionId` (string, required) — Unique identifier for the transaction. - `status` (string, required) — Current transaction status. - `message` (string, required) — Human-readable status message. - `authState` (AuthState, optional) — Detail for the transaction's current state. When the transaction status is `AUTH_REQUIRED`, `authMethodType` says how the payer completes authentication, and a matching detail object (`redirect`, `fields`, `ussd`, `bankTransfer`) carries the specifics. Error states instead carry `code`, `message`, and the raw `connectorFailureCode`/`connectorFailureMessage`. ## Errors ### 400 Bad Request Error Invalid request parameters. - `message` (string, required) — Human-readable description of what went wrong. - `error` (string, required) — Error category. Most endpoints return the HTTP reason phrase (e.g. "Bad Request", "Unauthorized", "Conflict", "Unprocessable Entity", "Internal Server Error"). Some return a machine-readable snake_case code instead — notably the FX and rate-lock rejections on checkout session creation and payment initiation, where several distinct codes share a single HTTP status. Where a code is present it is the finer discriminator and is safe to branch on. - `statusCode` (integer, required) — HTTP status code, mirroring the response header. The response header is the source of truth. ### 401 Unauthorized Error Unauthorized. - `message` (string, required) — Human-readable description of what went wrong. - `error` (string, required) — Error category. Most endpoints return the HTTP reason phrase (e.g. "Bad Request", "Unauthorized", "Conflict", "Unprocessable Entity", "Internal Server Error"). Some return a machine-readable snake_case code instead — notably the FX and rate-lock rejections on checkout session creation and payment initiation, where several distinct codes share a single HTTP status. Where a code is present it is the finer discriminator and is safe to branch on. - `statusCode` (integer, required) — HTTP status code, mirroring the response header. The response header is the source of truth. ### 422 Unprocessable Entity Error Unprocessable Entity. If the session carries a rate lock, the lock is re-verified here: one that is unknown, expired, or cancelled is rejected and a new lock must be fetched. Currency mismatches are not checked at this step. A lock's currencies are validated once, when it is attached at session creation, so only the two rejections below can occur here. Both share `422` — branch on `error`. - `message` (string, required) — Human-readable description of what went wrong. - `error` (string, required) — Error category. Most endpoints return the HTTP reason phrase (e.g. "Bad Request", "Unauthorized", "Conflict", "Unprocessable Entity", "Internal Server Error"). Some return a machine-readable snake_case code instead — notably the FX and rate-lock rejections on checkout session creation and payment initiation, where several distinct codes share a single HTTP status. Where a code is present it is the finer discriminator and is safe to branch on. - `statusCode` (integer, required) — HTTP status code, mirroring the response header. The response header is the source of truth. ### 502 Bad Gateway Error The rate-lock service could not be reached. Transient — retry with backoff. This is an upstream failure, not a problem with your request or your lock. - `message` (string, required) — Human-readable description of what went wrong. - `error` (string, required) — Error category. Most endpoints return the HTTP reason phrase (e.g. "Bad Request", "Unauthorized", "Conflict", "Unprocessable Entity", "Internal Server Error"). Some return a machine-readable snake_case code instead — notably the FX and rate-lock rejections on checkout session creation and payment initiation, where several distinct codes share a single HTTP status. Where a code is present it is the finer discriminator and is safe to branch on. - `statusCode` (integer, required) — HTTP status code, mirroring the response header. The response header is the source of truth. ## Types ### PaymentDetails - `type`: `card` (card) - `cardExpiryMonth` (string, required) — Expiry month (MM). - `cardExpiryYear` (string, required) — Expiry year (YYYY). - `cardHolderName` (string, required) - `encryptedCardCvv` (string, required) — Encrypted CVV token. - `encryptedCardNumber` (string, required) — Encrypted card number token. - `payerEmail` (string, required) - `type`: `mobilemoney` (mobilemoney) - `payerMobileNumber` (string, required) — Payer mobile number. - `provider` (string, required) — Mobile money provider (varies by country). - `payerFullName` (string, optional) — Full name of the payer. Falls back to the checkout session payer name when omitted. - `type`: `digitalwallet` (digitalwallet) - `deviceFingerprint` (string, required) — Base64-encoded device fingerprint from the wallets SDK, used for fraud screening. Omitting it will cause the payment to be rejected. - `encryptedWalletToken` (string, required) — Base64-encoded, single-use payment token released by the wallet. Short-lived and not reusable: a retry after a decline requires a new token from a new wallet interaction. Do not log it, and do not transform it. - `payerEmail` (string, required) - `provider` (enum, required) — The wallet that produced the token. - Allowed values: `apple-pay`, `google-pay` - `type`: `bnpl` (bnpl) - `payerEmail` (string, required) — Identifies the customer within the instalment flow. - `payerMobileNumber` (string, optional) — Optional. Speeds up the instalment flow when supplied. - `type`: `directcharge` (directcharge) - `type`: `ieft` (ieft) - `type`: `banktransfer` (banktransfer) - `type`: `capitecpay` (capitecpay) - `type`: `cardpresent` (cardpresent) ### AuthState Detail for the transaction's current state. When the transaction status is `AUTH_REQUIRED`, `authMethodType` says how the payer completes authentication, and a matching detail object (`redirect`, `fields`, `ussd`, `bankTransfer`) carries the specifics. Error states instead carry `code`, `message`, and the raw `connectorFailureCode`/`connectorFailureMessage`. - `state` (string, optional) — Transaction state name. In-flight states are uppercase (`PENDING`, `SCREENING_PENDING`, `SCREENING_COMPLETED`, `AUTH_REQUIRED`); terminal states are lowercase (`completed`, `failed`, `error`, `cancelled`, `expired`, `settled`). Match exact values, casing included. - `transitionedAt` (string, optional) — ISO 8601 timestamp when the transaction entered this state. - `authMethodType` (enum, optional) — How the payer completes authentication. Present on `AUTH_REQUIRED` states. - Allowed values: `redirect`, `fields`, `ussd`, `pendingApproval`, `bankTransfer`, `none` - `redirect` (AuthStateRedirect, optional) — Present when `authMethodType` is `redirect` — send the payer to `url`. ### AuthStateRedirect Present when `authMethodType` is `redirect` — send the payer to `url`. - `url` (string, optional) — URL to redirect the payer to for authentication. - `requiresTopLevel` (boolean, optional) — When true, the browser must navigate top-level to `url` rather than embedding it in an iframe. ## Examples **Request** ```json { "paymentMethodId": "card", "sessionId": "0d3f1d6c-1c49-4b89-9db6-1fdc06f24c8a", "paymentDetails": { "type": "card", "cardExpiryMonth": "12", "cardExpiryYear": "2028", "cardHolderName": "Chinwe Okafor", "encryptedCardCvv": "ev-cvv-token", "encryptedCardNumber": "ev-token", "payerEmail": "chinwe.okafor@example.com" } } ``` **Response** ```json { "transactionId": "9f3e4b2c-1a6d-4e88-9d3a-ff1234567890", "status": "AUTH_REQUIRED", "message": "Authorization required", "authState": { "state": "AUTH_REQUIRED", "transitionedAt": "2026-06-02T10:30:14Z", "authMethodType": "redirect", "redirect": { "url": "https://example.com/redirect" } } } ``` **SDK Code** ```python import requests url = "https://api.crisscross.money/v1/payment" payload = { "paymentMethodId": "card", "sessionId": "0d3f1d6c-1c49-4b89-9db6-1fdc06f24c8a", "paymentDetails": { "type": "card", "cardExpiryMonth": "12", "cardExpiryYear": "2028", "cardHolderName": "Chinwe Okafor", "encryptedCardCvv": "ev-cvv-token", "encryptedCardNumber": "ev-token", "payerEmail": "chinwe.okafor@example.com" } } headers = {"Content-Type": "application/json"} response = requests.post(url, json=payload, headers=headers) print(response.json()) ``` ```javascript const url = 'https://api.crisscross.money/v1/payment'; const options = { method: 'POST', headers: {'Content-Type': 'application/json'}, body: '{"paymentMethodId":"card","sessionId":"0d3f1d6c-1c49-4b89-9db6-1fdc06f24c8a","paymentDetails":{"type":"card","cardExpiryMonth":"12","cardExpiryYear":"2028","cardHolderName":"Chinwe Okafor","encryptedCardCvv":"ev-cvv-token","encryptedCardNumber":"ev-token","payerEmail":"chinwe.okafor@example.com"}}' }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ```go package main import ( "fmt" "strings" "net/http" "io" ) func main() { url := "https://api.crisscross.money/v1/payment" payload := strings.NewReader("{\n \"paymentMethodId\": \"card\",\n \"sessionId\": \"0d3f1d6c-1c49-4b89-9db6-1fdc06f24c8a\",\n \"paymentDetails\": {\n \"type\": \"card\",\n \"cardExpiryMonth\": \"12\",\n \"cardExpiryYear\": \"2028\",\n \"cardHolderName\": \"Chinwe Okafor\",\n \"encryptedCardCvv\": \"ev-cvv-token\",\n \"encryptedCardNumber\": \"ev-token\",\n \"payerEmail\": \"chinwe.okafor@example.com\"\n }\n}") req, _ := http.NewRequest("POST", url, payload) req.Header.Add("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(res) fmt.Println(string(body)) } ``` ```ruby require 'uri' require 'net/http' url = URI("https://api.crisscross.money/v1/payment") http = Net::HTTP.new(url.host, url.port) http.use_ssl = true request = Net::HTTP::Post.new(url) request["Content-Type"] = 'application/json' request.body = "{\n \"paymentMethodId\": \"card\",\n \"sessionId\": \"0d3f1d6c-1c49-4b89-9db6-1fdc06f24c8a\",\n \"paymentDetails\": {\n \"type\": \"card\",\n \"cardExpiryMonth\": \"12\",\n \"cardExpiryYear\": \"2028\",\n \"cardHolderName\": \"Chinwe Okafor\",\n \"encryptedCardCvv\": \"ev-cvv-token\",\n \"encryptedCardNumber\": \"ev-token\",\n \"payerEmail\": \"chinwe.okafor@example.com\"\n }\n}" response = http.request(request) puts response.read_body ``` ```java import com.mashape.unirest.http.HttpResponse; import com.mashape.unirest.http.Unirest; HttpResponse response = Unirest.post("https://api.crisscross.money/v1/payment") .header("Content-Type", "application/json") .body("{\n \"paymentMethodId\": \"card\",\n \"sessionId\": \"0d3f1d6c-1c49-4b89-9db6-1fdc06f24c8a\",\n \"paymentDetails\": {\n \"type\": \"card\",\n \"cardExpiryMonth\": \"12\",\n \"cardExpiryYear\": \"2028\",\n \"cardHolderName\": \"Chinwe Okafor\",\n \"encryptedCardCvv\": \"ev-cvv-token\",\n \"encryptedCardNumber\": \"ev-token\",\n \"payerEmail\": \"chinwe.okafor@example.com\"\n }\n}") .asString(); ``` ```php request('POST', 'https://api.crisscross.money/v1/payment', [ 'body' => '{ "paymentMethodId": "card", "sessionId": "0d3f1d6c-1c49-4b89-9db6-1fdc06f24c8a", "paymentDetails": { "type": "card", "cardExpiryMonth": "12", "cardExpiryYear": "2028", "cardHolderName": "Chinwe Okafor", "encryptedCardCvv": "ev-cvv-token", "encryptedCardNumber": "ev-token", "payerEmail": "chinwe.okafor@example.com" } }', 'headers' => [ 'Content-Type' => 'application/json', ], ]); echo $response->getBody(); ``` ```csharp using RestSharp; var client = new RestClient("https://api.crisscross.money/v1/payment"); var request = new RestRequest(Method.POST); request.AddHeader("Content-Type", "application/json"); request.AddParameter("application/json", "{\n \"paymentMethodId\": \"card\",\n \"sessionId\": \"0d3f1d6c-1c49-4b89-9db6-1fdc06f24c8a\",\n \"paymentDetails\": {\n \"type\": \"card\",\n \"cardExpiryMonth\": \"12\",\n \"cardExpiryYear\": \"2028\",\n \"cardHolderName\": \"Chinwe Okafor\",\n \"encryptedCardCvv\": \"ev-cvv-token\",\n \"encryptedCardNumber\": \"ev-token\",\n \"payerEmail\": \"chinwe.okafor@example.com\"\n }\n}", ParameterType.RequestBody); IRestResponse response = client.Execute(request); ``` ```swift import Foundation let headers = ["Content-Type": "application/json"] let parameters = [ "paymentMethodId": "card", "sessionId": "0d3f1d6c-1c49-4b89-9db6-1fdc06f24c8a", "paymentDetails": [ "type": "card", "cardExpiryMonth": "12", "cardExpiryYear": "2028", "cardHolderName": "Chinwe Okafor", "encryptedCardCvv": "ev-cvv-token", "encryptedCardNumber": "ev-token", "payerEmail": "chinwe.okafor@example.com" ] ] as [String : Any] let postData = JSONSerialization.data(withJSONObject: parameters, options: []) let request = NSMutableURLRequest(url: NSURL(string: "https://api.crisscross.money/v1/payment")! as URL, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10.0) request.httpMethod = "POST" request.allHTTPHeaderFields = headers request.httpBody = postData as Data let session = URLSession.shared let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in if (error != nil) { print(error as Any) } else { let httpResponse = response as? HTTPURLResponse print(httpResponse) } }) dataTask.resume() ```