Platinum-Edge API Website Get API keys

PlatinumEdge API

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

Two ways to accept a payment

ApproachBest forYou build
Hosted Checkout recommendedThe 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 APIFull 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.

Quickstart

The fastest way to take a deposit - three steps, copy-paste ready.

  1. 1. Get an API keyCreate a 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.
  2. 2. Create a checkout sessionCall 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
    }
  3. 3. Redirect, then confirm by webhookSend the player's browser to url. When they pay, we POST payment.approved to your webhook - credit the deposit then. See Hosted Checkout for every field.

Authentication

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
Every key shown in these docs is a placeholder for illustration only - no key on this page is live and none can be issued here. Real keys are generated in your merchant dashboard. Treat them like passwords: if one leaks, revoke it in the dashboard and create a new one.

Test mode

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

Hosted Checkout

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.

Two hosted options — same merchant account, same settings:
  • API session (documented here): call 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.
  • Your branded page: a permanent link at 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.
POST/checkout-sessions
FieldRequiredDescription
amountyesMajor units, e.g. "250.00"
return_urlrecommendedhttps:// page to send the player back to. We append ?ref=&status=.
merchant_referencenoYour player/deposit id; echoed on the webhook and status lookup.
method, bankCountrynoPre-select the rail (e.g. fps/GB). Omit to let the player choose on our page.
email, firstName, lastName, country, address1, city, zipCode, phonenoPrefill the player's details. Anything omitted is collected on the hosted page.
expires_innoSession 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"
  }'

Response 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.

Direct API

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.

POST/payments

Request body (JSON)

FieldRequiredDescription
amountyesMajor units, e.g. "250.00"
methodyessepa | instant | revolut | fps
bankCountryyesNL | MT | GB (FPS is GB only)
emailyesCustomer email
firstName, lastNameyesCustomer name
countryyesBilling country, ISO-2 (e.g. GB)
address1, city, zipCodeyesBilling address
phonenoCustomer phone
merchant_referencenoYour 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_urlnoWhere 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_keynoA 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.
solutionnoName 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_labelnoSet 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.

Example

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"
  }'

Response 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.

Cards Visa / Mastercard & more

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.

Two ways to integrate cards

MethodBest forYou 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.

Create a card payment

POST/payments

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.

FieldRequiredDescription
methodyesMust be "cards" to select the card rail.
amountyesMajor units, up to 2 decimals, as a string ("200.00"). Range 1.00 - 10,000.00.
currencyno3-letter ISO. Defaults to EUR. See Brands & currencies for the accepted set.
emailyesShopper email.
firstName, lastNameyesCardholder name (first / last).
countryyesISO 3166-1 alpha-2 only (SE, not Sweden). Mandatory for 3-D Secure v2 - a wrong code fails authentication and the payment.
address1, city, zipCodeyesBilling address, city and postal / ZIP code.
phonenoShopper phone in E.164 (+46701234567).
merchant_reference, idempotency_key, metadatanoSame 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.

Option A - Hosted card page

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.

Set a 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.

Option B - Embedded card form

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>
Do not add an 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).

Settle & outcome

POST/payments/{reference}/settle

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
  }
}

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_reasonSuggested 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."
You can also read 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.

Brands & currencies

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.

GroupValues
Card schemesVISA, MASTER, MAESTRO, AMEX, JCB, DINERS, DISCOVER, CHINA_UNION_PAY
Wallets (extra acquirer setup; Apple Pay also needs domain verification)APPLEPAY, GOOGLEPAY, SAMSUNGPAY, PAYPAL
CurrenciesEUR, GBP, USD, CAD, AUD, TRY, CHF, SEK, NOK, DKK, PLN, CZK, NZD, JPY
Which brands and currencies actually settle is set by your acquirer on the merchant account; the sets above are what the platform can present. Your live enabled list is confirmed per account at go-live. 3-D Secure v2 is on by default.

European methods Open Banking

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 Peru / Chile / Ecuador / Mexico / Colombia

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.

