Migrating from S2S to the Orchestration API
Field-by-field guide for merchants moving from the form-urlencoded S2S API to the JSON Orchestration API.
This guide is for merchants currently integrating directly with the S2S (Server-to-Server) API — the form-urlencoded endpoint at eu-test.oppwa.com / sandbox-card.peachpayments.com — and moving to the Orchestration API.
Endpoint mapping
| S2S | Orchestration API | Notes |
|---|---|---|
POST /v1/payments (paymentType=DB/PA) | POST /payments | Authorise + capture (DB) or auth-only (PA — set capture_method: "manual") |
POST /v1/payments/{id} (paymentType=CP) | POST /payments/{id}/capture | Capture a previously authorised PA |
POST /v1/payments/{id} (paymentType=RV) | POST /payments/{id}/cancel | Void (only works pre-capture) |
POST /v1/payments/{id} (paymentType=RF) | POST /refunds | Refund a captured payment |
GET /v1/payments/{id} | GET /payments/{id}?force_sync=true | Sync / retrieve |
POST /v1/threeDSecure | not user-facing | Orchestration handles standalone 3DS internally when authentication_type: "three_ds" |
GET /v1/threeDSecure/{id} | not user-facing | Same — handled by Orchestration |
Authentication mapping
| S2S | Orchestration API |
|---|---|
Authorization: Bearer <token> | api-key: <merchant_api_key> (per-merchant, not per-entity) |
entityId=<entity-id> (form param) | Configured on the merchant connector account (MCA): connector_account_details.key1 |
| Bearer token | Configured on the MCA: connector_account_details.api_key, auth_type: "BodyKey" |
You provision one MCA per S2S entity. If you have multiple entities (for example, one per acquirer), create one MCA per entity and route between them with Orchestration's routing rules.
Field-by-field request mapping
Basic transaction fields
| S2S | Orchestration API | Notes |
|---|---|---|
amount=92.00 (major units, two decimals) | "amount": 9200 (minor units, integer) | Orchestration converts to S2S's major-unit string automatically |
currency=ZAR | "currency": "ZAR" | |
paymentBrand=VISA | derived from card BIN, or "payment_method_type": "credit"/"debit" | Orchestration picks the brand from the card number; payment_method_type controls credit vs debit routing |
paymentType=DB | "capture_method": "automatic" (default) | DB = authorise + capture |
paymentType=PA | "capture_method": "manual" | Authorise only; capture later |
merchantTransactionId=ord_123 | "connector_request_reference_id": "ord_123" (or payment_id) | Orchestration truncates to S2S's 16-char limit |
descriptor=Coffee | "statement_descriptor_name": "Coffee" | |
testMode=EXTERNAL | MCA connector_meta_data: { "test_mode": "EXTERNAL" } | Opt-in. Default (absent) → S2S's INTERNAL default. |
Card details
| S2S | Orchestration API |
|---|---|
card.number=4111111111111111 | "payment_method_data.card.card_number": "4111111111111111" |
card.holder=Jane Jones | "payment_method_data.card.card_holder_name": "Jane Jones" |
card.expiryMonth=05 | "payment_method_data.card.card_exp_month": "05" |
card.expiryYear=2034 | "payment_method_data.card.card_exp_year": "34" (or "2034") |
card.cvv=123 | "payment_method_data.card.card_cvc": "123" |
payment_method is "card", payment_method_type is "credit" or "debit".
Wallets
| S2S | Orchestration API |
|---|---|
applePay.paymentToken=... | "payment_method_data.wallet.apple_pay_redirect": {} (browser SDK) or predecrypt token via Apple Pay decryption flow |
googlePay.paymentToken=... | "payment_method_data.wallet.google_pay_redirect": {} |
samsungPay.paymentToken=... | "payment_method_data.wallet.samsung_pay": { "token": "..." } |
paymentBrand=APPLEPAY | "payment_method_type": "apple_pay" |
paymentBrand=GOOGLEPAY | "payment_method_type": "google_pay" |
paymentBrand=SAMSUNGPAY | "payment_method_type": "samsung_pay" |
Customer
| S2S | Orchestration API |
|---|---|
customer.email=jane@x.com | "email": "jane@x.com" |
customer.givenName=Jane | "billing.address.first_name": "Jane" |
customer.surname=Jones | "billing.address.last_name": "Jones" |
customer.merchantCustomerId=cust_42 | "customer_id": "cust_42" |
customer.ip=1.2.3.4 | Auto-set by Orchestration from the request IP. Override with "browser_info.ip_address": "1.2.3.4" if needed |
customer.phone=+27115551234 | "phone": "+27115551234" |
Browser info (3DS 2.x)
| S2S | Orchestration API (browser_info) |
|---|---|
customer.browser.acceptHeader | accept_header |
customer.browser.language | language |
customer.browser.screenHeight | screen_height |
customer.browser.screenWidth | screen_width |
customer.browser.timezone | time_zone |
customer.browser.userAgent | user_agent |
customer.browser.javascriptEnabled | java_script_enabled |
customer.browser.javaEnabled | java_enabled |
customer.browser.screenColorDepth | color_depth / screen_color_depth |
customer.browser.challengeWindow | (Orchestration sets internally; S2S receives 03 for full-screen) |
Billing / shipping address
| S2S | Orchestration API |
|---|---|
billing.street1 | billing.address.line1 |
billing.street2 | billing.address.line2 |
billing.city | billing.address.city |
billing.state | billing.address.state |
billing.postcode | billing.address.zip |
billing.country=US | billing.address.country: "US" |
shipping.* | shipping.address.* (same field names as billing) |
Use-case examples
S2S's standingInstruction.*, createRegistration, and threeDSecure.* fields are derived by Orchestration from the shape of your request — you don't set them directly. Pick a use case below to see the before/after.
Save a card during a customer-present payment so it can be charged later.
Orchestration sends createRegistration=true and the correct standingInstruction.* fields automatically.
Response (excerpt)
Store mandate_id for subsequent MITs. connector_mandate_id is S2S's registrationId, and network_transaction_id is the CITI / traceId for Nedbank acquirers — both are stored by Orchestration automatically.
Custom parameters / metadata
S2S accepts arbitrary customParameters[key]=value form fields.
| S2S | Orchestration API | What Orchestration forwards |
|---|---|---|
customParameters[paymentId]=pay_xxx | (set automatically by Orchestration) | |
customParameters[your_key]=your_value | "metadata": { "your_key": "your_value" } (top-level) | not yet auto-forwarded — see below |
Response field mapping
| S2S response field | Orchestration API response field |
|---|---|
id | connector_transaction_id |
result.code | classified into status (succeeded / processing / failed) + error_code |
result.description | error_message |
result.cvvResponse | payment_method_data.card.payment_checks (CVV part of the JSON) |
registrationId | connector_mandate_id |
paymentBrand | payment_method_data.card.card_network |
card.binCountry | included in connector response data |
resultDetails.AuthCode (or ApprovalCode fallback) | payment_method_data.card.auth_code |
resultDetails.AcquirerResponse | payment_method_data.card.payment_checks (acquirer part) |
resultDetails.MerchantAdviceCode | exposed in connector response data |
resultDetails.ConnectorTxID2 (Nedbank) → RRN at position 2 | connector_response_reference_id |
resultDetails.ConnectorTxID3 (Nedbank) → CITI at position 4 | network_transaction_id |
STAN / originalTransactionId parsed from ConnectorTxIDs | payment_method_data.card.payment_checks |
risk.score | included in connector response data |
threeDSecure.eci / verificationId / dsTransactionId / acsTransactionId / version / flow | authentication_data |
standingInstruction.agreementId | persisted in mandate_metadata for replay on next MIT |
What you stop doing
You no longer need to:
- track
registrationId,agreementId, orinitialTransactionIdyourself — Orchestration persists them in the mandate - compute and send
standingInstruction.mode/source/type/initiator/transactionType— Orchestration derives them from the flow - maintain entity-specific routing logic — set up an MCA per entity and let Orchestration routing rules decide
- handle 3DS redirects manually for the standard flow — Orchestration exposes a single
next_action.redirect_to_urland a single redirect-back URL - parse
result.codeto classify success/pending/failure — Orchestration maps codes tostatusanderror_code/error_message
You still need to:
- decide
capture_method(auto vs manual) per transaction - decide
mit_categoryfor subsequent MITs - pass
setup_future_usage: "off_session"+customer_acceptanceon the initial CIT to mint a mandate - pass external-3DS data via
three_ds_dataif you do your own 3DS - store
payment_idandmandate_idreturned by Orchestration
Minimum testing checklist
- No-3DS card payment, auto-capture — issues a successful payment and gets a
connector_transaction_id. - Manual capture —
capture_method: "manual"→requires_capture→POST /payments/{id}/capture. - Refund —
POST /refundsagainst a succeeded payment. - Initial CIT (off-session) —
setup_future_usage: "off_session"+customer_acceptancereturns amandate_id+connector_mandate_id+network_transaction_id. - Subsequent MIT —
recurring_details.mandate_idreturns succeeded. - 3DS challenge —
authentication_type: "three_ds"returnsrequires_customer_actionwithnext_action.redirect_to_url; complete the redirect; payment becomessucceeded. - External 3DS — pre-authenticated payment with
three_ds_datasucceeds without further user action. - MIT after CIT — verify the second MIT against a CIT succeeds (proves
agreementId, traceIdreplay).