Direct payments
A direct payment creates a one-time payment in a single, merchant-initiated call. Unlike a
hosted checkout session, you supply the amount, currency, country, and
payment_method yourself rather than handing the whole flow to Orchestr.
The payment is completed on a provider-hosted page, so payment details never touch your
servers. (Paying by card without a redirect - collecting the card on your own page - is its own
flow: see Card payments.) A successful POST /v1/payments returns one of:
status: requires_action- the customer has a step to finish;next_actionsays which.status: completed- the payment settled immediately.status: failed- the payment was declined or errored.
When to use a direct payment
| Use a direct payment when… | Use a checkout session when… |
|---|---|
| You already know the amount, currency, country, and payment method and want a single server-to-server call. | You want Orchestr to host the entire payment page and collect customer/payment details for you. |
| You manage your own return-URL redirect. | You want line items, address/phone collection, and locale handling out of the box. |
| You want the provider's own page for the method. | You chose the method on your page but still want Orchestr's hosted form for it - pass it as the single payment_methods entry (see Pre-selecting the payment method). For a redirect method you can also keep the session and skip the form with straight_through: true (see Going straight to the provider). |
Both flows share the same providers and settlement - the resulting order surfaces with
subtype: direct_payment on the Orders and Transactions
endpoints.
1. Create a payment
This POST /v1/payments call reuses the buildSignatureHeader helper from the
Signing guide (the signed string for a request is
timestamp + path + body). It assumes you've exported UPPROVE_SECRET_KEY and
UPPROVE_SIGNING_SECRET as shown in Getting started.
- cURL
- Node.js
- Python
- PHP
- Java
- C#
REQ_PATH="/v1/payments"
BODY='{
"amount": "42.00",
"currency": "GBP",
"country": "GB",
"payment_method": {
"code": "paypal"
},
"merchant_reference": "order-1001",
"success_url": "https://shop.example.com/ok",
"cancel_url": "https://shop.example.com/cancel",
"customer": {
"email": "jane@example.com"
}
}'
TS=$(date +%s)
SIG=$(printf '%s' "${TS}${REQ_PATH}${BODY}" \
| openssl dgst -sha256 -hmac "$UPPROVE_SIGNING_SECRET" | sed 's/^.*= //')
curl -sS -X POST "https://api.sandbox.upprove.com${REQ_PATH}" \
-H "Authorization: Bearer $UPPROVE_SECRET_KEY" \
-H "X-Signature: t=${TS}, v1=${SIG}" \
-H "Content-Type: application/json" \
--data-raw "$BODY"
const path = '/v1/payments';
const body = JSON.stringify({
amount: '42.00',
currency: 'GBP',
country: 'GB',
payment_method: {code: 'paypal'},
merchant_reference: 'order-1001',
success_url: 'https://shop.example.com/ok',
cancel_url: 'https://shop.example.com/cancel',
customer: {email: 'jane@example.com'},
});
const res = await fetch(`https://api.sandbox.upprove.com${path}`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.UPPROVE_SECRET_KEY}`,
'X-Signature': buildSignatureHeader(process.env.UPPROVE_SIGNING_SECRET, path, body),
'Content-Type': 'application/json',
},
body,
});
const payment = await res.json();
if (payment.status === 'requires_action') {
console.log('redirect to', payment.next_action.redirect_url);
}
import json
import requests
path = "/v1/payments"
body = json.dumps(
{
"amount": "42.00",
"currency": "GBP",
"country": "GB",
"payment_method": {"code": "paypal"},
"merchant_reference": "order-1001",
"success_url": "https://shop.example.com/ok",
"cancel_url": "https://shop.example.com/cancel",
"customer": {"email": "jane@example.com"},
},
separators=(",", ":"),
)
resp = requests.post(
f"https://api.sandbox.upprove.com{path}",
data=body,
headers={
"Authorization": f"Bearer {UPPROVE_SECRET_KEY}",
"X-Signature": build_signature_header(UPPROVE_SIGNING_SECRET, path, body),
"Content-Type": "application/json",
},
)
payment = resp.json()
if payment["status"] == "requires_action":
print("redirect to", payment["next_action"]["redirect_url"])
<?php
$path = '/v1/payments';
$body = json_encode([
'amount' => '42.00',
'currency' => 'GBP',
'country' => 'GB',
'payment_method' => ['code' => 'paypal'],
'merchant_reference' => 'order-1001',
'success_url' => 'https://shop.example.com/ok',
'cancel_url' => 'https://shop.example.com/cancel',
'customer' => ['email' => 'jane@example.com'],
], JSON_UNESCAPED_SLASHES);
$ch = curl_init("https://api.sandbox.upprove.com{$path}");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('UPPROVE_SECRET_KEY'),
'X-Signature: ' . build_signature_header(getenv('UPPROVE_SIGNING_SECRET'), $path, $body),
'Content-Type: application/json',
],
]);
$payment = json_decode(curl_exec($ch), true);
echo $payment['status'], ' ', $payment['next_action']['redirect_url'] ?? '';
String path = "/v1/payments";
String body = """
{
"amount": "42.00",
"currency": "GBP",
"country": "GB",
"payment_method": {
"code": "paypal"
},
"merchant_reference": "order-1001",
"success_url": "https://shop.example.com/ok",
"cancel_url": "https://shop.example.com/cancel",
"customer": {
"email": "jane@example.com"
}
}""";
HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.sandbox.upprove.com" + path))
.header("Authorization", "Bearer " + System.getenv("UPPROVE_SECRET_KEY"))
.header("X-Signature", buildSignatureHeader(System.getenv("UPPROVE_SIGNING_SECRET"), path, body))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
var path = "/v1/payments";
var body = """
{
"amount": "42.00",
"currency": "GBP",
"country": "GB",
"payment_method": {
"code": "paypal"
},
"merchant_reference": "order-1001",
"success_url": "https://shop.example.com/ok",
"cancel_url": "https://shop.example.com/cancel",
"customer": {
"email": "jane@example.com"
}
}
""";
using var http = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, $"https://api.sandbox.upprove.com{path}")
{
Content = new StringContent(body, Encoding.UTF8, "application/json"),
};
request.Headers.Add("Authorization", $"Bearer {Environment.GetEnvironmentVariable("UPPROVE_SECRET_KEY")}");
request.Headers.Add("X-Signature",
BuildSignatureHeader(Environment.GetEnvironmentVariable("UPPROVE_SIGNING_SECRET"), path, body));
var response = await http.SendAsync(request);
Console.WriteLine(await response.Content.ReadAsStringAsync());
The required fields are amount, currency, country, payment_method, merchant_reference,
success_url, and cancel_url. merchant_reference must be unique per account and doubles as your
idempotency key - retrying with the same reference will
not create a duplicate. See Payment methods for every accepted
payment_method.code. Which of them are live for a given country and currency, and which
customer / payment_method.details fields each one needs, you can ask the API before you build
the request - GET /v1/payment-methods (see
Discovering methods and required fields).
If your account runs several brands, sites or serviced clients as
processing profiles, pass the profile's code in
processing_profile to address the payment to it. The code is echoed back on the payment, on its
transactions and on every webhook, and you can filter searches and exports by it. Omit the field to
operate at account scope; an unknown or disabled profile is rejected with a validation error.
:::note LATAM local methods require the customer's identity document and IP
For PIX, BOLETO, SPEI, OXXO and PSE, pass the paying customer's national identity document in
payment_method.details.document_id - the CPF in Brazil (11 digits, punctuation optional), CURP or
RFC in Mexico, cedula in Colombia (6–10 digits) - and the shopper's IP address in customer.ip.
The payment is rejected with a validation error when either is missing, or when the document is
malformed for the method's country (see
Payment methods).
"payment_method": {
"code": "pix",
"details": {
"document_id": "123.456.789-01"
}
},
"customer": {
"email": "jane@example.com",
"name": "Jane Doe",
"ip": "203.0.113.42"
}
customer.ip is the customer's address as your site sees it, never your server's - on a
server-to-server call only you can supply it. It is forwarded to the provider as a risk signal and
is not stored on the customer record.
:::
2. Read the response
A 200 OK returns the payment. The X-Signature response header signs the body - verify it
before trusting the response (see Signing).
Most payments come back requires_action - redirect the customer to next_action.redirect_url:
{
"order_id": "ord_5b4a3c2d",
"transaction_id": "txn_9f8e7d6c",
"status": "requires_action",
"amount": 42.00,
"currency": "GBP",
"next_action": {
"type": "redirect",
"redirect_url": "https://pay.provider.example/session/abc123"
}
}
Local methods return the artifact instead - branch on next_action.type: qr (e.g. Pix, MACH),
voucher (e.g. OXXO, Boleto) or bank_transfer (e.g. SPEI), with expires_at closing the payment
window. A qr action carries whichever of these the provider offers:
{
"status": "requires_action",
"next_action": {
"type": "qr",
"redirect_url": "https://pay.provider.example/qr/abc123",
"expires_at": "2026-10-15T12:30:00Z",
"qr": {
"data": "00020126580014br.gov.bcb.pix...",
"redirect_url": "https://pay.provider.example/qr/abc123",
"deep_links": [
{ "label": "Example Bank", "url": "https://app.examplebank.com/pay?ref=abc123", "target": "app" }
]
}
}
}
Render the QR from data (or show image_url when there is no payload), offer the deep_links
as app (target: app, phones) or online-bank (target: web, desktop) buttons, and keep
redirect_url as the provider-hosted alternative. When redirect_url is the only qr field
set, redirecting is the only way to pay. Field details are on the NextAction schema in the
API Reference.
Whenever the provider hosts a page for a qr or voucher action, its URL is also copied to the
top-level next_action.redirect_url (from qr.redirect_url or voucher.document_url). If you do
not render QRs or vouchers yourself, you can send the customer to next_action.redirect_url for
every action type that carries it.
For a payment that settles immediately, status is completed, next_action is null, and
provider_transaction_id carries the settlement reference:
{
"order_id": "ord_5b4a3c2d",
"transaction_id": "txn_9f8e7d6c",
"status": "completed",
"amount": 42.00,
"currency": "GBP",
"provider_transaction_id": "pi_3PqRsT2eZvKYlo2C1aBcD3eF",
"next_action": null
}
| Field | Description |
|---|---|
order_id | The order created for this payment. |
transaction_id | The payment transaction attempt. |
status | requires_action, completed, or failed. A card charge can also return authorized (a manual-capture hold), settling, or canceled. |
amount / currency | The payment amount and currency. |
provider_transaction_id | Settlement/charge reference; null before settlement. |
additional_identifiers | Extra provider-side identifiers, each {"identifier": …, "type": "checkout" | "order"}. |
next_action | Present when status is requires_action; null on terminal outcomes. |
3. Handle the outcome
The create response tells you the initial state; the final outcome arrives asynchronously via
the payment.* webhooks (payment.succeeded, payment.failed,
payment.requires_action, …). Fulfil the order on payment.succeeded.
Unlike a checkout session, a declined direct payment has no retry surface - the payment does not
reopen for another attempt. To try again, create a new payment with a new merchant_reference.
Inspect the decline code on the failed transaction to decide whether retrying
is worthwhile.
Next steps
- Signing - build and verify the
X-Signatureheader. - Webhooks - the
payment.*events that report the final outcome. - Decline codes - interpret failed payments.
- The full API Reference for
POST /v1/payments.