Create a session
The two calls your server makes — authenticate, then create the payment session — and the token it hands to your page.
Everything on this page happens on your server, before your checkout page renders. Two calls, and you are done: your credentials never reach the browser, and neither does the access token.
1. Authenticate
Section titled “1. Authenticate”Exchange your credentials for an access token. It lasts a while — cache it and reuse it until
expires_in runs out rather than calling this per order.
# jq (https://jqlang.github.io/jq) reads the answers; both steps share $ACCESS_TOKEN.
GRANT=$(curl -s -X POST "$ORCHESTRATOR_URL/oauth/token" \ -H "content-type: application/x-www-form-urlencoded" \ --data-urlencode "grant_type=client_credentials" \ --data-urlencode "client_id=$MERCHANT_ID" \ --data-urlencode "client_secret=$MERCHANT_SECRET")
ACCESS_TOKEN=$(echo "$GRANT" | jq -r .access_token)EXPIRES_IN=$(echo "$GRANT" | jq -r .expires_in)const grant = await fetch(`${ORCHESTRATOR_URL}/oauth/token`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "client_credentials", client_id: MERCHANT_ID, client_secret: MERCHANT_SECRET, }),});const { access_token, expires_in } = await grant.json();type Grant = { access_token: string; expires_in: number };
const grant = await fetch(`${ORCHESTRATOR_URL}/oauth/token`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "client_credentials", client_id: MERCHANT_ID, client_secret: MERCHANT_SECRET, }),});
const { access_token, expires_in }: Grant = await grant.json();import com.fasterxml.jackson.databind.JsonNode;import com.fasterxml.jackson.databind.ObjectMapper;import java.net.URI;import java.net.URLEncoder;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;import java.util.List;import java.util.Map;import static java.nio.charset.StandardCharsets.UTF_8;
var mapper = new ObjectMapper();var client = HttpClient.newHttpClient();
var form = "grant_type=client_credentials" + "&client_id=" + URLEncoder.encode(MERCHANT_ID, UTF_8) + "&client_secret=" + URLEncoder.encode(MERCHANT_SECRET, UTF_8);
var grant = client.send( HttpRequest.newBuilder(URI.create(ORCHESTRATOR_URL + "/oauth/token")) .header("content-type", "application/x-www-form-urlencoded") .POST(HttpRequest.BodyPublishers.ofString(form)) .build(), HttpResponse.BodyHandlers.ofString());
JsonNode token = mapper.readTree(grant.body());String accessToken = token.get("access_token").asText();int expiresIn = token.get("expires_in").asInt();using System.Net.Http.Headers;using System.Net.Http.Json;using System.Text.Json;
var http = new HttpClient();
var grant = await http.PostAsync( $"{OrchestratorUrl}/oauth/token", new FormUrlEncodedContent(new Dictionary<string, string> { ["grant_type"] = "client_credentials", ["client_id"] = MerchantId, ["client_secret"] = MerchantSecret, }));
var token = await grant.Content.ReadFromJsonAsync<JsonElement>();var accessToken = token.GetProperty("access_token").GetString();var expiresIn = token.GetProperty("expires_in").GetInt32();$grant = curl_init("$orchestratorUrl/oauth/token");curl_setopt_array($grant, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query([ 'grant_type' => 'client_credentials', 'client_id' => $merchantId, 'client_secret' => $merchantSecret, ]),]);
$token = json_decode(curl_exec($grant), true);$accessToken = $token['access_token'];$expiresIn = $token['expires_in'];import requests # pip install requests
grant = requests.post( f"{ORCHESTRATOR_URL}/oauth/token", data={ "grant_type": "client_credentials", "client_id": MERCHANT_ID, "client_secret": MERCHANT_SECRET, },)
token = grant.json()access_token = token["access_token"]expires_in = token["expires_in"]require "json"require "net/http"
grant = Net::HTTP.post_form( URI("#{ORCHESTRATOR_URL}/oauth/token"), "grant_type" => "client_credentials", "client_id" => MERCHANT_ID, "client_secret" => MERCHANT_SECRET)
token = JSON.parse(grant.body)access_token = token["access_token"]expires_in = token["expires_in"]import ( "bytes" "encoding/json" "net/http" "net/url")
grant, err := http.PostForm(orchestratorURL+"/oauth/token", url.Values{ "grant_type": {"client_credentials"}, "client_id": {merchantID}, "client_secret": {merchantSecret},})if err != nil { return err}defer grant.Body.Close()
var token struct { AccessToken string `json:"access_token"` ExpiresIn int `json:"expires_in"`}if err := json.NewDecoder(grant.Body).Decode(&token); err != nil { return err}use reqwest::Client;use serde_json::{json, Value};
// Cargo.toml — reqwest = { version = "0.12", features = ["json"] }// serde_json = "1"// tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
let http = Client::new();
let token: Value = http .post(format!("{orchestrator_url}/oauth/token")) .form(&[ ("grant_type", "client_credentials"), ("client_id", merchant_id), ("client_secret", merchant_secret), ]) .send() .await? .json() .await?;
let access_token = token["access_token"].as_str().unwrap();let expires_in = token["expires_in"].as_u64().unwrap();2. Create the session
Section titled “2. Create the session”Now declare the order. The session is the whole order — its total, its currency, and the methods you accept for it:
ELEMENTS_TOKEN=$(curl -s -X POST "$ORCHESTRATOR_URL/api/sessions" \ -H "content-type: application/json" \ -H "authorization: Bearer $ACCESS_TOKEN" \ -d '{ "currency": "EUR", "total": 100000, "orderId": "EP-123456", "methods": [{ "method": "card", "partialAuth": true }], "statementDescriptor": "MY SHOP", "returnUrl": "https://shop.example.com/checkout", "cancelUrl": "https://shop.example.com/cart" }' | jq -r .elementsToken)const response = await fetch(`${ORCHESTRATOR_URL}/api/sessions`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${access_token}` }, body: JSON.stringify({ currency: "EUR", total: 1000_00, // in the currency's minor unit — cents here orderId: "EP-123456", // YOUR reference for this order methods: [{ method: "card", partialAuth: true }], statementDescriptor: "MY SHOP", // what your customer reads on their bank statement returnUrl: "https://shop.example.com/checkout", // where redirected methods come back cancelUrl: "https://shop.example.com/cart", // optional: where a cancellation lands }),});const { elementsToken } = await response.json();// the fields this example uses — the whole body is in the HTTP API referencetype CreateSession = { currency: string; // ISO 4217 total: number; // in the currency's minor unit — cents here orderId: string; methods: { method: "card"; partialAuth?: boolean }[]; statementDescriptor?: string; returnUrl?: string; cancelUrl?: string;};
const body: CreateSession = { currency: "EUR", total: 1000_00, orderId: "EP-123456", // YOUR reference for this order methods: [{ method: "card", partialAuth: true }], statementDescriptor: "MY SHOP", // what your customer reads on their bank statement returnUrl: "https://shop.example.com/checkout", // where redirected methods come back cancelUrl: "https://shop.example.com/cart", // optional: where a cancellation lands};
const response = await fetch(`${ORCHESTRATOR_URL}/api/sessions`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${access_token}` }, body: JSON.stringify(body),});
const { elementsToken }: { elementsToken: string } = await response.json();var body = mapper.writeValueAsString(Map.of( "currency", "EUR", "total", 1000_00, // in the currency's minor unit — cents here "orderId", "EP-123456", // YOUR reference for this order "methods", List.of(Map.of("method", "card", "partialAuth", true)), "statementDescriptor", "MY SHOP", // what your customer reads on their bank statement "returnUrl", "https://shop.example.com/checkout", // where redirected methods come back "cancelUrl", "https://shop.example.com/cart")); // optional: where a cancellation lands
var response = client.send( HttpRequest.newBuilder(URI.create(ORCHESTRATOR_URL + "/api/sessions")) .header("content-type", "application/json") .header("authorization", "Bearer " + accessToken) .POST(HttpRequest.BodyPublishers.ofString(body)) .build(), HttpResponse.BodyHandlers.ofString());
String elementsToken = mapper.readTree(response.body()).get("elementsToken").asText();http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var response = await http.PostAsJsonAsync($"{OrchestratorUrl}/api/sessions", new{ currency = "EUR", total = 1000_00, // in the currency's minor unit — cents here orderId = "EP-123456", // YOUR reference for this order methods = new[] { new { method = "card", partialAuth = true } }, statementDescriptor = "MY SHOP", // what your customer reads on their bank statement returnUrl = "https://shop.example.com/checkout", // where redirected methods come back cancelUrl = "https://shop.example.com/cart", // optional: where a cancellation lands});
var session = await response.Content.ReadFromJsonAsync<JsonElement>();var elementsToken = session.GetProperty("elementsToken").GetString();$session = curl_init("$orchestratorUrl/api/sessions");curl_setopt_array($session, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'content-type: application/json', "authorization: Bearer $accessToken", ], CURLOPT_POSTFIELDS => json_encode([ 'currency' => 'EUR', 'total' => 1000_00, // in the currency's minor unit — cents here 'orderId' => 'EP-123456', // YOUR reference for this order 'methods' => [['method' => 'card', 'partialAuth' => true]], 'statementDescriptor' => 'MY SHOP', // what your customer reads on their bank statement 'returnUrl' => 'https://shop.example.com/checkout', // where redirected methods come back 'cancelUrl' => 'https://shop.example.com/cart', // optional: where a cancellation lands ]),]);
$elementsToken = json_decode(curl_exec($session), true)['elementsToken'];response = requests.post( f"{ORCHESTRATOR_URL}/api/sessions", headers={"authorization": f"Bearer {access_token}"}, json={ "currency": "EUR", "total": 1000_00, # in the currency's minor unit — cents here "orderId": "EP-123456", # YOUR reference for this order "methods": [{"method": "card", "partialAuth": True}], "statementDescriptor": "MY SHOP", # what your customer reads on their bank statement "returnUrl": "https://shop.example.com/checkout", # where redirected methods come back "cancelUrl": "https://shop.example.com/cart", # optional: where a cancellation lands },)
elements_token = response.json()["elementsToken"]body = { currency: "EUR", total: 1000_00, # in the currency's minor unit — cents here orderId: "EP-123456", # YOUR reference for this order methods: [{ method: "card", partialAuth: true }], statementDescriptor: "MY SHOP", # what your customer reads on their bank statement returnUrl: "https://shop.example.com/checkout", # where redirected methods come back cancelUrl: "https://shop.example.com/cart" # optional: where a cancellation lands}
response = Net::HTTP.post( URI("#{ORCHESTRATOR_URL}/api/sessions"), body.to_json, "content-type" => "application/json", "authorization" => "Bearer #{access_token}")
elements_token = JSON.parse(response.body)["elementsToken"]body, err := json.Marshal(map[string]any{ "currency": "EUR", "total": 1000_00, // in the currency's minor unit — cents here "orderId": "EP-123456", // YOUR reference for this order "methods": []any{map[string]any{"method": "card", "partialAuth": true}}, "statementDescriptor": "MY SHOP", // what your customer reads on their bank statement "returnUrl": "https://shop.example.com/checkout", // where redirected methods come back "cancelUrl": "https://shop.example.com/cart", // optional: where a cancellation lands})if err != nil { return err}
request, err := http.NewRequest("POST", orchestratorURL+"/api/sessions", bytes.NewReader(body))if err != nil { return err}request.Header.Set("content-type", "application/json")request.Header.Set("authorization", "Bearer "+token.AccessToken)
response, err := http.DefaultClient.Do(request)if err != nil { return err}defer response.Body.Close()
var session struct { ElementsToken string `json:"elementsToken"`}if err := json.NewDecoder(response.Body).Decode(&session); err != nil { return err}let session: Value = http .post(format!("{orchestrator_url}/api/sessions")) .bearer_auth(access_token) .json(&json!({ "currency": "EUR", "total": 1000_00, // in the currency's minor unit — cents here "orderId": "EP-123456", // YOUR reference for this order "methods": [{ "method": "card", "partialAuth": true }], "statementDescriptor": "MY SHOP", // what your customer reads on their bank statement "returnUrl": "https://shop.example.com/checkout", // where redirected methods come back "cancelUrl": "https://shop.example.com/cart", // optional: where a cancellation lands })) .send() .await? .json() .await?;
let elements_token = session["elementsToken"].as_str().unwrap();A few things worth knowing about that body:
orderIdis yours and it is required. Every PSP operation carries it, the confirmation screen shows it, and it is what you — or our support — will search for when reconciling a payment. A split payment sends several operations under that one reference.totalis in minor units (cents for euros), like every amount in this API.methodsdeclares what you accept for this order, and how. Configuring each one — card schemes, Apple Pay, PayPal, Wero, ANCV, Oney and their plans — is covered in payment methods. You only name aproviderwhen several of yours could serve the same method.- The merchant id comes from the token, never from the body: a session always belongs to whoever authenticated.
3. Hand the token to your page
Section titled “3. Hand the token to your page”The answer carries an opaque, short-lived elementsToken. That is the only thing the
browser ever needs — and the only thing it should ever get.
Your page takes it from there : see show the elements.
4. Then, the notification
Section titled “4. Then, the notification”When the session ends — paid, abandoned, expired — we call an address you declared, and that call is how your own system learns about it. It is the only way to hear the ending: your customer may close the tab on the confirmation screen, and the order is paid all the same.
Declare the address once on your account, and every session of yours is announced there. The payload, the retries and the answer we expect are on the webhooks page.
Sandbox and production
Section titled “Sandbox and production”Same calls, same bodies, same answers — only the credentials and the address change. Against
a local docker compose up, the demo merchant is demo / demo-secret and the orchestrator
answers on http://localhost:3000, with a simulated PSP that lets you play the verdicts
yourself. What you build there ships unchanged.