S
docs.syncra.money

Quickstart

Run your first payment in 5 minutes — from obtaining API keys to receiving the completion webhook

Quickstart

This guide takes you from onboarding to your first successful payment in 5 minutes. We will create a PayIn, redirect the player to the payment page, and receive the completion webhook. The code is copy-paste ready.

If you prefer the big picture first, start with the API overview.


What you need

Before you start integrating, make sure you have:

ResourceDescriptionWhere to get it
API TokenThe merchant's public 64-character hex token. Sent in the X-Merchant-Token header.Issued by the Syncra platform operator when your merchant account is created (shown once).
Webhook SecretA private secret key (32 bytes) used to compute HMAC-SHA256 signatures. Never transmitted over the network.Issued together with the API Token (shown once). Keep it secret.
Callback URLThe public HTTPS endpoint of your server that receives webhooks.Your server (for example, https://merchant.example/callbacks/syncra).

The Webhook Secret is shown once when keys are issued or rotated. If you lose it, ask the platform operator to rotate your keys. Details in Authentication.


Step 1. Authorization

Every request to the Syncra API is signed with HMAC-SHA256. The signature is computed over the string {timestamp}.{raw_body} and sent in the X-Signature header. This protects requests from tampering and replay attacks.

Below are ready-to-use signing examples for curl and Node.js. The full specification is in Authentication.

const crypto = require('crypto');

/**
 * Builds the authentication headers for a Syncra API request.
 * @param {string} body   — the raw request body (the JSON string as it goes on the wire).
 * @param {string} token  — X-Merchant-Token (64-char hex).
 * @param {string} secret — Webhook Secret (used for HMAC-SHA256).
 */
function authHeaders(body, token, secret) {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const payload = `${timestamp}.${body}`;
  const signature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  return {
    'X-Merchant-Token': token,
    'X-Signature': `t=${timestamp},v1=${signature}`,
    'Content-Type': 'application/json',
  };
}

module.exports = { authHeaders };
# With curl, openssl is the easiest way to compute the signature. The
# script below signs an arbitrary body and sends a balance GET request.
BODY='{"amount":150000,"currency":"RUB"}'
SECRET='whsec_your_webhook_secret_here'
TOKEN='mtoken_your_merchant_token_here'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')

curl -X GET https://api.syncra.money/api/v1/p2p/merchant/balance \
  -H "X-Merchant-Token: $TOKEN" \
  -H "X-Signature: t=$TS,v1=$SIG" \
  -H "Content-Type: application/json"

The server rejects requests whose t= timestamp in X-Signature differs from server time by more than 5 minutes (into the past) or 60 seconds (into the future). Make sure your system clock is synchronized (NTP).


Step 2. Create a PayIn

To accept a payment from a player, create a deal with POST /deals/payin. Five fields are enough to start: amount, currency, idempotency key, client identifier, and the payment method.

POST https://api.syncra.money/api/v1/p2p/merchant/deals/payin

Minimal field set

FieldTypeDescription
amountint64The amount in minor units of the currency (kopecks for RUB). 150000 = 1,500.00 RUB.
currencystringISO 4217 code, for example "RUB".
idempotency_keystringUnique request key (UUID v4). Protects against duplicates on retry. Learn more →
client_idstringThe player's identifier in your system (for example, player_9921).
payment_method_idstringRequired: the payment method UUID (canonical format) or slug from GET /payment-methods (Banks → Method catalog). An empty value is rejected with 400; get the identifier from the catalog before your first payment.

Request example (curl)

# 1. Generate an idempotency_key (UUID v4) — it must be unique per payment.
IDEMPOTENCY_KEY=$(uuidgen | tr 'A-Z' 'a-z')

# 2. The request body exactly as it goes on the wire (no extra whitespace).
BODY='{"amount":150000,"currency":"RUB","idempotency_key":"'"$IDEMPOTENCY_KEY"'","client_id":"player_9921","payment_method_id":"<UUID or slug from GET /payment-methods>"}'

# 3. Compute the HMAC-SHA256 signature.
SECRET='whsec_your_webhook_secret_here'
TOKEN='mtoken_your_merchant_token_here'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')

# 4. Send the request.
curl -X POST https://api.syncra.money/api/v1/p2p/merchant/deals/payin \
  -H "X-Merchant-Token: $TOKEN" \
  -H "X-Signature: t=$TS,v1=$SIG" \
  -H "Content-Type: application/json" \
  -d "$BODY"

Response example (200 OK)

{
  "deal": {
    "deal_id": "7ac148fe-19a3-45bb-b992-019fac55b721",
    "status": "ESCROW_LOCKED",
    "amount": "150000",
    "currency": "RUB",
    "payment_url": "https://checkout.syncra.money/pay/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
    "created_at": "2026-08-03T12:00:00Z"
  }
}

The payment_url field is the link to the hosted payment page — this is where you send the player.

The V2 response uses the payment_url field. In the deal object (MerchantDealInfo, proto field 9) it is stored under the same name, payment_url. A full walkthrough of the payment page is in Hosted Checkout.


Step 3. Redirect the player

Take payment_url from the response and redirect the player there. It is a hosted page on the checkout.syncra.money/pay/{hash} domain, branded for your merchant (colors, logo, name). You do not need to build a payment form yourself.

// Browser-side redirect after your backend creates the PayIn.
const response = await fetch('/your-backend/create-payin', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ playerId: 'player_9921', amount: 150000 }),
});
const { payment_url } = await response.json();

