One API to accept player deposits across cards, European open banking, and LATAM local methods - and to pay players out. Create a payment, send the player to a payment page, then credit their balance from a signed webhook. Built for casino and high-volume cashiers.
Base URL
https://platinum-edge.ca/api/v1
| Approach | Best for | You build |
|---|---|---|
| Hosted Checkout recommended | The fastest path. One page that accepts cards, APMs, open banking and LATAM - PlatinumEdge picks the rail. | One API call, then redirect the player. We host the page; you handle no card or bank data. |
| Direct API | Full control of the payment UI on your own page. | Collect the player's details, call the API per rail, then redirect to the URL we return. |
Both settle through the same pipeline and fire the same webhooks. Whichever you use, treat the webhook as the source of truth for crediting - the browser redirect is only a UX signal. You can mix approaches per transaction.
The fastest way to take a deposit - three steps, copy-paste ready.
pe_live_ secret key in your dashboard and send it as a Bearer token (keep it server-side). Building first? Use a pe_test_ key against test mode.POST /checkout-sessions with the amount. You get back a hosted url.
curl -X POST https://platinum-edge.ca/api/v1/checkout-sessions \
-H "Authorization: Bearer pe_live_..." \
-H "Content-Type: application/json" \
-d '{
"amount": "250.00",
"merchant_reference": "player-90431/dep-2207",
"return_url": "https://cashier.example.com/deposit/return"
}'
{
"ok": true,
"id": "cs_xxxxxxxxxxxxxxxx",
"url": "https://checkout.platinum-edge.ca/c/cs_xxxxxxxxxxxxxxxx",
"expires_in": 3600
}
url. When they pay, we POST payment.approved to your webhook - credit the deposit then. See Hosted Checkout for every field.Authenticate every request with a secret API key from the dashboard (API keys panel) as a Bearer token. Keep it server-side only - never expose it in a browser or mobile app.
Authorization: Bearer pe_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Generate a separate pe_test_ key in the dashboard to integrate end-to-end with no real money. A test key uses the exact same endpoints; the platform simulates the payment instead of calling the bank, so you can build and verify your create → webhook → return_url flow before going live.
Authorization: Bearer pe_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
amount ending in .13 (e.g. 10.13) is declined; any other amount is approved instantly.livemode: false in the response.GET /payments with the test key.pe_live_ key when you are ready - nothing else in your integration changes.The recommended, unified way to accept a deposit. One API call creates a session; you redirect the player to the returned URL. PlatinumEdge hosts the payment page and automatically picks the payment rail - cards, APMs, open banking and LATAM local methods - routing across our acquirers behind the scenes. The player pays on our page, so you handle no card or bank data.
Pass everything up front and the player skips straight to paying, or pass only the amount and let our page collect the rest.
POST /checkout-sessions per payment and redirect the player to the one-time url we return (https://checkout.platinum-edge.ca/c/<session>). Amount and your merchant account are locked to that session — best for programmatic deposits.https://checkout.platinum-edge.ca/pay/<your-slug> that you can share directly or as a QR code with no code. Your exact link (with your slug, and whether it is live) is shown in your dashboard → Developers.| Field | Required | Description |
|---|---|---|
amount | yes | Major units, e.g. "250.00" |
return_url | recommended | https:// page to send the player back to. We append ?ref=&status=. |
merchant_reference | no | Your player/deposit id; echoed on the webhook and status lookup. |
method, bankCountry | no | Pre-select the rail (e.g. fps/GB). Omit to let the player choose on our page. |
email, firstName, lastName, country, address1, city, zipCode, phone | no | Prefill the player's details. Anything omitted is collected on the hosted page. |
expires_in | no | Session lifetime in seconds (300-86400, default 3600). |
curl -X POST https://platinum-edge.ca/api/v1/checkout-sessions \
-H "Authorization: Bearer pe_live_..." \
-H "Content-Type: application/json" \
-d '{
"amount": "250.00",
"merchant_reference": "player-90431/dep-2207",
"return_url": "https://cashier.example.com/deposit/return"
}'
201{
"ok": true,
"id": "cs_xxxxxxxxxxxxxxxx",
"url": "https://checkout.platinum-edge.ca/c/cs_xxxxxxxxxxxxxxxx",
"expires_in": 3600
}
Redirect the player's browser to url. When they finish, we return them to your return_url and - separately - notify your server via webhook. Treat the webhook as the source of truth for crediting; the browser return is only a UX signal. The amount and merchant are locked to the session, so the player cannot change them.
Build the deposit UI on your own page. Collect the player and their bank details, create a payment, then redirect the player to the bank URL we return. This is the request shape for the European open-banking methods below; Cards and LATAM use the same POST /payments endpoint with their own selectors.
| Field | Required | Description |
|---|---|---|
amount | yes | Major units, e.g. "250.00" |
method | yes | sepa | instant | revolut | fps |
bankCountry | yes | NL | MT | GB (FPS is GB only) |
email | yes | Customer email |
firstName, lastName | yes | Customer name |
country | yes | Billing country, ISO-2 (e.g. GB) |
address1, city, zipCode | yes | Billing address |
phone | no | Customer phone |
merchant_reference | no | Your own ID for this deposit (player / cashier reference). Echoed back on the status webhook and on GET /payments/{reference}, so you can reconcile against your cashier without storing our reference. Max 128 chars. |
return_url | no | Where to send the player's browser after the bank flow finishes. Must be https://. We redirect to {return_url}?ref={reference}&status={status}. If omitted, the player lands on our generic result page. |
idempotency_key | no | A unique string per deposit attempt. If you retry the same key, you get the original payment back instead of a duplicate. Also accepted as the Idempotency-Key header. |
solution | no | Name the rail explicitly instead of letting us route. Accepted: "cards" (card processing), "crypto", "paystrax", "dopay" (LATAM), "aeterna". Omit it and we route on your enabled accounts, the amount and the customer's country - which is what most integrations should do. For LATAM, prefer method + country over naming the rail. |
white_label | no | Set true to receive a redirect_url on our checkout domain (https://checkout.platinum-edge.ca/r/{reference}) instead of the acquirer's raw URL, so your customer never sees the provider's brand in the address bar. Default false. |
curl -X POST https://platinum-edge.ca/api/v1/payments \
-H "Authorization: Bearer pe_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: dep_8f3a1c9e" \
-d '{
"amount": "250.00",
"method": "fps",
"bankCountry": "GB",
"email": "customer@example.com",
"firstName": "Alex", "lastName": "Doe",
"country": "GB", "address1": "1 High St", "city": "London", "zipCode": "EC1A 1BB",
"merchant_reference": "player-90431/dep-2207",
"return_url": "https://cashier.example.com/deposit/return"
}'
201{
"ok": true,
"payment": {
"reference": "PE-MC-XXXX-XXXX",
"redirect_url": "https://",
"status": "redirected",
"merchant_reference": "player-90431/dep-2207"
}
}
Redirect the customer's browser to redirect_url to complete the bank payment. When they finish, we return them to your return_url (if supplied) and - separately - notify your server via webhook (below). Treat the webhook as the source of truth for crediting a deposit; the browser return is only a UX signal.
Accept card payments through the same POST /payments endpoint by selecting method: "cards". Cards are processed with full 3-D Secure v2 and settle through the same pipeline and webhooks as every other rail. Card data goes straight from the shopper's browser to the certified processor - it never touches your server, keeping you in PCI-DSS SAQ-A scope.
| Method | Best for | You build |
|---|---|---|
| Hosted card page (simplest) | Getting live fastest - we host a ready-made, 3DS-ready card page. | One API call, then redirect the shopper to the returned redirect_url. |
| Embedded card form (SAQ-A) | Keeping the shopper on your own checkout page. | Fetch a card session, render the processor's iframe fields on your page, then settle server-to-server. |
Both keep you in SAQ-A, both fire the same webhooks. Pick one per integration.
Call POST /payments with method: "cards". Billing fields are required because card processing and 3-D Secure v2 need them - a wrong country fails authentication.
| Field | Required | Description |
|---|---|---|
method | yes | Must be "cards" to select the card rail. |
amount | yes | Major units, up to 2 decimals, as a string ("200.00"). Range 1.00 - 10,000.00. |
currency | no | 3-letter ISO. Defaults to EUR. See Brands & currencies for the accepted set. |
email | yes | Shopper email. |
firstName, lastName | yes | Cardholder name (first / last). |
country | yes | ISO 3166-1 alpha-2 only (SE, not Sweden). Mandatory for 3-D Secure v2 - a wrong code fails authentication and the payment. |
address1, city, zipCode | yes | Billing address, city and postal / ZIP code. |
phone | no | Shopper phone in E.164 (+46701234567). |
merchant_reference, idempotency_key, metadata | no | Same as every rail - your order id (echoed on status & webhook), retry dedupe, and opaque data echoed back on the payment. |
curl -X POST https://platinum-edge.ca/api/v1/payments \
-H "Authorization: Bearer pe_live_..." \
-H "Content-Type: application/json" \
-d '{
"amount": "200.00",
"method": "cards",
"currency": "EUR",
"email": "buyer@example.com",
"firstName": "Eric",
"lastName": "Eriksson",
"country": "SE",
"address1": "Gamla Brogatan 19",
"city": "Stockholm",
"zipCode": "111 20",
"phone": "+46701234567",
"merchant_reference": "your-order-id",
"idempotency_key": "your-order-id"
}'
{
"ok": true,
"payment": {
"reference": "PE-...",
"redirect_url": "https://checkout.platinum-edge.ca/c/...",
"status": "redirected",
"merchant_reference": "your-order-id"
}
}
From here, choose one path: send the shopper to redirect_url (hosted card page), or keep them on your page (embedded card form). Both end at settle.
The simplest integration: after create, redirect the shopper's browser to the returned redirect_url. We host the 3DS-ready card page; when they finish we return them to your return_url (if supplied) and notify your server by webhook. Treat the webhook as the source of truth for fulfilment - the browser return is only a UX signal.
return_url on create (https:// only) to bring the shopper back to your own thank-you / decline page. Without it they land on our generic result page.Render the card form inside your own checkout page. The fields are iframes served by the processor, so card data never touches your server (SAQ-A). Three steps after create:
1. Get the card session GET/payments/{reference}/card-session
{
"ok": true,
"settled": false,
"status": "redirected",
"amount": "200.00",
"currency": "EUR",
"script_url": "https://.../paymentWidgets.js?checkoutId=...",
"integrity": "sha384-...",
"brands": "VISA MASTER"
}
settled: true (no widget fields) means the payment already reached a terminal state - show your result page instead of a form, so a refreshed pay page cannot re-render a dead form.
2. Render the widget - set wpwlOptions before loading the script, then drop in the form; the script replaces it with the card fields.
<script>
var wpwlOptions = { style: "card", locale: "en", brandDetection: true, showCVVHint: true };
</script>
<script src="{script_url}"></script>
<form action="https://yoursite.com/return?o=YOUR_ORDER_TOKEN"
class="paymentWidgets" data-brands="{brands}"></form>
integrity (SRI) attribute to that script tag. The processor serves a per-checkout loader whose hash is not fixed, so an SRI attribute makes the browser silently block it and the card form renders blank with no error. Security comes from TLS plus the processor-hosted iframes. (card-session returns an integrity value only for compatibility - ignore it here.) Put an opaque/signed order token in the form action - not a raw id - so your return route knows which order came back.3. Settle on return - the processor sends the shopper to your action URL. Never trust its query parameters; settle server-to-server instead (below).
Asks the processor for the authoritative outcome, promotes the order and fires your webhook. Idempotent and promote-only: calling it twice, or racing our own reconciliation, can never change a settled payment. Your return route should call this and ignore the redirect query params.
{
"ok": true,
"settled": true,
"changed": true,
"payment": {
"reference": "PE-...",
"status": "approved",
"amount": "200.00",
"currency": "EUR",
"merchant_reference": "your-order-id",
"decline_reason": null
}
}
status: "approved" → fulfil the order, show your thank-you page. Verify merchant_reference matches the order you are about to fulfil.status: "declined" → nothing was charged; show your decline page using decline_reason (below).Buyer-safe decline reasons. On a decline the payment carries a decline_reason category you can turn into helpful copy. Issuer signals that would help someone test stolen cards (insufficient funds, lost / stolen, fraud suspicion) deliberately collapse into the generic declined. The exact processor code stays on status_reason for you and your support team - do not show it to shoppers.
decline_reason | Suggested message |
|---|---|
expired_card | "That card has expired - please use a current card." |
invalid_details | "Please check the card number, expiry and security code." |
verification_failed | "Your bank couldn't verify you - try again or use another card." |
too_many_attempts | "Your bank has paused attempts on this card - wait, or use another." |
declined | "Your bank did not authorise this payment." |
GET /payments/{reference} for current status at any time, and it - like every rail - is confirmed by webhook (payment.approved / payment.declined) for the shoppers who pay and close the tab before returning.The brands returned by card-session are the schemes enabled on your account - only brands your acquirer has boarded will authorise, so the list is explicit rather than "everything". The platform default is VISA MASTER.
| Group | Values |
|---|---|
| Card schemes | VISA, MASTER, MAESTRO, AMEX, JCB, DINERS, DISCOVER, CHINA_UNION_PAY |
| Wallets (extra acquirer setup; Apple Pay also needs domain verification) | APPLEPAY, GOOGLEPAY, SAMSUNGPAY, PAYPAL |
| Currencies | EUR, GBP, USD, CAD, AUD, TRY, CHF, SEK, NOK, DKK, PLN, CZK, NZD, JPY |
Four open-banking methods across NL, MT and GB. Each is a standalone solution below, all created with the Direct API request shape above (and offered automatically inside Hosted Checkout). The per-solution blocks are generated from the same rail config the gateway uses, so they never drift.
LATAM solutions are created with the same POST /payments endpoint by selecting a country and (where the country has more than one option) a method. The customer is sent to a hosted payment link (returned as redirect_url), and settlement is confirmed by webhook. Required fields, currency and allowed document types differ per solution - the blocks below are generated from the rail's FIELD_MATRIX + GEO_CURRENCY so the docs never drift.
| Field | Required | Description |
|---|---|---|
country | yes | ECUADOR | CHILE | PERU | MEXICO | COLOMBIA - selects the LATAM rail. |
method | cond. | Chile: cards | bank. Peru: bank | qr. Single-method countries (Ecuador, Mexico, Colombia) omit it. |
amount | yes | Major units. Currency is implied by the country (see each solution). CLP and COP are zero-decimal - send integer amounts (e.g. "5000", not "5000.00"). |
currency | no | Optional override; defaults to the country currency. |
email | yes | Customer email. |
firstName, lastName | yes | Customer name (first / last). |
documentType | yes | Case-sensitive ID type - allowed values are per-country (see each solution). |
documentNumber | yes | ID document number. Format is validated per geo (e.g. Peru DNI = 8 digits, Chile RUT = NNNNNNNN-D). |
phone, phoneCode | cond. | Phone + country code (e.g. "+57"). Required for Chile cards and Colombia; Ecuador rejects phone fields entirely - omit them. |
successUrl, errorUrl | cond. | Redirect URLs after the hosted payment. Required varies per solution. |
redirectUrl | cond. | Colombia only - the post-interaction return URL (paid, unpaid or aborted). |
expiresAt | cond. | Link expiry, ISO 8601. Ecuador requires a near-future expiry; Peru/Chile ignore it (provider sets 30 min). |
merchant_reference, return_url, idempotency_key | no | Same as the Europe Direct API - your deposit id, browser return, and idempotency. |
GET /payments/{reference} if a customer does not return.Real-time local payment rails: UPI in India and PIX in Brazil. Both are created with the same POST /payments endpoint by selecting a solution. The customer is sent to a hosted payment page (returned as redirect_url) where they approve the payment in their own banking or UPI app, and settlement is confirmed by webhook.
redirectUrl always points at checkout.platinum-edge.ca/r/<reference>
— on every rail, for every payment. That single host is the one to allowlist,
and it does not change. We redirect the customer onward from there, so if an
underlying banking host ever changes, nothing in your integration has to.
Send the customer to redirectUrl exactly as returned; do not unwrap
or follow it server-side.
These are push payments: the customer initiates the transfer from their app, so there is no card, no 3-D Secure and no chargeback. The trade-off is that a customer can simply not complete it - always treat the webhook as the source of truth and poll GET /payments/{reference} if a customer does not return.
| Solution | Country | Currency | Per transaction | Customer pays with |
|---|---|---|---|---|
upi | India | INR | 100 - 50,000 | Any UPI app - GPay, PhonePe, Paytm |
pix | Brazil | BRL | 10 - 15,000 | PIX in any Brazilian banking app |
INR for upi and BRL for pix, or omit currency and we default to it. A mismatch is rejected rather than converted - sending an INR amount as BRL would be a hundredfold error, not a rounding one.| Field | Required | Description |
|---|---|---|
solution | yes | upi | pix - selects the rail. Never inferred from country or currency. |
amount | yes | Major units, up to 2 decimals (e.g. "1000" = 1,000 INR). |
currency | no | Defaults to the solution's currency. If sent, it must match. |
email | yes | Customer email. A real address measurably improves approval rates and lowers risk scoring. |
firstName, lastName | yes | Customer name. |
phone | yes | Customer phone. Mandatory on this rail - the payment is rejected without it. |
personalId | yes | India: the payer's UPI ID (e.g. name@bank). Brazil: the payer's CPF, 11 digits.See the warning below - this is the field that most often breaks an integration. |
merchant_reference, return_url, idempotency_key | no | Same as every other rail - your deposit id, where to send the customer back, and idempotency. |
personalId must belong to the person actually paying. It is how the rail matches an incoming transfer to your order. If a customer enters someone else's UPI ID or CPF - or you send an account holder's rather than the payer's - the money can arrive and the order can still never be matched. Collect it from the person completing the payment, and do not reuse a stored value from another customer.curl -X POST https://platinum-edge.ca/api/v1/payments \\
-H "Authorization: Bearer $PE_API_KEY" \\
-H "Content-Type: application/json" \\
-d '{
"solution": "upi",
"amount": "1000",
"currency": "INR",
"email": "player@example.com",
"firstName": "Karol",
"lastName": "Monch",
"phone": "+919876543210",
"personalId": "karol@okaxis",
"merchant_reference": "DEP-10231"
}'
{
"ok": true,
"reference": "PE-PK-MTVU4NN4-2EFF48",
"redirect_url": "https://..."
}
Send the customer to redirect_url. In India they approve the request in their UPI app; in Brazil they scan or paste the PIX code in their banking app. When the payment settles you receive a webhook - credit the balance from that, never from the customer returning to your page.
| Message | What to do |
|---|---|
personalId is required: the payer's UPI ID. | Collect the UPI ID from the customer before calling us. There is no default. |
personalId is required: the payer's CPF, matching the person paying. | Collect the CPF of whoever will pay - not the account holder if they differ. |
phone is required for this solution. | Add the customer's phone number to the request. |
UPI settles INR only. / PIX settles BRL only. | Send the solution's own currency, or omit currency. |
Enter an amount between 100 and 50000 INR. | The amount is outside the per-transaction band in the table above. |
This payment method is not available right now. | The solution is not enabled on your account. Contact us - it is an account setting, not a code problem. |
Accept cryptocurrency payments with direct settlement to your exchange account. No third-party custodian, no settlement delay - funds land in your exchange account the moment the blockchain confirms.
Use the same POST /payments endpoint with provider: "crypto" to create a crypto checkout session.
curl -X POST https://platinum-edge.ca/api/v1/payments \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"provider": "crypto",
"amount": 100.00,
"currency": "EUR",
"email": "buyer@example.com",
"firstName": "John",
"lastName": "Doe",
"return_url": "https://yoursite.com/checkout/return",
"merchant_reference": "your-order-123"
}'
| Field | Required | Description |
|---|---|---|
provider | yes | Must be "crypto" for Crypto Gateway checkout |
amount | yes | Amount in major units (e.g., 100.00 for €100) |
currency | yes | Presentment currency: EUR or USD |
email | yes | Buyer's email for receipt |
firstName, lastName | yes | Buyer's name |
return_url | no | Where to redirect after payment (https only) |
merchant_reference | no | Your order ID; echoed in webhooks |
{
"ok": true,
"reference": "PE-KC-MTLSSJEV-956930",
"redirect_url": "https://platinum-edge.ca/checkout/crypto?ref=PE-KC-MTLSSJEV-956930",
"status": "pending",
"provider": "crypto",
"crypto": {
"quote_ttl_minutes": 60,
"underpay_tolerance_pct": 1
}
}
| Field | Description |
|---|---|
reference | Platinum Edge payment reference (PE-KC-...) |
redirect_url | Send the buyer's browser here |
crypto.quote_ttl_minutes | How long the quoted crypto amount is valid |
crypto.underpay_tolerance_pct | % below exact quote still accepted (covers network fees) |
{
"ok": false,
"status": 422,
"error": "Amount 5.00 EUR is below the minimum 10.00 EUR"
}
| Code | Error | Action |
|---|---|---|
| 403 | "This payment method is currently disabled" | Crypto rail is disabled (admin kill-switch) |
| 422 | "Amount X.XX EUR is below the minimum" | Increase amount |
| 422 | "Amount X.XX EUR exceeds the maximum" | Decrease amount |
| 422 | "Currency XXX is not supported" | Use EUR or USD |
| 429 | "Velocity limit exceeded" | Buyer hit rate limits |
Redirect the buyer to the redirect_url. Our hosted checkout guides them through:
| Coin | Networks | Confirmations |
|---|---|---|
| USDT | TRC20, ERC20, BEP20, Solana | 3-200 |
| USDC | ERC20, Solana | 64-200 |
| BTC | Bitcoin | 3 |
| ETH | Ethereum | 64 |
TRC20 (Tron) is fastest at 3 confirms; ERC20 requires 64 confirms.
We POST to your webhook URL when the payment settles:
{
"id": "evt_9f3a7c4b8e2d...",
"event": "payment.approved",
"livemode": true,
"created_at": "2026-09-03T14:30:00Z",
"data": {
"reference": "PE-KC-MTLSSJEV-956930",
"merchant_reference": "your-order-123",
"status": "approved",
"amount": "100.00",
"currency": "EUR"
}
}
| Event | When |
|---|---|
payment.approved | Deposit confirmed on-chain (SUCCESS in exchange) |
payment.declined | Quote expired with no matching deposit |
Every delivery carries an X-PE-Signature header:
X-PE-Signature: t=1693753800,v1=5d41402abc4b2a76b9719d911017c592
Verify by computing HMAC-SHA256(secret, t + "." + rawBody) and comparing to v1. Reject if t is older than 5 minutes (replay protection).
const crypto = require('crypto');
function verifyWebhook(req, secret) {
const sig = req.headers['x-pe-signature'];
const [tPart, vPart] = sig.split(',');
const t = tPart.split('=')[1];
const v1 = vPart.split('=')[1];
const expected = crypto
.createHmac('sha256', secret)
.update(t + '.' + JSON.stringify(req.body))
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))) {
throw new Error('Invalid signature');
}
// Reject old timestamps (replay protection)
if (Date.now() / 1000 - parseInt(t) > 300) {
throw new Error('Timestamp too old');
}
return true;
}
Unlike cards, crypto settlement is polling-based:
approvedPick a country, then a method. We jump you to that solution's section with its required fields, currency, allowed document types, and a copy-paste POST /payments example.
| Status | Meaning |
|---|---|
redirected / pending | Awaiting the customer's bank payment |
approved_for_payment | Authorized - money has left the payer's account |
approved | Settled - funds received |
declined | Not completed - no money taken |
expired | The customer never completed the payment in time. No money taken. Common on bank-redirect and crypto flows - treat it as a failed attempt, not an error. |
cancelled | The customer abandoned the payment deliberately. No money taken. |
error | The payment could not be processed for a technical reason. No money taken. Safe to let the customer retry. |
refunded | A previously approved payment was returned to the customer, in full or in part. Raised from the merchant dashboard (Refunds) - see the note below. |
approved as "do not credit", rather than matching on declined alone. A payment can end as expired, cancelled or error without ever being declined, and a settled one can later become refunded.POST /api/v1/refunds with { reference, amount?, reason? } opens a refund request; send an idempotency_key so a retry returns the original request instead of opening a second one. It does not move money by itself - refunds are executed per acquirer after review, and some rails have no refund API at all. You receive refund.approved or refund.rejected on your webhook when the decision is made, so your cashier can debit the player automatically. The Refunds section of your dashboard does the same thing for a human.Configure a webhook URL in the dashboard (Developers section). On every status change we send a POST to that URL so your cashier can credit a deposit automatically - no polling needed. This is identical for cards, Europe and LATAM solutions.
| Event | When |
|---|---|
payment.approved | Funds confirmed - safe to credit the player. |
payment.declined | Payment failed or was abandoned - do not credit. |
POST https://your-cashier.example.com/webhooks/platinum-edge
Content-Type: application/json
X-PE-Signature: t=1733600000,v1=9b2c...e1
{
"id": "evt_4a8c...",
"event": "payment.approved",
"livemode": true,
"created_at": "2026-06-08T12:00:00Z",
"data": {
"reference": "PE-MC-XXXX-XXXX",
"merchant_reference": "player-90431/dep-2207",
"status": "approved",
"amount": "250.00",
"currency": "GBP"
}
}
livemode is true for real payments and false for sandbox/test payments (test API key or the dashboard "send test payment"). Use it to ignore test events in your production handler, and to drive your own test assertions.
Each delivery carries an X-PE-Signature header: t=<unix-timestamp>,v1=<hmac>. Compute HMAC-SHA256 over the string "{t}.{raw_request_body}" using your webhook signing secret (shown once in the dashboard), and compare it - in constant time - to v1. Reject deliveries older than ~5 minutes to prevent replay.
// Node.js
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
const expected = crypto.createHmac("sha256", secret)
.update(parts.t + "." + rawBody).digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
const fresh = Math.abs(Date.now()/1000 - Number(parts.t)) < 300;
return ok && fresh;
}
Return 2xx within 10 seconds to acknowledge. Any other response (or a timeout) is retried with exponential backoff for up to 24 hours. Make your handler idempotent - key on data.reference - because a delivery can arrive more than once.
GET /payments/{reference} after redirecting the customer. Webhooks are strongly recommended for production deposits.This is the step that tops up the player's wallet in your system. We confirm the deposit and tell you who (merchant_reference) and how much (amount + currency); your server applies it to the player. We never touch your player accounts.
merchant_reference to something that identifies the player (and ideally the deposit), e.g. "player-90431/dep-2207".payment.approved with that same merchant_reference plus amount, currency, and our reference.merchant_reference to the player, and credits the amount - once.// POST /webhooks/platinum-edge (your server)
export default async function handler(req, res) {
const raw = req.rawBody; // the exact bytes we sent
if (!verify(raw, req.headers["x-pe-signature"], WEBHOOK_SECRET))
return res.status(401).end(); // reject forged / stale calls
const evt = JSON.parse(raw);
if (evt.event === "payment.approved") {
const playerId = playerFrom(evt.data.merchant_reference); // your mapping
// Idempotent: only credit the first time we see this reference.
if (await ledger.recordOnce(evt.data.reference)) {
await wallet.credit(playerId, evt.data.amount, evt.data.currency);
}
}
return res.status(200).json({ ok: true }); // 2xx within 10s
}
data.reference (retries can deliver twice); the amount/currency come from us and are locked to the deposit, so a player cannot inflate them; do not credit on the browser return_url alone - it is only a UX signal. The webhook is the source of truth.| Step | PlatinumEdge | You (casino) |
|---|---|---|
| Detect the bank payment succeeded | yes | - |
Send signed payment.approved (player ref + amount) | yes | - |
| Retry until acknowledged (up to 24h) | yes | - |
| Map reference to player and add balance | - | yes |
| Show the new balance in the player's profile | - | yes |
Payouts let you pay your own players - withdrawals from your cashier to a player's bank account. This is the reverse of a deposit and is distinct from settlement: settlement is when we automatically pay you (the merchant) your accumulated deposit balance; a payout is when you send money to one of your players. One neutral API covers every supported country.
pe_test_ key simulates a payout (no money moves) so you can build your integration end-to-end. Live payouts must be enabled on your account before they will process; until then a live request returns 503 { "ok": false, "error": "Payouts are not yet enabled for live mode." }. Ask us to enable live payouts when you are ready.Create a payout to a player's bank account. The amount is in major units; the currency is implied by the country (CLP and COP are zero-decimal - send integer amounts).
| Field | Required | Description |
|---|---|---|
country | yes | ECUADOR | CHILE | PERU | MEXICO | COLOMBIA. Selects the payout rail + currency. |
amount | yes | Major units, e.g. "250.00" (or integer "5000" for CLP / COP). |
currency | no | Optional override; defaults to the country currency. |
beneficiary | yes | The player's payout details - see Beneficiary by country for the required sub-fields. |
merchant_reference | no | Your own id for this payout (e.g. "player-90431/wd-5512"). Echoed on the payout webhook and on GET /payouts. Max 128 chars. |
idempotency_key | no | A unique string per payout attempt. Retrying the same key returns the original payout instead of sending a duplicate. Also accepted as the Idempotency-Key header. |
| Field | Description |
|---|---|
name | Beneficiary full name (first + last). For a company beneficiary, the company name. |
document_type | Case-sensitive ID type, per country (see each country block). |
document_number | ID document number. |
account_number | Destination account number (the CLABE for Mexico). |
bank_code | Case-sensitive bank code, per country (see each country block). |
account_type | Account type code, per country (e.g. savings / checking / CLABE). |
cci | Peru only - the interbank (CCI) number, different from account_number. For other countries this is optional and defaults to account_number. |
201{
"ok": true,
"payout": {
"reference": "PE-PO-XXXX-XXXX",
"status": "pending",
"amount": "250.00",
"currency": "PEN",
"country": "PERU",
"merchant_reference": "player-90431/wd-5512"
}
}
A payout starts pending. The final outcome arrives later as a payout webhook (and is reflected on GET /payouts). A 422 with a missing array is returned when required beneficiary fields are absent for the country.
Required beneficiary sub-fields differ per country. The blocks below are generated from the same rail data the payout API validates against, so they never drift.
| Status | Meaning |
|---|---|
pending | Accepted and awaiting settlement to the player's account (initial state). |
paid | Settled - the funds reached the player's account. |
bounced | Rejected by the bank (e.g. invalid account, closed account, wrong code). Final. |
reverted | Was paid, then reversed - the funds were returned. Final. |
When a payout reaches a terminal state we POST a webhook to your configured URL, signed with the same X-PE-Signature scheme as payment webhooks (see Webhooks for verification). Subscribe to these events in the dashboard.
| Event | When |
|---|---|
payout.paid | The payout settled - the player received the funds. |
payout.bounced | The payout was rejected and did not pay out. |
payout.reverted | A previously-paid payout was reversed; funds returned. |
{
"id": "evt_...",
"event": "payout.paid",
"livemode": true,
"created_at": "2026-06-18T12:00:00Z",
"data": {
"reference": "PE-PO-XXXX-XXXX",
"merchant_reference": "player-90431/wd-5512",
"status": "paid",
"amount": "250.00",
"currency": "PEN"
}
}
Be idempotent on data.reference - a delivery can arrive more than once. livemode is false for sandbox payouts.
List your payouts, newest first. A test key lists only sandbox payouts; a live key lists only live payouts. Paginate with the before cursor.
{ "ok": true, "data": [ /* payouts */ ], "paging": { "limit": 50, "next_before": 123 } }
For payment platforms and PSPs integrating PlatinumEdge as an upstream provider. Your gateway calls our API server to server; we return a hosted checkout URL and notify you of the outcome with a signed webhook.
You keep your own platform, your own merchant relationships and your own checkout. PlatinumEdge is a payment rail behind them. You do not need to build a card form, hold card data or certify against a scheme - the payer lands on our hosted page and we route to the acquirer.
| You provide | We provide |
|---|---|
| Your gateway, your merchants, your reconciliation | Cards, open banking, LATAM local methods and crypto |
| A server to server call per payment | A hosted checkout page, PCI DSS Level 1 |
| An endpoint to receive webhooks | Signed webhooks, retried for 24 hours |
| Settlement to your own merchants | Settlement to you |
POST /api/v1/checkout-sessions with the amount, currency and your own reference.url. Redirect the payer to it in a full browser window or tab.success_url or cancel_url.An API key resolves to one merchant account. There is no platform-level key that can transact on behalf of many accounts in one call, and no sub_merchant_id parameter today.
In practice that means one PlatinumEdge account, and one key, per merchant you route to us. We create the account; you hold the key in your platform and select it per transaction. If you need a single credential spanning many merchants, or programmatic merchant creation, tell us - it is on our roadmap and the design depends on how your merchants are contracted.
pe_test_ key works for the direct API and for reads, but a checkout session carries no test flag, so a session created with a test key would produce a real charge. Use the direct API in test mode, then switch to a live key for hosted checkout. We will say so explicitly when we issue your sandbox credentials.POST /api/v1/refunds with the payment reference, and optionally an amount for a partial refund. Send an idempotency_key so a retry returns the original request instead of opening a second one.
This opens a refund request; it does not move money by itself. Refunds execute per acquirer after review, and some rails have no refund API at all and are settled by hand. You receive refund.approved or refund.rejected on your webhook when the decision is made - reconcile from that, not from the 201.
Every webhook is signed HMAC-SHA256 over {timestamp}.{body} and sent as X-PE-Signature: t=<unix>,v1=<hex>. Verify the signature and reject a timestamp older than five minutes. Compare digests with a constant-time function.
| Event | Meaning |
|---|---|
payment.approved | Funds authorised. Safe to credit. |
payment.declined | Final failure. |
payment.pending | Awaiting the bank or the payer. |
refund.requested | A refund was opened. |
refund.approved | Refund executed. |
refund.rejected | Refund refused. |
payout.paid / payout.failed | Outbound payment outcome. |
chargeback.opened | A dispute was raised. |
Subscribe only to what you handle. New event types are added over time, so ignore unrecognised events rather than erroring - a strict parser that rejects unknown types will start failing the day we add one. Respond 2xx quickly and process asynchronously; we retry with exponential backoff for about 24 hours, and the same event may arrive twice, so make your handler idempotent on the event id.
If your platform uses a connector framework with a defined upstream interface, send us that specification - building to your documented contract is faster and less error-prone than either side guessing.
Fetch a single payment's current status.
curl https://platinum-edge.ca/api/v1/payments/PE-MC-XXXX-XXXX \
-H "Authorization: Bearer pe_live_..."
{
"ok": true,
"payment": {
"reference": "PE-MC-XXXX-XXXX",
"status": "approved",
"amount": "250.00",
"fee": "7.25",
"net": "242.75",
"currency": "GBP",
"method": "card",
"settlement": "settled",
"customer_email": "customer@example.com",
"customer_name": "Alex Doe",
"merchant_reference": "player-90431/dep-2207",
"card_bin": "455673", "card_last4": "4321", "card_scheme": "visa",
"mid_code": "FIB",
"approved_at": "...", "settlement_date": "2026-09-09", "refund_status": null,
"created_at": "...", "updated_at": "..."
}
}
The money fields, which is what most integrations reconcile against:
| Field | Description |
|---|---|
amount | What the customer was charged, major units. |
fee | Our fee on this payment, major units, same currency. Returned on every payment - it was previously undocumented, so integrations reconciling amount against a settlement report saw an unexplained shortfall. |
net | amount minus fee: what settles to you. Reconcile against this, not amount. |
settlement | Where this payment is in our settlement cycle. |
method | The rail that took it, e.g. card, openbanking. |
decline_reason | Neutral decline category. null unless status is declined. |
card_bin, card_last4, card_scheme | Masked card details for your own risk screening, on settled card sales. null otherwise. The middle digits are never stored. |
mid_code | Short code for the account this transacted on, e.g. FIB. Lets you see that a route changed when an approval rate moves. null when unresolved. |
approved_at | When the payment was approved. null until then - use this, not created_at, when ageing a deposit. |
settlement_date | The date this payment settles to you, derived from approved_at. null unless approved. |
refund_status | Refund lifecycle: pending, approved, rejected, cancelled, or refunded once the payment itself is reversed. null when no refund was ever requested. |
List your payments, newest first. Paginate with the before cursor.
| Query | Description |
|---|---|
limit | 1-200 (default 50) |
before | Cursor: pass the last id from the previous page |
{ "ok": true, "data": [ /* payments */ ], "paging": { "limit": 50, "next_before": 123 } }
Errors return a non-2xx status with { "ok": false, "error": "message" }. 401 means a missing/invalid/revoked API key.