Common LATAM request shape

FieldRequiredDescription
countryyesECUADOR | CHILE | PERU | MEXICO | COLOMBIA - selects the LATAM rail.
methodcond.Chile: cards | bank. Peru: bank | qr. Single-method countries (Ecuador, Mexico, Colombia) omit it.
amountyesMajor 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").
currencynoOptional override; defaults to the country currency.
emailyesCustomer email.
firstName, lastNameyesCustomer name (first / last).
documentTypeyesCase-sensitive ID type - allowed values are per-country (see each solution).
documentNumberyesID document number. Format is validated per geo (e.g. Peru DNI = 8 digits, Chile RUT = NNNNNNNN-D).
phone, phoneCodecond.Phone + country code (e.g. "+57"). Required for Chile cards and Colombia; Ecuador rejects phone fields entirely - omit them.
successUrl, errorUrlcond.Redirect URLs after the hosted payment. Required varies per solution.
redirectUrlcond.Colombia only - the post-interaction return URL (paid, unpaid or aborted).
expiresAtcond.Link expiry, ISO 8601. Ecuador requires a near-future expiry; Peru/Chile ignore it (provider sets 30 min).
merchant_reference, return_url, idempotency_keynoSame as the Europe Direct API - your deposit id, browser return, and idempotency.
LATAM settlement is webhook + polling. The provider only notifies on PAID / REVERTED (there is no failure webhook), so always treat the webhook as the source of truth and poll GET /payments/{reference} if a customer does not return.

Local rails India / Brazil

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.

Allowlisting: the only host we return is ours. 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.

Solutions

SolutionCountryCurrencyPer transactionCustomer pays with
upiIndiaINR100 - 50,000Any UPI app - GPay, PhonePe, Paytm
pixBrazilBRL10 - 15,000PIX in any Brazilian banking app
The currency is fixed per solution. Send 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.

Request body (JSON)

FieldRequiredDescription
solutionyesupi | pix - selects the rail. Never inferred from country or currency.
amountyesMajor units, up to 2 decimals (e.g. "1000" = 1,000 INR).
currencynoDefaults to the solution's currency. If sent, it must match.
emailyesCustomer email. A real address measurably improves approval rates and lowers risk scoring.
firstName, lastNameyesCustomer name.
phoneyesCustomer phone. Mandatory on this rail - the payment is rejected without it.
personalIdyesIndia: 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_keynoSame 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.

Example

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"
  }'

Response 200

{
  "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.

Errors you will meet while integrating

MessageWhat 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.

Crypto Gateway BTC / ETH / USDT / USDC

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.

Key differences from card/bank payments:
No chargebacks - crypto is final and irreversible
No 3-D Secure - funds go directly to your exchange address
Quote expiry - price is locked for 60 minutes; buyer must pay within that window
Polling settlement - we poll the exchange deposit list and promote the order when confirmations are met

Create a crypto payment

Use the same POST /payments endpoint with provider: "crypto" to create a crypto checkout session.

Request

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"
  }'

Request fields

FieldRequiredDescription
provideryesMust be "crypto" for Crypto Gateway checkout
amountyesAmount in major units (e.g., 100.00 for €100)
currencyyesPresentment currency: EUR or USD
emailyesBuyer's email for receipt
firstName, lastNameyesBuyer's name
return_urlnoWhere to redirect after payment (https only)
merchant_referencenoYour order ID; echoed in webhooks

Response - Success (200)

{
  "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
  }
}
FieldDescription
referencePlatinum Edge payment reference (PE-KC-...)
redirect_urlSend the buyer's browser here
crypto.quote_ttl_minutesHow long the quoted crypto amount is valid
crypto.underpay_tolerance_pct% below exact quote still accepted (covers network fees)

Response - Error

{
  "ok": false,
  "status": 422,
  "error": "Amount 5.00 EUR is below the minimum 10.00 EUR"
}
CodeErrorAction
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

Hosted crypto checkout

