CentaPay

Last updated

Accept wallets#

Take Apple Pay and Google Pay through the same server-to-server endpoint you already use, without card data touching your server.

Availability. CentaPay is pre-launch. Card acceptance in Uzbekistan runs on HUMO and UZCARD, with production expected in Q4 2026 and sandbox access ahead of it. Coverage differs by market, and Kazakhstan and Pakistan launch on other payment methods rather than cards. The coverage table on our main site is the single source for what is live where, and for current dates. Nothing in this guide should be read as a service available today.

A wallet payment is a SALE with two extra fields and all the card fields removed. The wallet supplies an encrypted payment token, you pass it through, and the platform does the rest.

Every example below uses the same sample credentials so the hashes are reproducible: payer_email of [email protected] and a PASSWORD of SANDBOX_PASSWORD. Payload values are illustrative, but every field name, its presence or absence, and every hash is exact.

What changes, and what does not#

Three things change against a card sale.

You send digital_wallet, either applepay or googlepay. You send payment_token, the wallet's own token. And you omit every card field.

The signature changes too, and this is where wallet integrations usually fail.

Formula 8 is the whole signature#

A wallet SALE signs with Formula 8, which is email and password only.

md5(strtoupper(
  strrev(email) . PASSWORD
))

No card fragment, because there is no card. No trans_id, because the transaction does not exist yet. For our sample inputs that is:

5a3ad716b1b70d6e0a8d5f06548cf7ea

Because the card term is gone, this digest is the same for every wallet sale made by the same customer with the same password. That is expected, and it is not a weakness in your integration. Signing a wallet sale with Formula 1 instead produces an authentication failure that points nowhere near the cause.

The request#

POSThttps://{PAYMENT_URL}/post
curl -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=WAL-3001" \
  -d "order_amount=120000" \
  -d "order_currency=UZS" \
  -d "order_description=Order WAL-3001" \
  -d "digital_wallet=googlepay" \
  -d "payment_token={WALLET_PAYMENT_TOKEN}" \
  -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=5a3ad716b1b70d6e0a8d5f06548cf7ea"
$url = 'https://{PAYMENT_URL}/post';

$fields = [
    'action' => 'SALE',
    'client_key' => '{CLIENT_KEY}',
    'order_id' => 'WAL-3001',
    'order_amount' => '120000',
    'order_currency' => 'UZS',
    'order_description' => 'Order WAL-3001',
    'digital_wallet' => 'googlepay',
    'payment_token' => '{WALLET_PAYMENT_TOKEN}',
    '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' => '5a3ad716b1b70d6e0a8d5f06548cf7ea',
];

$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': 'WAL-3001',
    'order_amount': '120000',
    'order_currency': 'UZS',
    'order_description': 'Order WAL-3001',
    'digital_wallet': 'googlepay',
    'payment_token': '{WALLET_PAYMENT_TOKEN}',
    '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': '5a3ad716b1b70d6e0a8d5f06548cf7ea',
}

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': 'WAL-3001',
  'order_amount': '120000',
  'order_currency': 'UZS',
  'order_description': 'Order WAL-3001',
  'digital_wallet': 'googlepay',
  'payment_token': '{WALLET_PAYMENT_TOKEN}',
  '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': '5a3ad716b1b70d6e0a8d5f06548cf7ea',
}

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

For Apple Pay, send digital_wallet=applepay and the full Apple Pay token JSON as payment_token. Everything else is identical.

The callback verifies differently from the request#

The request signs with Formula 8. The callback does not.

Wallet flows verify their callbacks with Formula 2, using the stored card mask, exactly as a card sale does. So a wallet payment involves two different constructions, one outbound and one inbound, and reusing the request digest to verify the callback fails every time.

action=SALE
result=SUCCESS
status=SETTLED
order_id=WAL-3001
trans_id=a7b8c9d0-1e2f-4a3b-8c4d-5e6f7a8b9c0d
trans_date=2026-07-25 17:04:12
descriptor=CENTAPAY WALLET
amount=120000
currency=UZS
card=411111****1111
card_expiration_date=01/2038
digital_wallet=googlepay
pan_type=DPAN
hash=9a047925bcf958b5f1fe68ae28056bd6

Wallet callbacks carry two fields a card sale does not. digital_wallet names the provider, and pan_type is DPAN or FPAN, present on wallet transactions only.

The two digests on this page come from the same customer and password and differ entirely, because Formula 8 and Formula 2 are different constructions. If yours match, you have used one of them twice.

Virtual flow and card flow#

By default, wallet payments are classified as virtual. Card details are not stored, and DMS and recurring creation are limited.

To enable card flow, where the token is decrypted and card data stored for recurring use, set up the Processing Private Key in the admin panel and verify provider support.

Choose before you build. A recurring product on virtual-flow wallets will not behave as you expect, and the switch is an account configuration rather than a code change.

Apple Pay setup#

In your Apple Developer account:

  • Create a Merchant ID in Certificates, Identifiers and Profiles
  • Register and verify all payment domains
  • Create a Merchant Identity Certificate, generating *.csr and *.key, uploading the CSR and downloading the *.pem

Then configure it under Merchants, Wallets, Apple Pay in the admin panel.

The client-side flow is Apple's: check availability with ApplePaySession.canMakePayments(), show the button per Apple's UX guidelines, validate merchant identity through a server-side validation session, then create the payment request and send the resulting token to your server.

Google Pay setup#

Review the Google Pay Web or Android documentation, complete the integration checklist and branding requirements, verify domains in Google Business Console, and adhere to the Google Pay APIs Acceptable Use Policy and Terms of Service.

Request PaymentData with these parameters:

  • allowPaymentMethods: CARD
  • tokenizationSpecification: { "type": "PAYMENT_GATEWAY" }
  • allowedCardNetworks: {ENABLED_CARD_NETWORKS} - the networks enabled on your account, confirmed during onboarding
  • allowedCardAuthMethods: ['PAN_ONLY', 'CRYPTOGRAM_3DS']
  • gateway = value from your CentaPay account manager
  • gatewayMerchantId = your CLIENT_KEY

The Environment setting must match between your Google Pay configuration and your CentaPay account, TEST or PRODUCTION.

Set allowedCardNetworks to what your account can actually acquire

This array populates the Google Pay payment sheet, so any network you list is offered to the cardholder. If they choose one your account cannot acquire, Google returns a valid token, the SALE declines, and the customer has already been told they paid.

It is the one parameter on this page where a copied example is actively dangerous. Take the value from onboarding and list nothing beyond it.

PAN_ONLY moves 3DS responsibility

For the PAN_ONLY authentication method, 3D Secure responsibility transfers to the acquirer. Confirm your acquirer supports this before enabling it.

What next#

Technical questions go to [email protected].