Take a payment#
Accept a card payment from a customer in Uzbekistan.
This covers the one-step sale, the two-step authorise and capture, saving a card for later, and routing to a sub-merchant. The 3D Secure redirect has its own guide, because most of your live traffic will go through it and it deserves the space.
Every example below uses the same sample credentials so the hashes are reproducible: payer_email of [email protected], a PASSWORD of SANDBOX_PASSWORD, and the sandbox test card. Payload values are illustrative, but every field name, its presence or absence, and every hash is exact. You can recompute any digest on this page from the inputs shown.
The shape of a payment#
Three things happen, and only the third is authoritative.
You POST a signed request. You receive a synchronous JSON response telling you what happened at that instant. Some time later you receive a callback telling you what actually happened. The synchronous response can say SUCCESS and the payment can still fail, and it can say UNDEFINED and the payment can still succeed. Fulfil on the callback.
A single-step sale#
SALE authorises and captures in one operation. Use it when you ship or deliver immediately.
Request
https://{PAYMENT_URL}/postcurl -X POST https://{PAYMENT_URL}/post \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "action=SALE" \
-d "client_key={CLIENT_KEY}" \
-d "order_id=TAP-1001" \
-d "order_amount=120000" \
-d "order_currency=UZS" \
-d "order_description=Order TAP-1001" \
-d "card_number=4111111111111111" \
-d "card_exp_month=01" \
-d "card_exp_year=2038" \
-d "card_cvv2=123" \
-d "payer_first_name=John" \
-d "payer_last_name=Smith" \
-d "[email protected]" \
-d "payer_phone=998901234567" \
-d "payer_country=UZ" \
-d "payer_city=Tashkent" \
-d "payer_address=5 Amir Temur Ave" \
-d "payer_zip=100000" \
-d "payer_ip=203.0.113.10" \
-d "term_url_3ds=https://yoursite.example/3ds-return" \
-d "hash=c8b58f1a6a6083fd4f0bd17d3ef58a45"$url = 'https://{PAYMENT_URL}/post';
$fields = [
'action' => 'SALE',
'client_key' => '{CLIENT_KEY}',
'order_id' => 'TAP-1001',
'order_amount' => '120000',
'order_currency' => 'UZS',
'order_description' => 'Order TAP-1001',
'card_number' => '4111111111111111',
'card_exp_month' => '01',
'card_exp_year' => '2038',
'card_cvv2' => '123',
'payer_first_name' => 'John',
'payer_last_name' => 'Smith',
'payer_email' => '[email protected]',
'payer_phone' => '998901234567',
'payer_country' => 'UZ',
'payer_city' => 'Tashkent',
'payer_address' => '5 Amir Temur Ave',
'payer_zip' => '100000',
'payer_ip' => '203.0.113.10',
'term_url_3ds' => 'https://yoursite.example/3ds-return',
'hash' => 'c8b58f1a6a6083fd4f0bd17d3ef58a45',
];
$body = http_build_query($fields);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);import requests
from urllib.parse import urlencode
url = 'https://{PAYMENT_URL}/post'
fields = {
'action': 'SALE',
'client_key': '{CLIENT_KEY}',
'order_id': 'TAP-1001',
'order_amount': '120000',
'order_currency': 'UZS',
'order_description': 'Order TAP-1001',
'card_number': '4111111111111111',
'card_exp_month': '01',
'card_exp_year': '2038',
'card_cvv2': '123',
'payer_first_name': 'John',
'payer_last_name': 'Smith',
'payer_email': '[email protected]',
'payer_phone': '998901234567',
'payer_country': 'UZ',
'payer_city': 'Tashkent',
'payer_address': '5 Amir Temur Ave',
'payer_zip': '100000',
'payer_ip': '203.0.113.10',
'term_url_3ds': 'https://yoursite.example/3ds-return',
'hash': 'c8b58f1a6a6083fd4f0bd17d3ef58a45',
}
body = urlencode(fields)
response = requests.post(
url,
data=body,
headers={'Content-Type': 'application/x-www-form-urlencoded'},
)
result = response.json()const url = 'https://{PAYMENT_URL}/post'
const fields = {
'action': 'SALE',
'client_key': '{CLIENT_KEY}',
'order_id': 'TAP-1001',
'order_amount': '120000',
'order_currency': 'UZS',
'order_description': 'Order TAP-1001',
'card_number': '4111111111111111',
'card_exp_month': '01',
'card_exp_year': '2038',
'card_cvv2': '123',
'payer_first_name': 'John',
'payer_last_name': 'Smith',
'payer_email': '[email protected]',
'payer_phone': '998901234567',
'payer_country': 'UZ',
'payer_city': 'Tashkent',
'payer_address': '5 Amir Temur Ave',
'payer_zip': '100000',
'payer_ip': '203.0.113.10',
'term_url_3ds': 'https://yoursite.example/3ds-return',
'hash': 'c8b58f1a6a6083fd4f0bd17d3ef58a45',
}
const body = new URLSearchParams(fields).toString()
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
})
const result = await response.json()Signed with Formula 1, over payer_email, PASSWORD and the card's first six and last four digits joined then reversed. The hash does not depend on the amount, the order or the currency, so the same card and customer produce the same signature on every request. That is expected.
payer_ip is the cardholder's address, not your server's. term_url_3ds is mandatory even here, where no redirect occurs, because the platform does not know in advance whether the issuer will challenge.
The four synchronous responses
A SALE returns one of four shapes. Handle all of them. Treating anything other than SUCCESS as a failure will cost you real payments, because UNDEFINED frequently settles.
Success. Authorised and captured.
{
"action": "SALE",
"result": "SUCCESS",
"status": "SETTLED",
"order_id": "TAP-1001",
"trans_id": "c3d4e5f6-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
"trans_date": "2026-07-25 14:32:07",
"descriptor": "CENTAPAY TAP",
"amount": "120000",
"currency": "UZS"
}status can also be PENDING or PREPARE on a success, and PENDING appears here only when you sent auth=Y.
Redirect. The issuer wants to authenticate the cardholder. redirect_url, redirect_method and redirect_params are present, and redirect_params may be empty or absent depending on the acquirer. Covered in the 3D Secure guide.
Declined. result and status are both DECLINED, and decline_reason carries a human-readable explanation. Note what is missing: a declined response has no card field, which matters when you come to verify the callback.
Undefined. The outcome is not yet known.
{
"action": "SALE",
"result": "UNDEFINED",
"status": "PREPARE",
"order_id": "TAP-1001",
"trans_id": "c3d4e5f6-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
"trans_date": "2026-07-25 14:32:07",
"descriptor": "CENTAPAY TAP",
"amount": "120000",
"currency": "UZS"
}This is not an error. Show the customer a pending state and wait for the callback.
The callback
action=SALE result=SUCCESS status=SETTLED order_id=TAP-1001 trans_id=c3d4e5f6-7a8b-4c9d-8e0f-1a2b3c4d5e6f trans_date=2026-07-25 14:32:09 descriptor=CENTAPAY TAP amount=120000 currency=UZS card=411111****1111 card_expiration_date=01/2038 hash=d8a83f26a4462460a50aefff0927469b
Verify with Formula 2, over payer_email, PASSWORD, trans_id and the card's first six and last four. The masked PAN behaves exactly like the full one, since only the first six and last four characters are used and the asterisks are never touched.
Two things trip people up. The payer's email is not in the callback, so store it against your order_id when you create the payment. And trans_id is uppercased along with everything else before hashing, which a lowercase UUID makes easy to forget.
Return the plain string OK once you have accepted the notification, or ERROR if you have not. Acknowledge first and process afterwards, because five timeouts within five minutes block your callback URL for fifteen minutes and every merchant sharing that URL stops receiving notifications with you.
If your account has extended data enabled, the success callback also carries rrn, approval_code, issuer_country, issuer_bank, arn and related acquirer fields. They are useful for reconciliation and for disputes. They are configured per account, so do not depend on them being present unless you have confirmed they are switched on.
Amounts and currencies#
| Currency | Code | Format | Example |
|---|---|---|---|
| Kazakhstani Tenge | KZT | Integer, no decimal component | 5000 |
| Uzbekistani Som | UZS | Integer, no decimal component | 120000 |
| US Dollar | USD | Float, XX.XX | 49.99 |
Sending 50.00 in UZS is a formatting error, not a fifty-som payment. Both som and tenge are zero-exponent currencies, so the integer is the whole amount.
Where currency conversion is applied, the callback carries exchange_rate, exchange_currency and exchange_amount, and exchange_rate_base as well if the conversion was doubled. Store them. Reconciling a converted payment without the rate that was actually used is guesswork.
Authorise now, capture later#
Send auth=Y on the SALE to reserve funds without taking them. Everything else about the request is identical, including the signature.
https://{PAYMENT_URL}/postcurl -X POST https://{PAYMENT_URL}/post \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "action=SALE" \
-d "client_key={CLIENT_KEY}" \
-d "order_id=TAP-1002" \
-d "order_amount=120000" \
-d "order_currency=UZS" \
-d "order_description=Order TAP-1002" \
-d "auth=Y" \
-d "card_number=4111111111111111" \
-d "card_exp_month=01" \
-d "card_exp_year=2038" \
-d "card_cvv2=123" \
-d "payer_first_name=John" \
-d "payer_last_name=Smith" \
-d "[email protected]" \
-d "payer_phone=998901234567" \
-d "payer_country=UZ" \
-d "payer_city=Tashkent" \
-d "payer_address=5 Amir Temur Ave" \
-d "payer_zip=100000" \
-d "payer_ip=203.0.113.10" \
-d "term_url_3ds=https://yoursite.example/3ds-return" \
-d "hash=c8b58f1a6a6083fd4f0bd17d3ef58a45"$url = 'https://{PAYMENT_URL}/post';
$fields = [
'action' => 'SALE',
'client_key' => '{CLIENT_KEY}',
'order_id' => 'TAP-1002',
'order_amount' => '120000',
'order_currency' => 'UZS',
'order_description' => 'Order TAP-1002',
'auth' => 'Y',
'card_number' => '4111111111111111',
'card_exp_month' => '01',
'card_exp_year' => '2038',
'card_cvv2' => '123',
'payer_first_name' => 'John',
'payer_last_name' => 'Smith',
'payer_email' => '[email protected]',
'payer_phone' => '998901234567',
'payer_country' => 'UZ',
'payer_city' => 'Tashkent',
'payer_address' => '5 Amir Temur Ave',
'payer_zip' => '100000',
'payer_ip' => '203.0.113.10',
'term_url_3ds' => 'https://yoursite.example/3ds-return',
'hash' => 'c8b58f1a6a6083fd4f0bd17d3ef58a45',
];
$body = http_build_query($fields);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);import requests
from urllib.parse import urlencode
url = 'https://{PAYMENT_URL}/post'
fields = {
'action': 'SALE',
'client_key': '{CLIENT_KEY}',
'order_id': 'TAP-1002',
'order_amount': '120000',
'order_currency': 'UZS',
'order_description': 'Order TAP-1002',
'auth': 'Y',
'card_number': '4111111111111111',
'card_exp_month': '01',
'card_exp_year': '2038',
'card_cvv2': '123',
'payer_first_name': 'John',
'payer_last_name': 'Smith',
'payer_email': '[email protected]',
'payer_phone': '998901234567',
'payer_country': 'UZ',
'payer_city': 'Tashkent',
'payer_address': '5 Amir Temur Ave',
'payer_zip': '100000',
'payer_ip': '203.0.113.10',
'term_url_3ds': 'https://yoursite.example/3ds-return',
'hash': 'c8b58f1a6a6083fd4f0bd17d3ef58a45',
}
body = urlencode(fields)
response = requests.post(
url,
data=body,
headers={'Content-Type': 'application/x-www-form-urlencoded'},
)
result = response.json()const url = 'https://{PAYMENT_URL}/post'
const fields = {
'action': 'SALE',
'client_key': '{CLIENT_KEY}',
'order_id': 'TAP-1002',
'order_amount': '120000',
'order_currency': 'UZS',
'order_description': 'Order TAP-1002',
'auth': 'Y',
'card_number': '4111111111111111',
'card_exp_month': '01',
'card_exp_year': '2038',
'card_cvv2': '123',
'payer_first_name': 'John',
'payer_last_name': 'Smith',
'payer_email': '[email protected]',
'payer_phone': '998901234567',
'payer_country': 'UZ',
'payer_city': 'Tashkent',
'payer_address': '5 Amir Temur Ave',
'payer_zip': '100000',
'payer_ip': '203.0.113.10',
'term_url_3ds': 'https://yoursite.example/3ds-return',
'hash': 'c8b58f1a6a6083fd4f0bd17d3ef58a45',
}
const body = new URLSearchParams(fields).toString()
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
})
const result = await response.json()A successful authorisation returns status: PENDING rather than SETTLED. Keep the trans_id.
Then capture. The capture request needs only the transaction, not the card.
https://{PAYMENT_URL}/postcurl -X POST https://{PAYMENT_URL}/post \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "action=CAPTURE" \
-d "client_key={CLIENT_KEY}" \
-d "trans_id=d4e5f6a7-8b9c-4d0e-9f1a-2b3c4d5e6f70" \
-d "hash=d3f9629e3a779b3052830e9dfacf3241"$url = 'https://{PAYMENT_URL}/post';
$fields = [
'action' => 'CAPTURE',
'client_key' => '{CLIENT_KEY}',
'trans_id' => 'd4e5f6a7-8b9c-4d0e-9f1a-2b3c4d5e6f70',
'hash' => 'd3f9629e3a779b3052830e9dfacf3241',
];
$body = http_build_query($fields);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);import requests
from urllib.parse import urlencode
url = 'https://{PAYMENT_URL}/post'
fields = {
'action': 'CAPTURE',
'client_key': '{CLIENT_KEY}',
'trans_id': 'd4e5f6a7-8b9c-4d0e-9f1a-2b3c4d5e6f70',
'hash': 'd3f9629e3a779b3052830e9dfacf3241',
}
body = urlencode(fields)
response = requests.post(
url,
data=body,
headers={'Content-Type': 'application/x-www-form-urlencoded'},
)
result = response.json()const url = 'https://{PAYMENT_URL}/post'
const fields = {
'action': 'CAPTURE',
'client_key': '{CLIENT_KEY}',
'trans_id': 'd4e5f6a7-8b9c-4d0e-9f1a-2b3c4d5e6f70',
'hash': 'd3f9629e3a779b3052830e9dfacf3241',
}
const body = new URLSearchParams(fields).toString()
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
})
const result = await response.json()Omit amount to capture the full authorised sum. Include it to capture less. One partial capture is allowed, so if you capture 70,000 of a 120,000 authorisation the remaining 50,000 is gone, not available for a second capture.
The capture is signed with Formula 2, which needs the card's first six and last four even though you are not sending the card. Use the values from your own record of the original request.
There is a property here worth knowing before it confuses you. The Formula 2 digest that signs the capture request is byte-identical to the Formula 2 digest you use to verify the capture callback, because both hash the same email, password, trans_id and card fragment. Seeing the same hash go out and come back is correct, not a replay.
action=CAPTURE result=SUCCESS status=SETTLED order_id=TAP-1002 trans_id=d4e5f6a7-8b9c-4d0e-9f1a-2b3c4d5e6f70 trans_date=2026-07-25 15:04:11 descriptor=CENTAPAY TAP amount=120000 currency=UZS hash=d3f9629e3a779b3052830e9dfacf3241
A declined capture returns status: PENDING, not DECLINED, with a decline_reason. That is unusual and worth handling explicitly rather than falling through to a generic error branch.
Saving a card#
Add req_token=Y to a SALE. The response and the callback return a 64-character card_token.
-d "req_token=Y"
For later charges, send card_token in place of card_number, card_exp_month, card_exp_year and card_cvv2.
https://{PAYMENT_URL}/postcurl -X POST https://{PAYMENT_URL}/post \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "action=SALE" \
-d "client_key={CLIENT_KEY}" \
-d "order_id=TAP-1003" \
-d "order_amount=120000" \
-d "order_currency=UZS" \
-d "order_description=Order TAP-1003" \
-d "card_token=7f3a9c1e5b2d84670a1c3e5f7b9d02468ace13579bdf02468ace13579bdf0246" \
-d "payer_first_name=John" \
-d "payer_last_name=Smith" \
-d "[email protected]" \
-d "payer_phone=998901234567" \
-d "payer_country=UZ" \
-d "payer_city=Tashkent" \
-d "payer_address=5 Amir Temur Ave" \
-d "payer_zip=100000" \
-d "payer_ip=203.0.113.10" \
-d "term_url_3ds=https://yoursite.example/3ds-return" \
-d "hash=b803e9b0f8c69a391be0fe34b77eb0ca"$url = 'https://{PAYMENT_URL}/post';
$fields = [
'action' => 'SALE',
'client_key' => '{CLIENT_KEY}',
'order_id' => 'TAP-1003',
'order_amount' => '120000',
'order_currency' => 'UZS',
'order_description' => 'Order TAP-1003',
'card_token' => '7f3a9c1e5b2d84670a1c3e5f7b9d02468ace13579bdf02468ace13579bdf0246',
'payer_first_name' => 'John',
'payer_last_name' => 'Smith',
'payer_email' => '[email protected]',
'payer_phone' => '998901234567',
'payer_country' => 'UZ',
'payer_city' => 'Tashkent',
'payer_address' => '5 Amir Temur Ave',
'payer_zip' => '100000',
'payer_ip' => '203.0.113.10',
'term_url_3ds' => 'https://yoursite.example/3ds-return',
'hash' => 'b803e9b0f8c69a391be0fe34b77eb0ca',
];
$body = http_build_query($fields);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);import requests
from urllib.parse import urlencode
url = 'https://{PAYMENT_URL}/post'
fields = {
'action': 'SALE',
'client_key': '{CLIENT_KEY}',
'order_id': 'TAP-1003',
'order_amount': '120000',
'order_currency': 'UZS',
'order_description': 'Order TAP-1003',
'card_token': '7f3a9c1e5b2d84670a1c3e5f7b9d02468ace13579bdf02468ace13579bdf0246',
'payer_first_name': 'John',
'payer_last_name': 'Smith',
'payer_email': '[email protected]',
'payer_phone': '998901234567',
'payer_country': 'UZ',
'payer_city': 'Tashkent',
'payer_address': '5 Amir Temur Ave',
'payer_zip': '100000',
'payer_ip': '203.0.113.10',
'term_url_3ds': 'https://yoursite.example/3ds-return',
'hash': 'b803e9b0f8c69a391be0fe34b77eb0ca',
}
body = urlencode(fields)
response = requests.post(
url,
data=body,
headers={'Content-Type': 'application/x-www-form-urlencoded'},
)
result = response.json()const url = 'https://{PAYMENT_URL}/post'
const fields = {
'action': 'SALE',
'client_key': '{CLIENT_KEY}',
'order_id': 'TAP-1003',
'order_amount': '120000',
'order_currency': 'UZS',
'order_description': 'Order TAP-1003',
'card_token': '7f3a9c1e5b2d84670a1c3e5f7b9d02468ace13579bdf02468ace13579bdf0246',
'payer_first_name': 'John',
'payer_last_name': 'Smith',
'payer_email': '[email protected]',
'payer_phone': '998901234567',
'payer_country': 'UZ',
'payer_city': 'Tashkent',
'payer_address': '5 Amir Temur Ave',
'payer_zip': '100000',
'payer_ip': '203.0.113.10',
'term_url_3ds': 'https://yoursite.example/3ds-return',
'hash': 'b803e9b0f8c69a391be0fe34b77eb0ca',
}
const body = new URLSearchParams(fields).toString()
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
})
const result = await response.json()The signature changes. A token sale uses the Formula 1 variant, over payer_email, PASSWORD and the reversed token, with no card fragment. Signing a token sale with the card formula is the single most common integration failure at this step, and it produces an authentication error rather than anything that points at the cause.
The precedence rules are unforgiving and silent. If you send both card_token and card data, the token is ignored. If you send both req_token and card_token, req_token is ignored. Nothing warns you.
Tokens are for merchant-initiated charging on your own schedule. If you want the platform to run the schedule, that is RECURRING_SALE, in the recurring guide.
Routing to a sub-merchant#
If you are a PSP or a platform, send channel_id to attribute a transaction to one of your sub-merchants. Maximum sixteen characters.
-d "channel_id=SUBM-0042"
It is echoed in the callback and available in reporting, so it is what you reconcile on at sub-merchant level. Set it on every transaction from the start. Backfilling attribution after the fact is not possible.
Idempotency: order IDs, timeouts and duplicates#
order_id must be unique for every payment you create. Reusing one returns error code 400.
If a request times out or the connection drops, do not resend it. Call GET_TRANS_STATUS_BY_ORDER with your order_id and read the actual state. Blind resubmission creates duplicate orders, and where cascading is enabled a single payment request can already have generated several underlying transactions, so a retry can multiply rather than repeat.
What next#
Handle 3D Secure
covers the redirect, which is where most of your live traffic goes.
Refund and reverse
covers CREDITVOID and VOID.
Bill recurring
covers RECURRING_SALE and schedules.
The API reference
lists every parameter of every action, and operations covers callback delivery, reconciliation and the go-live checklist.
Technical questions go to [email protected].