Redirect the buyer to the redirect_url. Our hosted checkout guides them through:

  1. Coin selection - USDT, USDC, BTC, or ETH (configurable)
  2. Network selection - For multi-chain tokens (USDT supports TRC20, ERC20, BEP20, Solana)
  3. Payment screen - QR code, deposit address, exact crypto amount, countdown timer
  4. Success/expiry - Confirmation or option to retry if quote expired

Supported coins & networks

CoinNetworksConfirmations
USDTTRC20, ERC20, BEP20, Solana3-200
USDCERC20, Solana64-200
BTCBitcoin3
ETHEthereum64

TRC20 (Tron) is fastest at 3 confirms; ERC20 requires 64 confirms.

Crypto webhooks

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"
  }
}

Events

EventWhen
payment.approvedDeposit confirmed on-chain (SUCCESS in exchange)
payment.declinedQuote expired with no matching deposit

Signature verification

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;
}

Settlement & reconciliation

Unlike cards, crypto settlement is polling-based:

  1. Our reconciliation cron polls the exchange deposit API every 5 minutes
  2. When a deposit matches (same address, amount within tolerance, confirmations met), the order is promoted to approved
  3. Your webhook fires
Late payments: A deposit arriving after quote expiry (but within 24h) still promotes the order. The reconciler re-checks expired orders specifically for this case.
Underpayments: Deposits below the tolerance threshold stay open until expiry, then become an operator question (visible in your admin panel).

Solution picker

Pick 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.

Find your solution

Payment statuses

StatusMeaning
redirected / pendingAwaiting the customer's bank payment
approved_for_paymentAuthorized - money has left the payer's account
approvedSettled - funds received
declinedNot completed - no money taken
expiredThe 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.
cancelledThe customer abandoned the payment deliberately. No money taken.
errorThe payment could not be processed for a technical reason. No money taken. Safe to let the customer retry.
refundedA previously approved payment was returned to the customer, in full or in part. Raised from the merchant dashboard (Refunds) - see the note below.
Handle statuses you do not recognise. Treat anything that is not 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.
Refunds can be raised over the API or in the dashboard. 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.

Webhooks

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.

Events

EventWhen
payment.approvedFunds confirmed - safe to credit the player.
payment.declinedPayment failed or was abandoned - do not credit.

Payload

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.

Verifying the signature

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;
}

Responding & retries

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.

Until your webhook is configured you can still poll GET /payments/{reference} after redirecting the customer. Webhooks are strongly recommended for production deposits.

Crediting the player's balance (your side)

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.

Flow

  1. When you create the payment / checkout session, set merchant_reference to something that identifies the player (and ideally the deposit), e.g. "player-90431/dep-2207".
  2. On success we POST payment.approved with that same merchant_reference plus amount, currency, and our reference.
  3. Your webhook handler verifies the signature, maps merchant_reference to the player, and credits the amount - once.

Reference handler

// 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
}
Rules: verify the signature before trusting any event; be idempotent on 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.

Who does what

StepPlatinumEdgeYou (casino)
Detect the bank payment succeededyes-
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 Pay your players

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.

Test first. A 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

POST/payouts

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).

Request body (JSON)

FieldRequiredDescription
countryyesECUADOR | CHILE | PERU | MEXICO | COLOMBIA. Selects the payout rail + currency.
amountyesMajor units, e.g. "250.00" (or integer "5000" for CLP / COP).
currencynoOptional override; defaults to the country currency.
beneficiaryyesThe player's payout details - see Beneficiary by country for the required sub-fields.
merchant_referencenoYour own id for this payout (e.g. "player-90431/wd-5512"). Echoed on the payout webhook and on GET /payouts. Max 128 chars.
idempotency_keynoA 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.

Beneficiary object

FieldDescription
nameBeneficiary full name (first + last). For a company beneficiary, the company name.
document_typeCase-sensitive ID type, per country (see each country block).
document_numberID document number.
account_numberDestination account number (the CLABE for Mexico).
bank_codeCase-sensitive bank code, per country (see each country block).
account_typeAccount type code, per country (e.g. savings / checking / CLABE).
cciPeru only - the interbank (CCI) number, different from account_number. For other countries this is optional and defaults to account_number.

