SDK and Code Examples
Usage examples for the Syncra Merchant API V2 in various programming languages
SDK integration and usage examples
This section provides ready-to-use code examples for integrating with the Syncra Merchant API V2 in four languages: curl (HTTP), Python, Node.js, and PHP.
All examples below are verified against the V2 proto field names. Key invariants:
- PayIn:
payment_method_id(UUID), withoutsuccess_url/failed_url. - Withdraw:
tron_address(NOTaddress), withoutblockchain: "TRC20". - Webhook verify: the
X-Signatureheader (NOTX-Syncra-Signature).
1. Quick start
Creating a simple payment link to accept a payment. payment_method_id
is required — a UUID or a slug from GET /payment-methods
(section 3 below).
curl -X POST https://api.syncra.money/api/v1/p2p/merchant/deals/payin \
-H "X-Merchant-Token: your_merchant_token" \
-H "X-Timestamp: 1783856120" \
-H "X-Signature: t=1783856120,v1=9e87d0c3c8801d0a5198d023b6b19a16f2c8d2037920ab3e09819cd8e412cfab" \
-H "Content-Type: application/json" \
-d '{
"amount": 150000,
"currency": "RUB",
"payment_method_id": "sbp_rub",
"idempotency_key": "quickstart_order_001"
}'import requests
import time
import hmac
import hashlib
import json
token = "your_merchant_token"
secret = "your_webhook_secret"
timestamp = str(int(time.time()))
body = json.dumps({
"amount": 150000,
"currency": "RUB",
"payment_method_id": "sbp_rub",
"idempotency_key": "quickstart_order_001"
})
payload = f"{timestamp}.{body}"
signature = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
headers = {
"X-Merchant-Token": token,
"X-Timestamp": timestamp,
"X-Signature": f"t={timestamp},v1={signature}",
"Content-Type": "application/json"
}
response = requests.post(
"https://api.syncra.money/api/v1/p2p/merchant/deals/payin",
headers=headers,
data=body
)
print(response.json())const crypto = require('crypto');
const token = 'your_merchant_token';
const secret = 'your_webhook_secret';
const timestamp = Math.floor(Date.now() / 1000).toString();
const body = JSON.stringify({
amount: 150000,
currency: 'RUB',
payment_method_id: 'sbp_rub',
idempotency_key: 'quickstart_order_001'
});
const payload = `${timestamp}.${body}`;
const signature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
fetch('https://api.syncra.money/api/v1/p2p/merchant/deals/payin', {
method: 'POST',
headers: {
'X-Merchant-Token': token,
'X-Timestamp': timestamp,
'X-Signature': `t=${timestamp},v1=${signature}`,
'Content-Type': 'application/json'
},
body: body
})
.then(res => res.json())
.then(console.log);<?php
$token = "your_merchant_token";
$secret = "your_webhook_secret";
$timestamp = (string)time();
$body = json_encode([
"amount" => 150000,
"currency" => "RUB",
"payment_method_id" => "sbp_rub",
"idempotency_key" => "quickstart_order_001"
]);
$payload = $timestamp . '.' . $body;
$signature = hash_hmac('sha256', $payload, $secret);
$ch = curl_init("https://api.syncra.money/api/v1/p2p/merchant/deals/payin");
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"X-Merchant-Token: {$token}",
"X-Timestamp: {$timestamp}",
"X-Signature: t={$timestamp},v1={$signature}",
"Content-Type: application/json"
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
echo $response;
?>2. Initialization and the authentication helper (Auth Setup)
A helper class for building the HMAC-SHA256 signature headers. The
signature is computed over the string "{timestamp}.{body}".
import time
import hmac
import hashlib
class SyncraAuth:
def __init__(self, token: str, secret: str):
self.token = token
self.secret = secret
def get_headers(self, body_str: str) -> dict:
timestamp = str(int(time.time()))
payload = f"{timestamp}.{body_str}"
signature = hmac.new(
self.secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
return {
"X-Merchant-Token": self.token,
"X-Timestamp": timestamp,
"X-Signature": f"t={timestamp},v1={signature}",
"Content-Type": "application/json"
}const crypto = require('crypto');
class SyncraAuth {
constructor(token, secret) {
this.token = token;
this.secret = secret;
}
getHeaders(body) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const payload = `${timestamp}.${body}`;
const signature = crypto
.createHmac('sha256', this.secret)
.update(payload)
.digest('hex');
return {
'X-Merchant-Token': this.token,
'X-Timestamp': timestamp,
'X-Signature': `t=${timestamp},v1=${signature}`,
'Content-Type': 'application/json'
};
}
}<?php
class SyncraAuth {
private $token;
private $secret;
public function __construct($token, $secret) {
$this->token = $token;
$this->secret = $secret;
}
public function getHeaders($bodyStr) {
$timestamp = (string)time();
$payload = $timestamp . '.' . $bodyStr;
$signature = hash_hmac('sha256', $payload, $this->secret);
return [
"X-Merchant-Token: {$this->token}",
"X-Timestamp: {$timestamp}",
"X-Signature: t={$timestamp},v1={$signature}",
"Content-Type: application/json"
];
}
}
?>3. Accepting payments (Create PayIn)
Creating a payment session for the customer. Uses payment_method_id
(the method UUID from GET /payment-methods). The success_url /
failed_url fields are not passed in V2 — redirects are configured
at the merchant level.
curl -X POST https://api.syncra.money/api/v1/p2p/merchant/deals/payin \
-H "X-Merchant-Token: your_token" \
-H "X-Timestamp: 1783856120" \
-H "X-Signature: t=1783856120,v1=9e87d0c3c8801d0a5198d023b6b19a16f2c8d2037920ab3e09819cd8e412cfab" \
-H "Content-Type: application/json" \
-d '{
"amount": 250000,
"currency": "RUB",
"payment_method_id": "550e8400-e29b-41d4-a716-446655440000",
"idempotency_key": "unique_order_99812",
"callback_url": "https://yourserver.com/callback",
"description": "Customer 99812 account top-up"
}'# Uses the SyncraAuth helper class from section 2
auth = SyncraAuth("your_token", "your_secret")
body = json.dumps({
"amount": 250000,
"currency": "RUB",
"payment_method_id": "550e8400-e29b-41d4-a716-446655440000",
"idempotency_key": "unique_order_99812",
"callback_url": "https://yourserver.com/callback",
"description": "Customer 99812 account top-up"
})
headers = auth.get_headers(body)
res = requests.post(
"https://api.syncra.money/api/v1/p2p/merchant/deals/payin",
headers=headers, data=body
)
print(res.json())
# Response: MerchantDealInfo (deal_id, payment_url).const auth = new SyncraAuth('your_token', 'your_secret');
const body = JSON.stringify({
amount: 250000,
currency: 'RUB',
payment_method_id: '550e8400-e29b-41d4-a716-446655440000',
idempotency_key: 'unique_order_99812',
callback_url: 'https://yourserver.com/callback',
description: 'Customer 99812 account top-up'
});
const headers = auth.getHeaders(body);
fetch('https://api.syncra.money/api/v1/p2p/merchant/deals/payin', {
method: 'POST',
headers: headers,
body: body
})
.then(res => res.json())
.then(console.log);
// Response: MerchantDealInfo (deal_id, payment_url).<?php
$auth = new SyncraAuth("your_token", "your_secret");
$body = json_encode([
"amount" => 250000,
"currency" => "RUB",
"payment_method_id" => "550e8400-e29b-41d4-a716-446655440000",
"idempotency_key" => "unique_order_99812",
"callback_url" => "https://yourserver.com/callback",
"description" => "Customer 99812 account top-up"
]);
$ch = curl_init("https://api.syncra.money/api/v1/p2p/merchant/deals/payin");
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, $auth->getHeaders($body));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
echo $response;
// Response: MerchantDealInfo (deal_id, payment_url).
?>4. Payouts to customers (Create PayOut)
Making a P2P payout to bank cards. The payment_method field is a slug
(card_rub, sbp_rub, mobcom_rub) or a method UUID from
GET /payment-methods, NOT an uppercase enum.
curl -X POST https://api.syncra.money/api/v1/p2p/merchant/deals/payout \
-H "X-Merchant-Token: your_token" \
-H "X-Timestamp: 1783856120" \
-H "X-Signature: t=1783856120,v1=9e87d0c3c8801d0a5198d023b6b19a16f2c8d2037920ab3e09819cd8e412cfab" \
-H "Content-Type: application/json" \
-d '{
"amount": 500000,
"currency": "RUB",
"wallet_id": "8f870a31-b88d-4cb0-a882-628d08cbda88",
"target_requisite": "2202201234567890",
"target_name": "Ivan I.",
"payment_method": "card_rub",
"idempotency_key": "unique_payout_1291"
}'auth = SyncraAuth("your_token", "your_secret")
body = json.dumps({
"amount": 500000,
"currency": "RUB",
"wallet_id": "8f870a31-b88d-4cb0-a882-628d08cbda88",
"target_requisite": "2202201234567890",
"target_name": "Ivan I.",
"payment_method": "card_rub",
"idempotency_key": "unique_payout_1291"
})
headers = auth.get_headers(body)
res = requests.post(
"https://api.syncra.money/api/v1/p2p/merchant/deals/payout",
headers=headers, data=body
)
print(res.json())
# Response: MerchantDealInfo (deal_id, status, target_requisite).const auth = new SyncraAuth('your_token', 'your_secret');
const body = JSON.stringify({
amount: 500000,
currency: 'RUB',
wallet_id: '8f870a31-b88d-4cb0-a882-628d08cbda88',
target_requisite: '2202201234567890',
target_name: 'Ivan I.',
payment_method: 'card_rub',
idempotency_key: 'unique_payout_1291'
});
const headers = auth.getHeaders(body);
fetch('https://api.syncra.money/api/v1/p2p/merchant/deals/payout', {
method: 'POST',
headers: headers,
body: body
})
.then(res => res.json())
.then(console.log);
// Response: MerchantDealInfo (deal_id, status, target_requisite).<?php
$auth = new SyncraAuth("your_token", "your_secret");
$body = json_encode([
"amount" => 500000,
"currency" => "RUB",
"wallet_id" => "8f870a31-b88d-4cb0-a882-628d08cbda88",
"target_requisite" => "2202201234567890",
"target_name" => "Ivan I.",
"payment_method" => "card_rub",
"idempotency_key" => "unique_payout_1291"
]);
$ch = curl_init("https://api.syncra.money/api/v1/p2p/merchant/deals/payout");
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, $auth->getHeaders($body));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
echo $response;
// Response: MerchantDealInfo (deal_id, status, target_requisite).
?>5. Webhook verification (Verify Callback)
Verifying the signature of incoming webhooks in your handler. The
signature arrives in the X-Signature header in the
t=<unix>,v1=<hex_hmac_sha256> format.
The header is called X-Signature (NOT X-Syncra-Signature).
The signature is computed over the raw body of the request — do not
normalize the JSON.
import hmac
import hashlib
def verify_syncra_webhook(raw_body: str, signature_header: str, secret: str) -> bool:
# Header: X-Signature: t=<unix>,v1=<hex>
parts = signature_header.split(',')
t_val = [p for p in parts if p.startswith('t=')][0][2:]
v1_val = [p for p in parts if p.startswith('v1=')][0][3:]
payload = f"{t_val}.{raw_body}"
expected = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(v1_val, expected)const crypto = require('crypto');
function verifySyncraWebhook(rawBody, signatureHeader, secret) {
// Header: X-Signature: t=<unix>,v1=<hex>
const parts = signatureHeader.split(',');
const t = parts.find(p => p.startsWith('t=')).substring(2);
const v1 = parts.find(p => p.startsWith('v1=')).substring(3);
const payload = `${t}.${rawBody}`;
const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex');
return crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}<?php
function verifySyncraWebhook($rawBody, $signatureHeader, $secret) {
// Header: X-Signature: t=<unix>,v1=<hex>
$parts = explode(',', $signatureHeader);
$t = '';
$v1 = '';
foreach ($parts as $part) {
if (strpos($part, 't=') === 0) $t = substr($part, 2);
if (strpos($part, 'v1=') === 0) $v1 = substr($part, 3);
}
$payload = $t . '.' . $rawBody;
$expected = hash_hmac('sha256', $payload, $secret);
return hash_equals($v1, $expected);
}
?>6. Deal history (List Deals)
A paginated request for the merchant's deal list with filters. The
ListMerchantDeals endpoint takes its parameters in the query string of
the GET method.
For GET requests, the HMAC signature is computed over an empty body (the parameters go in the URL, not in a JSON body).
curl -X GET "https://api.syncra.money/api/v1/p2p/merchant/deals?deal_type=PAYIN&statuses=COMPLETED&page_size=20" \
-H "X-Merchant-Token: your_token" \
-H "X-Timestamp: 1783856120" \
-H "X-Signature: t=1783856120,v1=9e87d0c3c8801d0a5198d023b6b19a16f2c8d2037920ab3e09819cd8e412cfab"auth = SyncraAuth("your_token", "your_secret")
# For a GET request, sign the empty body
headers = auth.get_headers("")
params = {
"deal_type": "PAYIN",
"statuses": ["COMPLETED"],
"page_size": 20
}
res = requests.get(
"https://api.syncra.money/api/v1/p2p/merchant/deals",
headers=headers, params=params
)
print(res.json())const auth = new SyncraAuth('your_token', 'your_secret');
// For a GET request, sign the empty body
const headers = auth.getHeaders('');
const params = new URLSearchParams({
deal_type: 'PAYIN',
page_size: '20'
});
params.append('statuses', 'COMPLETED');
fetch(`https://api.syncra.money/api/v1/p2p/merchant/deals?${params}`, {
method: 'GET',
headers: headers
})
.then(res => res.json())
.then(console.log);<?php
$auth = new SyncraAuth("your_token", "your_secret");
// For a GET request, sign the empty body
$headers = $auth->getHeaders("");
$query = http_build_query([
"deal_type" => "PAYIN",
"statuses" => ["COMPLETED"],
"page_size" => 20
]);
$ch = curl_init("https://api.syncra.money/api/v1/p2p/merchant/deals?" . $query);
curl_setopt($ch, CURLOPT_HTTPGET, 1);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
echo $response;
?>Filter parameters (Query Parameters)
| Field | Type | Required | Description |
|---|---|---|---|
deal_type | string | No | Operation type: "PAYIN" or "PAYOUT". |
statuses | array | No | An array of canonical statuses (a repeated parameter, for example statuses=COMPLETED&statuses=EXPIRED). |
created_after | string(RFC3339) | No | Lower bound on the deal creation time (inclusive). |
created_before | string(RFC3339) | No | Upper bound on the deal creation time (inclusive). |
page_size | int32 | No | Items per page (default 50, maximum 250). |
page_token | string | No | The next-page token from the previous response. |
7. Resolving disputes (Create Appeal)
Creating an appeal for a deal with a reason and the UUIDs of the uploaded evidence.
curl -X POST https://api.syncra.money/api/v1/p2p/merchant/appeals \
-H "X-Merchant-Token: your_token" \
-H "X-Timestamp: 1783856120" \
-H "X-Signature: t=1783856120,v1=9e87d0c3c8801d0a5198d023b6b19a16f2c8d2037920ab3e09819cd8e412cfab" \
-H "Content-Type: application/json" \
-d '{
"deal_id": "7ac148fe-19a3-45bb-b992-019fac55b721",
"reason": "Funds were transferred, but the payment was not credited",
"file_ids": ["f3a1b2c3-4d5e-6f7a-8b9c-0d1e2f3a4b5c"]
}'auth = SyncraAuth("your_token", "your_secret")
body = json.dumps({
"deal_id": "7ac148fe-19a3-45bb-b992-019fac55b721",
"reason": "Funds were transferred, but the payment was not credited",
"file_ids": ["f3a1b2c3-4d5e-6f7a-8b9c-0d1e2f3a4b5c"]
})
headers = auth.get_headers(body)
res = requests.post(
"https://api.syncra.money/api/v1/p2p/merchant/appeals",
headers=headers, data=body
)
print(res.json())const auth = new SyncraAuth('your_token', 'your_secret');
const body = JSON.stringify({
deal_id: '7ac148fe-19a3-45bb-b992-019fac55b721',
reason: 'Funds were transferred, but the payment was not credited',
file_ids: ['f3a1b2c3-4d5e-6f7a-8b9c-0d1e2f3a4b5c']
});
const headers = auth.getHeaders(body);
fetch('https://api.syncra.money/api/v1/p2p/merchant/appeals', {
method: 'POST',
headers: headers,
body: body
})
.then(res => res.json())
.then(console.log);<?php
$auth = new SyncraAuth("your_token", "your_secret");
$body = json_encode([
"deal_id" => "7ac148fe-19a3-45bb-b992-019fac55b721",
"reason" => "Funds were transferred, but the payment was not credited",
"file_ids" => ["f3a1b2c3-4d5e-6f7a-8b9c-0d1e2f3a4b5c"]
]);
$ch = curl_init("https://api.syncra.money/api/v1/p2p/merchant/appeals");
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, $auth->getHeaders($body));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
echo $response;
?>8. Crypto withdrawal
Withdrawing the merchant balance in Tether (USDT) to an external TRON
wallet. The address field is called tron_address (NOT address);
blockchain is optional and defaults to TRON.
curl -X POST https://api.syncra.money/api/v1/p2p/merchant/wallet/withdraw \
-H "X-Merchant-Token: your_token" \
-H "X-Timestamp: 1783856120" \
-H "X-Signature: t=1783856120,v1=9e87d0c3c8801d0a5198d023b6b19a16f2c8d2037920ab3e09819cd8e412cfab" \
-H "Content-Type: application/json" \
-d '{
"amount_usdt": 1000000,
"tron_address": "TYHCiW3XN5sP9cWJ5T4Dfs9Y18h46t9w1a",
"idempotency_key": "withdraw_001"
}'auth = SyncraAuth("your_token", "your_secret")
body = json.dumps({
"amount_usdt": 1000000,
"tron_address": "TYHCiW3XN5sP9cWJ5T4Dfs9Y18h46t9w1a",
"idempotency_key": "withdraw_001"
})
headers = auth.get_headers(body)
res = requests.post(
"https://api.syncra.money/api/v1/p2p/merchant/wallet/withdraw",
headers=headers, data=body
)
print(res.json())const auth = new SyncraAuth('your_token', 'your_secret');
const body = JSON.stringify({
amount_usdt: 1000000,
tron_address: 'TYHCiW3XN5sP9cWJ5T4Dfs9Y18h46t9w1a',
idempotency_key: 'withdraw_001'
});
const headers = auth.getHeaders(body);
fetch('https://api.syncra.money/api/v1/p2p/merchant/wallet/withdraw', {
method: 'POST',
headers: headers,
body: body
})
.then(res => res.json())
.then(console.log);<?php
$auth = new SyncraAuth("your_token", "your_secret");
$body = json_encode([
"amount_usdt" => 1000000,
"tron_address" => "TYHCiW3XN5sP9cWJ5T4Dfs9Y18h46t9w1a",
"idempotency_key" => "withdraw_001"
]);
$ch = curl_init("https://api.syncra.money/api/v1/p2p/merchant/wallet/withdraw");
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, $auth->getHeaders($body));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
echo $response;
?>9. Payment details on the payment page (CheckoutView)
POST /deals/payin itself does not return payment details — it only
returns payment_url. The customer gets the sensitive data (card number,
phone, IBAN) when they open the payment page: it calls the public endpoint
GET /api/v1/p2p/checkout/{hash} (no authorization — the hash acts as
a capability token) and renders CheckoutView.
If you are building your own frontend (instead of the hosted payform), use the same endpoint. All sensitive fields are encrypted in Vault and decrypted server-side; the PayIn API protocol provides no way to pass the PAN/IBAN to the merchant.
The CheckoutView contract (payment detail fields)
| Field | Type | Purpose | Non-empty when |
|---|---|---|---|
card_number | string | The full PAN (decrypted from Vault) or the wallet identifier for E_WALLET. | BANK_CARD, E_WALLET, deal active. |
holder_name | string | The full name of the card/account holder. | BANK_CARD, E_WALLET, BANK_TRANSFER, deal active. |
phone_number | string | The phone number in E.164 for SBP/SIM. | SBP, SIM, deal active. |
iban | string (optional) | The recipient's IBAN (ISO 13616), decrypted from Vault with the same transit key as the PAN. | BANK_TRANSFER, deal active. |
payment_method_name | string | The bank/channel name to display. | Always, when set. |
All payment detail fields are optional and empty when they do not
apply to the deal type. In terminal states (COMPLETED, EXPIRED,
FAILED, ...) the sensitive fields are zeroed out — the customer no
longer needs the PAN/IBAN.
Response example: an IBAN transfer (BANK_TRANSFER)
{
"deal_id": "7ac148fe-19a3-45bb-b992-019fac55b721",
"hash": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
"status": "ESCROW_LOCKED",
"amount": 150000,
"currency": "TRY",
"payment_method_name": "Havale — Bank Ziraat",
"expires_at": 1783859720,
"brand_name": "Aurora Store",
"primary_color": "#7c3aed",
"logo_url": "https://cdn.aurora-store.example/logo.svg",
"theme": "dark",
"success_redirect_url": "https://aurora-store.example/deposit/success",
"fail_redirect_url": "https://aurora-store.example/deposit/failed",
"card_number": "",
"holder_name": "Ahmet Yilmaz",
"phone_number": "",
"iban": "TR32 0006 2000 1234 5678 9012 34"
}The payment detail type (BANK_CARD, SBP, E_WALLET, SIM,
BANK_TRANSFER) is not passed in CheckoutView directly — it is
unambiguously derived from which of the card_number / phone_number /
iban fields is filled. The type reference is in
Requisite types.