// Send the player to the Syncra payment page.
window.location = payment_url;
app.post('/api/deposit', async (req, res) => {
  const deal = await syncra.createPayIn({
    amount: req.body.amount,
    currency: 'RUB',
    idempotency_key: crypto.randomUUID(),
    client_id: req.user.id,
  });
  // Return the URL to the frontend — let it redirect the player.
  res.json({ payment_url: deal.payment_url });
});
<!-- If you want to redirect straight from the backend -->
<meta http-equiv="refresh" content="0; url={{payment_url}}" />

What the player sees on the payment page:

  1. The trader's payment details — card number / phone number, cardholder name.
  2. The amount and a timer — how much to transfer and how much time is left.
  3. Step-by-step instructions — how to make the transfer in the banking app.
  4. The "I paid" button — pressing it starts payment verification.

A full breakdown of every screen is in Hosted Checkout.


Step 4. Receive the webhook

When the payment is confirmed, Syncra sends a POST request with the deal.completed event to your callback_url. Below is a minimal Node.js server that verifies the signature and credits the deposit.

// webhook.js — run with: node webhook.js (requires express: npm i express)
const crypto = require('crypto');
const express = require('express');

const WEBHOOK_SECRET = 'whsec_your_webhook_secret_here';
const app = express();

// IMPORTANT: the signature is computed over the RAW body. Intercept the body
// before JSON parsing and keep it as rawBody.
app.use('/callbacks/syncra', express.raw({ type: 'application/json' }));

app.post('/callbacks/syncra', (req, res) => {
  const rawBody = req.body.toString('utf8');            // the raw body as a string
  const signatureHeader = req.get('X-Signature'); // t=...,v1=...

  // 1. Verify the V2 signature.
  if (!verifySignature(rawBody, signatureHeader, WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'invalid signature' });
  }

  // 2. Parse the body ONLY after the signature check passes.
  const event = JSON.parse(rawBody);

  // 3. Handle the event. IMPORTANT: return 200 as fast as possible — move
  //    heavy logic (crediting the deposit) to a background queue.
  if (event.event === 'deal.completed') {
    console.log(`Deposit confirmed: deal=${event.deal_id} amount=${event.amount}`);
    // creditPlayer(event.deal_id, event.amount); // your business logic
  }

  // 4. Return 200 — otherwise Syncra will retry the webhook.
  res.status(200).json({ status: 'success' });
});

// HMAC-SHA256 verification for the t={ts},v1={hex} format
function verifySignature(rawBody, signatureHeader, secret) {
  if (!signatureHeader) return false;
  const tsPart = signatureHeader.split(',').find(p => p.startsWith('t='));
  const sigPart = signatureHeader.split(',').find(p => p.startsWith('v1='));
  if (!tsPart || !sigPart) return false;

  const timestamp = tsPart.substring(2);
  const provided = sigPart.substring(3);

  // Replay protection: reject requests older than 5 minutes.
  const age = Math.abs(Math.floor(Date.now() / 1000) - parseInt(timestamp, 10));
  if (age > 300) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  // Timing-safe comparison.
  return crypto.timingSafeEqual(
    Buffer.from(provided, 'utf8'),
    Buffer.from(expected, 'utf8'),
  );
}

app.listen(3000, () => console.log('Webhook server on :3000'));
Useful fields of the deal.completed webhook
{
  "event": "deal.completed",
  "deal_id": "7ac148fe-19a3-45bb-b992-019fac55b721",
  "merchant_id": "8f870a31-b88d-4cb0-a882-628d08cbda88",
  "amount": 150000,
  "currency": "RUB",
  "status": "COMPLETED",
  "received_amount": 150000,
  "amount_match_status": "full",
  "timestamp": "2026-08-03T12:04:12Z"
}
FieldWhat it is for
deal_idThe deal identifier — use it to match the payment to your order.
amount / received_amountHow much was expected / how much actually arrived.
amount_match_statusfull / partial / overpaid — the amount match; overpayment and underpayment exist as separate statuses only in the numeric V1 codes (14/15) — in V2 they are always expressed through this field.
statusThe deal status (COMPLETED for deal.completed; the full reference is in Statuses).

Always make your processing idempotent. Syncra may deliver the same webhook more than once (network desync, retries). Check by deal_id that you have already credited this payment. The full rules are in Callbacks.


Done

You have just processed your first payment. The full flow:

Загрузка диаграммы...

What's next

On this page