Response 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.

Beneficiary requirements by 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.

Payout statuses

StatusMeaning
pendingAccepted and awaiting settlement to the player's account (initial state).
paidSettled - the funds reached the player's account.
bouncedRejected by the bank (e.g. invalid account, closed account, wrong code). Final.
revertedWas paid, then reversed - the funds were returned. Final.

Payout webhook events

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.

EventWhen
payout.paidThe payout settled - the player received the funds.
payout.bouncedThe payout was rejected and did not pay out.
payout.revertedA 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 payouts

GET/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 } }

PSP & platform integration Upstream provider

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.

How it works

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 provideWe provide
Your gateway, your merchants, your reconciliationCards, open banking, LATAM local methods and crypto
A server to server call per paymentA hosted checkout page, PCI DSS Level 1
An endpoint to receive webhooksSigned webhooks, retried for 24 hours
Settlement to your own merchantsSettlement to you

Payment flow

  1. Your gateway calls POST /api/v1/checkout-sessions with the amount, currency and your own reference.
  2. We return a url. Redirect the payer to it in a full browser window or tab.
  3. The payer completes payment on our page. We handle 3-D Secure and the rail.
  4. We send a signed webhook to your endpoint. This is the authoritative result.
  5. The payer is returned to your success_url or cancel_url.
Do not embed the checkout in an iframe. 3-D Secure challenge flows and several bank redirect pages refuse to render inside a third-party frame, and the payment will fail for a subset of your traffic rather than all of it - which makes it look intermittent. Use a top-level redirect. This also keeps your merchants eligible for the simplest PCI self-assessment, because every element of the payment page is served directly by us.
The browser return is not a result. A payer can close the tab, lose connection or return before the acquirer has confirmed. Credit and reconcile from the webhook only.

Accounts & keys

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.

Test keys cannot create a hosted checkout session. A 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.

Refunds

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.

Webhook events

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.

EventMeaning
payment.approvedFunds authorised. Safe to credit.
payment.declinedFinal failure.
payment.pendingAwaiting the bank or the payer.
refund.requestedA refund was opened.
refund.approvedRefund executed.
refund.rejectedRefund refused.
payout.paid / payout.failedOutbound payment outcome.
chargeback.openedA 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.

Integration checklist

  1. Confirm the account model with us - how many of your merchants, and how they are contracted.
  2. Receive sandbox credentials and your webhook signing secret.
  3. Build the direct API path in test mode; verify signatures on a real webhook.
  4. Switch to a live key and test one hosted checkout session end to end with a small real amount.
  5. Confirm your reconciliation matches ours for that transaction before scaling volume.

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.

Get a payment

GET/payments/{reference}

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:

FieldDescription
amountWhat the customer was charged, major units.
feeOur 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.
netamount minus fee: what settles to you. Reconcile against this, not amount.
settlementWhere this payment is in our settlement cycle.
methodThe rail that took it, e.g. card, openbanking.
decline_reasonNeutral decline category. null unless status is declined.
card_bin, card_last4, card_schemeMasked card details for your own risk screening, on settled card sales. null otherwise. The middle digits are never stored.
mid_codeShort 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_atWhen the payment was approved. null until then - use this, not created_at, when ageing a deposit.
settlement_dateThe date this payment settles to you, derived from approved_at. null unless approved.
refund_statusRefund lifecycle: pending, approved, rejected, cancelled, or refunded once the payment itself is reversed. null when no refund was ever requested.

List payments

GET/payments

List your payments, newest first. Paginate with the before cursor.

QueryDescription
limit1-200 (default 50)
beforeCursor: pass the last id from the previous page
{ "ok": true, "data": [ /* payments */ ], "paging": { "limit": 50, "next_before": 123 } }

Errors

Errors return a non-2xx status with { "ok": false, "error": "message" }. 401 means a missing/invalid/revoked API key.