# CentaPay - full text Every canonical page of www.centapay.com as plain text, generated from the built site so it cannot disagree with the pages it reproduces. Navigation and footer chrome are omitted because they repeat on every route. This file is also published in parts, for fetching one area rather than the whole site: https://www.centapay.com/llms-marketing.txt, https://www.centapay.com/llms-reference.txt, https://www.centapay.com/llms-guides.txt, https://www.centapay.com/llms-operations.txt. Each part carries the same blocks as this file, byte for byte. CentaPay Ltd, AIFC Kazakhstan, BIN 260740900699. CentaPay is completing its final authorisation work with the Astana Financial Services Authority. We are not yet accepting clients or processing transactions. Services will be available from Q4 2026. --- ## Home Source: https://www.centapay.com/ Payments Infrastructure for Central Asia. Local payments for international businesses and PSPs. Reliable technology with compliance and security at its heart. Get in touch View API docs → 5 Countries by H1 2027 99.95% Uptime SLO T+1 Settlement Cycle 1 Integration English Common LawInternational SettlementPayFacPCI DSSAIFC IncorporatedFXLocal Payment MethodsCross-Border OnlyAIFC-IncorporatedEnglish Common LawInternational SettlementPayFacPCI DSSAIFC IncorporatedFXLocal Payment MethodsCross-Border OnlyAIFC-Incorporated Who We Serve Built for global enterprises & PSPs. CentaPay is built for international businesses and PSPs needing regulated, direct access to Central Asian consumers. All flows are cross-border by design. Direct Merchants International businesses connecting directly to Central Asian acquiring. Your own dedicated setup, clear reporting, and cross-border settlement in your preferred currency. One integration - one relationship - one point of accountability. PSPs & Payment Platforms Licensed PSPs adding Central Asian acquiring to their offering. Per-merchant MID visibility maintained throughout. Settlement aggregated to your nominated account with full sub-merchant reporting. Add Central Asia to your routing with a single API integration. Coverage Central Asia. Where we operate. Incorporated in the AIFC and operating from Astana, we will serve clients globally. Kazakhstan, Uzbekistan and Pakistan go live together, with further markets to follow. SandboxSep 2026 ProductionQ4 2026 KazakhstanKZ Kazakhstan KZT · Kazakhstan Tenge Cross-border only: flows from Kazakhstan to you, or disbursements from you into Kazakhstan. Payment methods QR bank transfer Bank-rail collection, settled cross-border in your preferred currency. Availability SandboxSep 2026 ProductionQ4 2026 All three markets go live on the same two dates. UzbekistanUZ Uzbekistan UZS · Uzbekistani Som Cross-border only: flows from Uzbekistan to you, or disbursements from you into Uzbekistan. Payment methods HUMO · UZCARD The two domestic card schemes, covering the great majority of local cards. Availability SandboxSep 2026 ProductionQ4 2026 All three markets go live on the same two dates. PakistanPK Pakistan PKR · Pakistani Rupee Cross-border only: flows from Pakistan to you, or disbursements from you into Pakistan. Payment methods e-wallets Wallet collection and disbursement, settled cross-border. Availability SandboxSep 2026 ProductionQ4 2026 All three markets go live on the same two dates. We only operate cross-border payments - flows from Central Asia to you, or disbursements from you into Central Asia. One integration, built to add markets without a second one. Planned H1 2027: Kyrgyzstan, Azerbaijan About CentaPay Built with purpose. CentaPay is an AIFC-incorporated fintech building the cross-border payment infrastructure that connects the international digital economy with Central Asia. We will be AFSA-regulated to provide Money Services including acquiring, processing and settlement of payments. We will do one thing well: regulated, reliable cross-border infrastructure for businesses and PSPs that need to operate in a region most global providers cannot reach. 01 AIFC-Incorporated Incorporated in the Astana International Financial Centre. All contracts are English Common Law - a jurisdiction your legal team already understands. 02 Direct Local Acquiring We maintain local banking and acquiring relationships across the region. Your team integrates once with us - we handle everything behind it. 03 Cross-Border Only Exclusively built for international payment flows. This focus lets us optimise for compliance, settlement certainty and operational speed without compromise. 04 Expanding Coverage Kazakhstan, Uzbekistan and Pakistan launching together, with Kyrgyzstan and Azerbaijan planned for H1 2027. One integration, built to add markets without a second one. Ready to expand in Central Asia? Expand with the optimal payments infrastructure. Talk to our team about business models, integration options, compliance and risk controls, commercial terms, and how we can help you enter and expand across Central Asia. Get in touch→ --- ## Leadership team Source: https://www.centapay.com/team Our Team Experts in every discipline. Payments, compliance and financial crime expertise, operating from Astana and the UK. CEO Chief Executive Officer, Co-Founder Sikander Hauser PreviouslyWorldpayAlipayBanking Circle Twenty-two years in payments, eleven of them at Worldpay. Founding member of Alipay EMEA, where he oversaw e-commerce across the region and its expansion into new markets. Later at Banking Circle, then CEO of an FCA-regulated payments acquirer in the UK and of a global payments business. Passionate about technology, product and emerging-market ecosystems. CCO Chief Compliance Officer, Co-Founder Karina Uzukbayeva PreviouslyRevolutOpenPaydBanking Circle Expertise spanning cross-border payments, banking and investments, with compliance leadership at Revolut, OpenPayd and Banking Circle. A specialist in fraud, compliance and risk, she pairs global standards with local reality. A Kazakh national based in London, fluent in English, Russian and Kazakh. GM General Manager Dana Akhmet PreviouslyEYAIX More than ten years in finance and accounting. Built the accounting and finance function of the Astana International Exchange (AIX) from scratch as Finance Director and Chief Accountant, following Ernst & Young, where she served international clients including Microsoft and Cisco. Certified Professional Accountant of Kazakhstan with an MBA from KIMEP University, leading financial control and settlement operations full-time from Astana. MLRO Money Laundering Reporting Officer Madina Assanova PreviouslyKazakhstan FIU Financial crime specialist, formerly Chief Expert at Kazakhstan's Financial Intelligence Unit, leading investigations with banks, law enforcement and international FIUs, most recently MLRO at an AIFC-licensed payments institution. CAMS-certified, with a law degree from MEPhI and state honours for contribution to economic security. Leads AML/CTF compliance full-time from Astana. --- ## Frequently asked questions Source: https://www.centapay.com/faq Questions Frequently asked questions Answers for prospective clients and partner institutions. If your question is not here, write to info@centapay.com. About CentaPay What is CentaPay? CentaPay will operate as a regulated payment facilitator built for cross-border trade into Central Asia. We will give international businesses and licensed payment service providers a single integration to collect payments from consumers in the region using the local methods those consumers actually use, and to send funds back out to beneficiaries there. Every flow we handle is cross-border by design. Link to this answer What is CentaPay's role in the payment chain? CentaPay is the acquiring bank's direct client and single counterparty. We will operate as a regulated payment facilitator: we will contract with international clients, submit their transactions, and take responsibility for the commercial and compliance relationship with each of them. The institution will face one authorised, supervised entity rather than a long list of foreign merchants. Link to this answer Who is behind CentaPay? The founding team has spent two decades in cross-border payments and compliance at Worldpay, Alipay, Revolut, OpenPayd, Banking Circle and Ernst & Young, with operational leadership based in Astana. Full profiles are on our team page. Link to this answer Where is CentaPay based? Our registered office is in the Astana International Financial Centre, at 10 Dinmukhamed Qonayev Street, Emerald Tower, 3rd Floor, Esil district, Astana Z05H9A7, Republic of Kazakhstan. Our finance and financial crime functions are based full-time in Astana, with commercial and executive presence in the United Kingdom. Link to this answer Coverage and payment methods Which countries and payment methods do you cover? Kazakhstan with QR bank transfer, Uzbekistan with HUMO and UZCARD, and Pakistan with local e-wallets, with Kyrgyzstan and Azerbaijan planned. Sandbox and production timing for every market is published on the coverage table. One integration, built to add markets without a second one. Link to this answer Which currencies do you support? Payers will transact in their local currency - tenge, som or rupee depending on the market. Settlement to you will be in US dollars as standard. Other settlement currencies can be discussed case by case, depending on your banking arrangements and volumes. Link to this answer Do you support payouts as well as collections? Yes. We will support disbursements into the region, including content creator payments, marketplace seller payouts, refunds and documented business-to-business remittance. We will not support person-to-person transfers. Link to this answer Do you serve businesses inside Kazakhstan? No. We will not onboard Kazakhstan-domiciled merchants, and Kazakhstan-domiciled sub-merchants of a client are equally out of scope. The focus is deliberate: being exclusively cross-border keeps our regulatory position clean, our controls proportionate and our operational attention undivided. Link to this answer Getting started Who can become a client? We will work with licensed payment service providers and enterprise merchants only. All clients will be classified as Professional Clients under AFSA conduct of business rules, and we will not provide services to retail consumers. We will also not onboard businesses domiciled in Kazakhstan, since our model is built for cross-border flows. Link to this answer Which industries do you support? We will serve most legitimate digital commerce, with a defined list of categories we will not accept and a further list we will accept only with enhanced diligence. Both lists are published in full on our prohibited and restricted services page, so you can check your business type before making an enquiry. Link to this answer Can our sub-merchants process through you? Yes. Under our payment facilitator model, a licensed payment service provider can bring its own merchant portfolio to us. Each sub-merchant will receive its own individual merchant identifier - we will not pool, aggregate or blend identifiers - so transaction reporting stays transparent at the sub-merchant level for you, for us and for our local acquiring partners. Each sub-merchant will be subject to pre-approval. The model is built for enterprise portfolios rather than long-tail volume. Link to this answer What do you need from us to onboard? Standard know-your-business diligence: corporate documents, evidence of your own regulatory authorisation, ownership and control structure, details of your compliance framework, and information about the flows you intend to process. If you are bringing sub-merchants, we will also need to review that portfolio. We will give you the full requirement list at first contact rather than drip-feeding it. Link to this answer How does integration work? You will integrate once, server to server, against a single API. Full technical documentation is published on our developer pages, including a machine-readable OpenAPI specification and a markdown version of the guide for AI-assisted integration. A sandbox with test credentials will be available from Sep 2026 before you commit. Client engineering teams typically complete the technical work in two to four weeks, depending on how much of the flow you already have built. Link to this answer Pricing and settlement What does it cost? Pricing depends on your volumes, payment methods, markets and which of our two operating models fits you. We do not publish a rate card, because a number set without those variables would be meaningless. Get in touch and we will quote against your actual flows. Link to this answer How and when are we settled? We aim for settlement on a T+1 basis. Actual timing depends on the payment method, local partner cut-off times and the banking calendars of the markets involved, so the applicable cycle is confirmed for you specifically in your commercial agreement rather than promised generically here. Link to this answer What service levels do you offer? Our platform will operate against a 99.95 per cent uptime objective, with disaster recovery in a second Kazakhstani city. Formal service levels, including support response times and incident handling, are set out in your agreement. Link to this answer Risk, compliance and security Who regulates CentaPay, and how can we verify the licence? CentaPay Ltd is incorporated in the Astana International Financial Centre with Business Identification Number 260740900699. CentaPay is completing its final authorisation work with the Astana Financial Services Authority. We are not yet accepting clients or processing transactions. Services will be available from Q4 2026. Our status is a matter of public record and can be verified directly on the AFSA public register. Link to this answer What law governs your agreements? Our client and partner agreements will be governed by English common law within the AIFC legal framework. For counterparties whose legal teams work in common law jurisdictions, this removes a significant part of the diligence burden that a purely domestic structure would create. Link to this answer Who carries chargeback liability? CentaPay and its clients will be liable for all chargebacks. Link to this answer How are sub-merchants controlled? Every sub-merchant will receive its own individual merchant identifier. We will not pool, aggregate or blend identifiers under a single master, which means transaction activity is attributable at all times. Sub-merchants will be subject to pre-approval and will be restricted to pre-agreed business categories, and our prohibited and restricted category lists are published rather than internal. Link to this answer What does your financial crime programme look like? Our programme is risk-based and proportionate to our model. It covers customer due diligence and enhanced due diligence for higher-risk relationships, automated sanctions, politically exposed person and adverse media screening at onboarding and on an ongoing basis, automated transaction monitoring with escalation to a dedicated Money Laundering Reporting Officer, reporting to the Financial Monitoring Agency where required, staff training, and board-level oversight. Our MLRO is based full-time in Astana and came to us from Kazakhstan's Financial Intelligence Unit. Link to this answer How is client money handled? Client money will be held in segregated accounts in accordance with AFSA Client Money Rules, kept separate from our own operating funds, and reconciled daily. Link to this answer How is payment data handled? Transaction data relating to Kazakhstan-issued payment instruments will be stored in Kazakhstan in accordance with Law No. 94-V on Personal Data and Its Protection. Card data will be handled within PCI DSS certified infrastructure, and our data protection obligations sit under the AIFC Data Protection Regulations. Link to this answer How do we get in touch? Write to info@centapay.com. Tell us whether you are a payment service provider, an enterprise merchant or a financial institution, and roughly what volumes and markets you have in mind, and we will route you to the right person. Link to this answer If your question is not here, write to info@centapay.com. --- ## Compliance and regulation Source: https://www.centapay.com/compliance Compliance Centre Statements & Policies. CentaPay is completing its final authorisation work with the Astana Financial Services Authority. We are not yet accepting clients or processing transactions. Services will be available from Q4 2026. Our legal and risk documentation governs how we will operate, who we will serve, how we protect data, and how we prevent financial crime. AFSA AIFC Incorporated AIFC Incorporated B2B Only · No Retail 01Legal Legal Policies Governs data protection, website cookies, and permitted platform use. Issued under AIFC Data Protection Regulations 2024 and English Common Law. Our legal policies define how CentaPay processes personal data, operates its website, and sets the conditions under which our payment infrastructure may be used. All policies are reviewed regularly and kept current with applicable regulation. Privacy Policy How we collect, use, store, and protect personal data in connection with our payment services and website. Covers data localisation, legal basis for processing, and your rights as a data subject. AIFC Data Protection 2024KZ Law No. 94-VB2B OnlyNo Data Sales → Cookies Policy Our use of cookies on centapay.com and associated subdomains, including what we collect and how to manage your preferences. WebsiteCookiescentapay.com → Acceptable Use Policy Defines what CentaPay's payment infrastructure may and may not be used for. All clients are contractually required to comply with these terms as a condition of onboarding. PlatformPermitted UseRestrictions → 02Risk Risk Documents Governs financial crime prevention and merchant eligibility. Risk-based approach aligned with AFSA rules and FATF recommendations. Our risk documents define how CentaPay prevents money laundering and terrorist financing, and the categories of business we do not accept. All clients are classified as Professional Clients under AFSA conduct of business rules - we will not provide services to retail consumers. AML Statement Our anti-money laundering and counter-terrorist financing programme. Covers customer due diligence, ongoing transaction monitoring, sanctions compliance, and board-level governance arrangements. AFSA AML/CTF RulesFATF AlignedRisk-Based ApproachProfessional Clients → Prohibited Services Merchant categories and service types that CentaPay does not support under any circumstances. Prospective clients must review this document before submitting an onboarding enquiry. Prohibited CategoriesMerchant EligibilityPre-Onboarding → --- ## Privacy Source: https://www.centapay.com/privacy 1 Who we are CentaPay Ltd ("CentaPay", "we", "us", or "our") is a company incorporated in the Astana International Financial Centre (AIFC), Kazakhstan, and is completing its final authorisation work with the Astana Financial Services Authority (AFSA). Our registered office is in Astana, Kazakhstan, and we maintain an operational presence in London, United Kingdom. B2B only. CentaPay provides cross-border payment infrastructure to international businesses and payment service providers. We do not provide services to retail consumers. 2 Scope of this policy This Privacy Policy explains how we collect, use, store, and protect personal data in connection with: our website at centapay.com and any associated subdomains; our payment processing and settlement services; and our business relationships with clients, partners, and prospective clients. This policy is issued in accordance with the AIFC Data Protection Regulations 2024 and, where applicable, Kazakhstan Law No. 94-V "On Personal Data and Its Protection". 3 Data we collect 3.1 Website visitors When you visit our website, we may collect: technical data such as IP address, browser type, device type, and operating system; usage data such as pages viewed, time on site, and referring URL; and any information you submit voluntarily through our contact form. 3.2 Client and partner data In the course of onboarding and servicing our clients, we collect personal data relating to directors, beneficial owners, authorised representatives, and key personnel. This includes: full name, date of birth, and nationality; government-issued identification documents; proof of address documentation; professional role and contact details; and information required to satisfy our regulatory obligations, including source of funds and source of wealth where applicable. 3.3 Transaction data We process transaction data in connection with the payment services we provide. This may include payment instrument details, transaction amounts, merchant identifiers, and payment outcomes. 4 How we use personal data to provide, operate, and improve our payment services; to comply with our regulatory obligations, including customer due diligence and transaction monitoring; to onboard and manage client relationships; to communicate with prospective clients who contact us through our website; to maintain the security and integrity of our systems; and to comply with applicable law, regulation, or lawful request from a competent authority. 5 Legal basis for processing Legal obligation Where processing is required to comply with AFSA rules, AIFC regulations, or applicable law. Contractual necessity Where processing is necessary to perform or enter into a contract with our client. Legitimate interest Where necessary for our legitimate business interests, such as fraud prevention, provided not overridden by the data subject's rights. Consent Where you have provided your consent, for example by submitting a contact form. 6 Data storage and transfers CentaPay stores personal data in accordance with applicable data localisation requirements. Where Kazakhstan law requires personal data to be stored locally, we comply with those requirements. Some personal data may be processed by third-party service providers located outside Kazakhstan. Where such transfers occur, we ensure appropriate safeguards are in place. 7 Data sharing We may share personal data with: our acquiring bank partners, as required to process transactions; card schemes (Visa, Mastercard) in accordance with scheme rules; our technology and infrastructure providers; professional advisers, auditors, and legal counsel; regulatory and law enforcement authorities where required by law; and any other party where we are legally required to do so. We do not sell personal data to third parties. 8 Data retention We retain personal data for as long as necessary to fulfil the purposes for which it was collected, including to satisfy legal, regulatory, and contractual requirements. Data typeRetention period Client and compliance recordsAs required by applicable regulation Transaction recordsAs required by applicable regulation Website contact form submissions12 months, unless a business relationship is established 9 Your rights Depending on the applicable data protection framework, you may have the right to: request access to the personal data we hold about you; request correction of inaccurate or incomplete data; request deletion of your data, subject to our legal retention obligations; object to or request restriction of processing in certain circumstances; and withdraw consent where processing is based on consent. To exercise any of these rights, please contact us at privacy@centapay.com. 10 Security We implement appropriate technical and organisational measures to protect personal data against unauthorised access, loss, destruction, or alteration. These include encryption, access controls, and regular security assessments. 11 Changes to this policy We may update this policy from time to time. Where changes are material, we will update the "Last updated" date at the top of this page. 12 Contact Questions about this policy or our data practices? CentaPay Ltd · AIFC, Astana, Kazakhstan privacy@centapay.com → --- ## Cookies Source: https://www.centapay.com/cookies 1 What are cookies Cookies are small text files placed on your device when you visit a website. They are widely used to make websites function, improve performance, and provide information to the website operator. This policy explains how CentaPay Ltd ("CentaPay", "we", "us") uses cookies and similar technologies on centapay.com. 2 How we use cookies 2.1 Essential cookies These cookies are necessary for the website to function and cannot be disabled. Essential Session cookies Maintain your browsing session and enable core site functionality Session Security cookies Protect against cross-site request forgery and support secure browsing Session Cookie consent Store your cookie preferences so we do not ask you repeatedly 12 months 2.2 Analytics cookies These cookies help us understand how visitors interact with our website. This data is aggregated and anonymised. Analytics Google Analytics (_ga, _gid) Measure website traffic, visitor behaviour, and page performance Up to 2 years Wix analytics Platform-level analytics provided by our website hosting provider Up to 2 years 2.3 Functional cookies These cookies enable enhanced functionality and personalisation, such as remembering your preferred language or region. They may be set by us or by third-party providers whose services we have added to our pages. 3 Third-party cookies Our website is hosted on the Wix platform, which may set its own cookies in connection with the operation of the site. Additionally, if you interact with embedded content or links to third-party services, those services may set their own cookies. We do not control cookies set by third parties and recommend reviewing their respective privacy and cookie policies. 4 Managing cookies You can control and manage cookies through your browser settings. Most browsers allow you to: view what cookies are stored and delete them individually or in bulk block cookies from specific or all websites set preferences for first-party and third-party cookies separately Please note that disabling essential cookies may affect the functionality of our website. For instructions on managing cookies, consult the help section of your browser: ChromeFirefoxSafariEdge 5 Do Not Track Some browsers offer a "Do Not Track" (DNT) signal. There is no industry-wide standard for how websites should respond to DNT signals, and our website does not currently respond to them. However, you can manage your cookie preferences using the browser controls described above. 6 Changes to this policy We may update this Cookies Policy from time to time to reflect changes in our practices or in applicable law. The "Last updated" date at the top of this page indicates when the policy was most recently revised. 7 Contact Questions about our use of cookies? CentaPay Ltd · AIFC, Astana, Kazakhstan info@centapay.com → --- ## Acceptable use Source: https://www.centapay.com/acceptable-use 1 Introduction This Acceptable Use Policy ("AUP") sets out the terms governing your use of centapay.com, docs.centapay.com, and any associated subdomains (together, the "Website"), which are operated by CentaPay Ltd ("CentaPay", "we", "us"). By accessing or using the Website, you agree to comply with this policy. If you do not agree with any part of this policy, you should not use the Website. This policy governs use of the Website only. The terms governing CentaPay's payment services are set out separately in the applicable client agreement. 2 Permitted use You may use the Website for the following purposes: browsing information about CentaPay's services, coverage, and company accessing our developer documentation and API reference materials contacting us through the forms or contact details provided any other use that is lawful and consistent with the purpose of the Website 3 Prohibited conduct You must not use the Website in any way that is unlawful, harmful, or inconsistent with its intended purpose. × You must not use the Website for any unlawful purpose or in violation of any applicable law or regulation attempt to gain unauthorised access to any part of the Website, its servers, or any connected systems use automated tools, bots, scrapers, or crawlers to access, index, or extract content, except standard search engine indexing submit false, misleading, or spam content through any contact form or communication channel transmit any material that contains viruses, malware, or other harmful code reproduce, distribute, or republish any content without our prior written consent use the Website in any manner that could damage, disable, or impair its availability impersonate any person or entity, or misrepresent your affiliation with any person or entity 4 Intellectual property All content on the Website, including text, graphics, logos, images, and software, is the property of CentaPay or its licensors and is protected by applicable intellectual property laws. The CentaPay name, logo, and brand marks are trademarks of CentaPay Ltd. Nothing on the Website grants you any licence or right to use any content, trademarks, or other intellectual property without our prior written consent. Our developer documentation at docs.centapay.com is made available for the purpose of evaluating and integrating with our services. It may not be reproduced or redistributed for any other purpose. 5 Third-party links The Website may contain links to third-party websites or resources. These links are provided for convenience only. We do not endorse, control, or accept responsibility for the content, privacy practices, or availability of any third-party website. 6 Disclaimer The Website and its content are provided on an "as is" and "as available" basis. While we make reasonable efforts to keep the information accurate and up to date, we do not warrant that it is complete, accurate, or free from errors. Nothing on the Website constitutes financial, legal, or professional advice. Information about our services is provided for general informational purposes only and does not constitute an offer or solicitation. To the fullest extent permitted by applicable law, CentaPay excludes liability for any loss or damage arising from your use of, or reliance on, the Website or its content. 7 Changes to this policy We may update this policy from time to time. Where changes are material, we will update the "Last updated" date at the top of this page. Continued use of the Website following an update constitutes acceptance of the revised terms. 8 Contact Questions about this policy? CentaPay Ltd · AIFC, Astana, Kazakhstan info@centapay.com → --- ## AML statement Source: https://www.centapay.com/aml 1 Our commitment CentaPay Ltd is incorporated in the Astana International Financial Centre (AIFC) and is completing its final authorisation work with the Astana Financial Services Authority (AFSA). We are committed to preventing the use of our payment infrastructure for money laundering, terrorist financing, sanctions evasion, or any other form of financial crime. We maintain a compliance programme that is proportionate to our business model, the jurisdictions in which we operate, and the nature of the payment flows we process. 2 Regulatory framework Our AML/CTF programme is designed to comply with: the AFSA Anti-Money Laundering, Counter-Terrorist Financing and Sanctions Rules; the AIFC General Rules (GEN); applicable provisions of Kazakhstan's AML/CTF legislation; and relevant guidance from the Financial Action Task Force (FATF). 3 Our approach CentaPay applies a risk-based approach to financial crime prevention. Key elements include: Customer due diligence - we identify and verify all clients and their beneficial owners before establishing a business relationship, and apply enhanced measures where we assess a higher level of risk. Ongoing monitoring - we monitor client relationships and transaction activity on an ongoing basis to identify unusual or potentially suspicious patterns. Sanctions compliance - we screen clients and relevant parties against applicable sanctions lists at onboarding and on an ongoing basis. Governance - our compliance programme is overseen by dedicated compliance and reporting functions with direct access to the Board of Directors. Training - all relevant staff receive AML/CTF training at induction and on a recurring basis. Professional Clients only. CentaPay operates exclusively with business clients. All clients will be classified as Professional Clients under AFSA's conduct of business rules. We do not provide services to retail consumers. 4 Cooperation with authorities CentaPay cooperates fully with AFSA, law enforcement, and other competent authorities in connection with their investigations and inquiries. We respond promptly to lawful requests for information and provide any assistance required under applicable law. 5 Contact Questions or concerns about financial crime? CentaPay Ltd · AIFC, Astana, Kazakhstan compliance@centapay.com → --- ## Prohibited and restricted services Source: https://www.centapay.com/restricted 1 Overview CentaPay maintains a list of business types that are either prohibited or restricted from using our payment services. This list reflects our regulatory obligations, acquiring bank requirements, card scheme rules, and internal risk appetite. Prohibited - not supported under any circumstances. Restricted - may be supported subject to enhanced due diligence and specific terms, at our sole discretion. This list is not exhaustive. CentaPay reserves the right to decline any business type where necessary to comply with applicable law, regulation, or contractual obligations. 2 Prohibited categories × Prohibited Businesses domiciled in Kazakhstan, including online platforms, apps, e-commerce businesses, and service providers Kazakhstan-domiciled sub-merchants of any Platform Client, or Kazakhstan-domiciled sellers on any marketplace Client Gambling, gaming, betting, lottery, casino, and skill-based competitions (licensed or unlicensed) Unlicensed financial services, including money transmission, lending, or investment Adult content, escort services, or sexually explicit material Controlled substances, narcotics, or drug paraphernalia Weapons, ammunition, or explosives Counterfeit goods, IP infringement, or pirated content Pyramid schemes, MLM (recruitment-based revenue), or Ponzi schemes Cryptocurrency exchanges, ICOs, or unregulated token sales Shell companies or entities with no identifiable commercial activity Binary options, spread betting, or CFDs Tobacco, e-cigarettes, or vaping products Debt collection agencies Bail bond services Products or services subject to trade sanctions or export controls 3 Prohibited MCCs MCCDescription 5933Pawn shops 5937Antique reproductions 5962Direct marketing - travel (high fraud) 5966Outbound telemarketing 5967Inbound teleservices 5993Cigar stores and stands 6051Non-FI foreign currency / money orders 6211Security brokers and dealers 6532Stored value load 7273Dating and escort services 7295Babysitting services 7800Government-owned lottery 7801Government-licensed online casinos 7802Government-licensed horse/dog racing 7995Gambling - betting, casino chips, wagering 9222Fines 9223Bail and bond payments 4 Restricted categories Accepted subject to enhanced due diligence, additional documentation, and specific terms. At CentaPay's sole discretion. ! Restricted Travel agencies, tour operators, and booking platforms Digital goods, in-app purchases, and virtual currencies (non-crypto) Subscription services with recurring billing Nutraceuticals, dietary supplements, and wellness products Telemedicine and online pharmacy (licensed) Age-restricted goods Precious metals, gemstones, and jewellery Timeshare and vacation ownership Charitable organisations and fundraising platforms Legal services and law firms High-value goods requiring enhanced fraud screening 5 Restricted MCCs MCCDescription 4722Travel agencies and tour operators 4816Computer network / information services 5122Drugs, druggist sundries 5192Books, periodicals, newspapers 5399General merchandise (misc) 5816Digital goods - games 5817Digital goods - applications 5818Digital goods - large merchant 5912Drug stores and pharmacies 5944Jewellery, watches, silverware 5964Direct marketing - catalogue 5968Direct marketing - subscription 5969Direct marketing - other 7012Timeshares 7922Theatrical producers / ticket agencies 8099Health practitioners (NEC) 8398Charitable / social service organisations 6 General prohibitions CentaPay will also not process transactions that: involve sanctioned persons, entities, or jurisdictions; relate to money laundering, terrorist financing, or proceeds of crime; involve goods or services illegal in the buyer's or seller's jurisdiction; do not correspond to a genuine sale of goods or services; or would breach our regulatory or contractual obligations. 7 Review process If your business falls into a restricted category or you are unsure whether your business type is supported, please contact us before submitting an application. This list is reviewed periodically and may be updated to reflect changes in regulatory obligations, acquiring bank requirements, card scheme rules, or risk appetite. 8 Contact Questions about whether your business is supported? CentaPay Ltd · AIFC, Astana, Kazakhstan info@centapay.com → --- ## Developer documentation Source: https://www.centapay.com/docs Last updated 6 August 2026 CentaPay Developer Documentation Everything you need to integrate cross-border payment acceptance and disbursements for Central Asia. Availability. CentaPay is pre-launch. Production is expected in Q4 2026, with sandbox access ahead of it. Coverage differs by market, and the payment methods available differ by market too. The coverage table on our main site is the single source for what is live where, and for current dates. Nothing in this documentation should be read as a service available today. AFSA RegulatedS2S · Server-to-ServerProfessional Clients OnlyPCI DSS Required API guide (Markdown) OpenAPI 3.1 spec AsyncAPI (callbacks) Postman collection Machine-readable formats for AI-assisted integration. The Postman collection carries one request per action and signs each one for you. Fill in PAYMENT_URL, CLIENT_KEY and PASSWORD and send. Start here# Quickstart Check your hash implementation, send a first payment, read the response and verify the callback. Guides# Task-oriented walkthroughs, organised by what you are trying to do. Take a payment SALE, currencies, AUTH and CAPTURE, tokenisation, sub-merchant routing. Handle 3D Secure The redirect flow. Refund and reverse CREDITVOID and VOID. Pay out to a card CREDIT2CARD, plus the CREDIT2VIRTUAL coming-soon note. Bill recurring RECURRING_SALE, RETRY, schedule operations. Accept wallets Apple Pay and Google Pay. Reference and operations# API reference Credentials, hash formulas, every action and its parameters, and callback parameters. Error and decline codes Every code a failed request can return, with its cause and whether a retry can succeed. Testing Test cards and the scenarios their expiry dates trigger, forcing a callback to fail, and producing a chargeback in sandbox. Operations Callbacks, status queries, chargebacks, settlement and reconciliation, and go-live. AI and agents Point an agent at these docs over MCP, or take the whole thing as Markdown. --- ## AI and agents Source: https://www.centapay.com/docs/ai Last updated 6 August 2026 AI and agents# Four ways to give an assistant or an agent this documentation, in order of how little work each one costs you. If you are wiring up a coding agent, start with the MCP server and stop reading. Availability. CentaPay is pre-launch. Production is expected in Q4 2026, with sandbox access ahead of it. Coverage differs by market, and the payment methods available differ by market too. The coverage table on our main site is the single source for what is live where, and for current dates. Nothing in this documentation should be read as a service available today. MCP server# A remote MCP server that answers questions about these pages directly. No downloads, no file handling, and nothing to keep up to date: it reads the published pages, so it changes when they do. Copyhttps://www.centapay.com/mcp Three tools: ToolWhat it does search_docsRanked search across every section. Takes an action name, a parameter, an error code or a plain question. get_pageOne route in full, section by section, with its heading anchors. list_routesEvery route with its title and section count. Claude Code# Copyclaude mcp add --transport http centapay-docs https://www.centapay.com/mcp Cursor# In ~/.cursor/mcp.json for every project, or .cursor/mcp.json for one: Copy{ "mcpServers": { "centapay-docs": { "url": "https://www.centapay.com/mcp" } } } Claude Desktop# Add it as a custom connector in settings, using the same URL. On a build that only speaks stdio, bridge it: Copy{ "mcpServers": { "centapay-docs": { "command": "npx", "args": ["-y", "mcp-remote", "https://www.centapay.com/mcp"] } } } The URL is the only part of this that comes from us. The file locations and the config shape belong to each client, and they change independently of this page. A plain GET of the endpoint returns the server name and its tool list, which is the quickest way to confirm a client is reaching it. The whole site as text# llms.txt is the index: every route with one line on what it is for, so a model can route to the right page from a task description rather than an operation name. It is the file to paste when an assistant needs to know what exists. llms-full.txt is the whole thing: all 30 routes, marketing and documentation, as one plain-text document. It is generated from the published pages, so it cannot disagree with them. Paste it when an assistant needs the content rather than the map. It is also published in four parts, one per area, for fetching a single section rather than the whole site. Some IDE assistants read both automatically when they are present at the site root. Both are, and neither needs anything from you. Markdown for any page# Append .md to any documentation URL for that page as Markdown, with its headings, tables and code samples intact. Copyhttps://www.centapay.com/docs/reference.md https://www.centapay.com/docs/guides/take-a-payment.md Useful when you want one page in a prompt rather than the whole site. The published hash vectors survive the conversion, so a digest copied from a .md file is the digest on the page. OpenAPI and Postman# The OpenAPI specification is what a code generator or an agent uses to construct requests rather than read about them: parameter names, types, whether a field is required, and the shape of each response. It describes the transport. It does not describe which hash formula an action signs with, and that is the part people get wrong, so an agent working from the spec alone will still need the reference. The Postman collection carries one request per action with a pre-request script that computes the correct hash for each, which makes it the fastest way to see a signed request that works. The API guide is the reference and the quickstart as one Markdown file, for handing whole to an assistant. What an agent cannot know# This is the part worth putting in front of a model, because an agent filling these gaps confidently is the failure this page exists to prevent. There is no sandbox host in these pages. CentaPay is pre-launch. Endpoints appear as placeholders such as {PAYMENT_URL} because the real value is issued with your credentials, not published. An agent that invents a hostname has invented it. Credentials are issued by hand. There is no self-service signup and no key endpoint. Anything describing one is wrong. Three questions are still open. Five have been answered since this page was written and are documented on troubleshooting: callback delivery retry, the delivery log, deliberate sandbox triggers, callback timeout testing, and the per-request callback URL parameter. Still outstanding: reconciliation report formats, the definitive status transition list, and the signature for GET_TRANS_STATUS_BY_ORDER, which is escalated and unresolved. These pages say nothing about any of the three rather than guessing, and a hash construction in particular is never inferred. Availability differs by market and nothing here is live. The coverage table is the single source for what is planned where. If a page does not state something, the honest answer is that it is not documented. Guessing a parameter name or a hash construction costs a failed integration and an afternoon. Questions go to support@centapay.com. --- ## Changelog Source: https://www.centapay.com/docs/changelog Last updated 6 August 2026 Changelog This documentation tracks S2S CARD protocol v5.6.4. CentaPay monitors platform release announcements and updates these pages when the protocol changes. Availability. CentaPay is pre-launch. Production is expected in Q4 2026, with sandbox access ahead of it. Coverage differs by market, and the payment methods available differ by market too. The coverage table on our main site is the single source for what is live where, and for current dates. Nothing in this documentation should be read as a service available today. A dated entry is added here for every change that alters a request, a response, a callback or a hash formula. Corrections to wording are not listed. Subscribe to the Atom feed to be told when one is added. There are no entries yet. The protocol version above is the first tracked release, and no request, response, callback or hash formula has changed since tracking began. --- ## Compliance Source: https://www.centapay.com/docs/compliance Last updated 6 August 2026 Availability. CentaPay is pre-launch. Production is expected in Q4 2026, with sandbox access ahead of it. Coverage differs by market, and the payment methods available differ by market too. The coverage table on our main site is the single source for what is live where, and for current dates. Nothing in this documentation should be read as a service available today. Compliance Merchant eligibility, prohibited services, and CentaPay's AML programme are published in full on Compliance. PSP clients retain responsibility for KYC on their sub-merchants. The AML Statement there sets out the obligations. --- ## Error and decline codes Source: https://www.centapay.com/docs/errors Last updated 6 August 2026 Error and decline codes# Every code the platform can return on a failed request, with the cause it reports and whether sending the same request again can succeed. Availability. CentaPay is pre-launch. Production is expected in Q4 2026, with sandbox access ahead of it. Coverage differs by market, and the payment methods available differ by market too. The coverage table on our main site is the single source for what is live where, and for current dates. Nothing in this documentation should be read as a service available today. Error & Decline Codes# When a request fails validation, the synchronous response contains: Response { "result": "ERROR", "error_message": "Description of the error", "error_code": 204002 } Error Code Reference The Category column is the cause, as the platform reports it. The Retry column is what to do about it. They are separate axes, and the same cause can carry different retry advice depending on the code. Codes marked Not classified are ones where the documented cause does not settle the retry behaviour. They are left unclassified deliberately rather than guessed at, because wrong retry advice either hammers an endpoint that will never succeed or abandons one that would. CodeDescriptionCategoryRetryGuidance 400#Duplicate request (order_id already used)ValidationTerminalThe order_id is spent. Issue a new one. Do not retry the same request. 204002#Enabled merchant mappings or MIDs not foundConfigurationAccount configurationContact support. Retrying will not change the mapping. 204003#Payment type not supportedConfigurationAccount configurationContact support. Retrying will not change the mapping. 204004#Payment method not supportedConfigurationAccount configurationContact support. Retrying will not change the mapping. 204005#Payment action not supportedConfigurationAccount configurationContact support. Retrying will not change the mapping. 204006#Payment system/brand not supportedConfigurationAccount configurationContact support. Retrying will not change the mapping. 204007#Day MID limit not set or exceededLimitsNot classifiedConflates a limit not set with a limit exceeded. See questions.md Q4. 204008#Day merchant mapping limit not set or exceededLimitsNot classifiedConflates a limit not set with a limit exceeded. See questions.md Q4. 204009#Payment type not foundConfigurationAccount configurationContact support. Retrying will not change the mapping. 204010#Payment method not foundConfigurationAccount configurationContact support. Retrying will not change the mapping. 204011#Payment system/brand not foundConfigurationAccount configurationContact support. Retrying will not change the mapping. 204012#Payment currency not foundConfigurationAccount configurationContact support. Retrying will not change the mapping. 204013#Payment action not foundConfigurationAccount configurationContact support. Retrying will not change the mapping. 204014#Month MID limit exceededLimitsTransientA monthly window. Retry in the next period, not sooner. 204015#Week merchant mapping limit exceededLimitsTransientA weekly window. Retry in the next period, not sooner. 208001#Payment not foundTransactionTerminalThe trans_id does not exist. Check it rather than retrying. 208002#Cannot request 3DS for payment not in 3DS statusTransactionNot classifiedDepends on whether the payment can still reach that state. See questions.md Q4. 208003#Cannot capture payment not in PENDING statusTransactionNot classifiedDepends on whether the payment can still reach PENDING. See questions.md Q4. 208004#Capture amount exceeds auth amountTransactionTerminalArithmetic. Retrying the same amount always fails. 208005#Cannot refund payment not in SETTLED or PENDING statusTransactionNot classifiedDepends on whether the payment can still settle. See questions.md Q4. 208006#Refund amount exceeds payment amountTransactionTerminalArithmetic. Retrying the same amount always fails. 208008#Reversal amount exceeds payment amountTransactionTerminalArithmetic. Retrying the same amount always fails. 208009#Partial reversal not allowedTransactionTerminalNot permitted for this transaction. Send a full reversal. 208010#Chargeback amount exceeds payment amountTransactionTerminalArithmetic. Retrying the same amount always fails. 205005#Card token invalid or not foundTokenTerminalThat token will not become valid. Collect the card again. 205006#Card token expiredTokenTerminalThat token will not become valid. Collect the card again. 205007#Card token not accessibleTokenNot classifiedCause not stated. See questions.md Q4. 100000#Previous payment not completedValidationTransientWait for the earlier payment to reach a final state, then retry. Decline reasons are returned in the decline_reason field for DECLINED transactions. These are human-readable strings from the issuer or risk engine, not codes. Common examples: "Insufficient funds", "Card expired", "Do not honor". Where to go next# Symptoms that do not carry a code, such as a request that gets no response at all, are on Troubleshooting. The sandbox scenarios that produce specific declines are on Testing. --- ## Machine-readable formats Source: https://www.centapay.com/docs/formats Last updated 6 August 2026 Machine-readable formats The same surface this page describes, in the formats tooling reads. All three are generated from the specification on every build, so they cannot drift from what is documented here. Availability. CentaPay is pre-launch. Production is expected in Q4 2026, with sandbox access ahead of it. Coverage differs by market, and the payment methods available differ by market too. The coverage table on our main site is the single source for what is live where, and for current dates. Nothing in this documentation should be read as a service available today. OpenAPI 3.1 The specification. Import it into a client generator, a mock server or an API console. AsyncAPI 3.0 The callbacks, as a message contract. Generated from the OpenAPI schemas, so the two cannot disagree about a field. Feed it to a generator to scaffold your callback handler. Postman collection One request per action, with the signature computed in a pre-request script so every call is ready to send. Single-file Markdown The whole of this documentation as one file, for reading offline or feeding to a model. --- ## Glossary Source: https://www.centapay.com/docs/glossary Last updated 6 August 2026 Glossary Terms this documentation uses, defined from what the platform actually does with them. Where the platform names a field but does not define the concept behind it, the entry says where the value comes from rather than guessing. Availability. CentaPay is pre-launch. Production is expected in Q4 2026, with sandbox access ahead of it. Coverage differs by market, and the payment methods available differ by market too. The coverage table on our main site is the single source for what is live where, and for current dates. Nothing in this documentation should be read as a service available today. TermWhat it is here card_tokenA stored reference to a card, returned when a sale is made with tokenisation. It replaces the card fragment in the Formula 1 variant, so a request that sends a token signs differently from one that sends a PAN. recurring_tokenReturned in the response and the callback when a sale is sent with recurring_init=Y. Charging it later needs the token, the card's first six and last four digits, and the payer email, because RECURRING_SALE signs with Formula 1 and sends no card fields. schedule_idIdentifies a schedule the platform runs. It is part of the Formula 4 signature, which is why the digest differs per schedule where Formula 3 does not. channel_idRoutes a request to a sub-account. Up to sixteen characters, optional, and available on both the acceptance and payout sides. Sub-merchantAn account beneath yours, reached with channel_id. Settlement is aggregated with sub-merchant reporting. DescriptorThe statement descriptor, returned on successful transactions. It is what the cardholder sees on their statement. CascadingAn account setting under which one payment request can produce several underlying transactions. It is the reason a timed-out request must be resolved with GET_TRANS_STATUS_BY_ORDER rather than resent: a blind retry can multiply rather than repeat. pan_typeDPAN or FPAN, present on wallet transactions only. It says whether the wallet supplied a device account number or the funding PAN. rrn, approval_code, connector_nameAcquirer-level fields on success callbacks. They are absent unless Extended Data is enabled for your account, under Configuration, Protocol Mappings, "Add Extended Data to Callback". The platform passes these through from the acquirer rather than deriving them. arnCarried on chargeback callbacks when configured. Like the fields above, it originates with the acquirer. Extended DataThe account setting that adds the acquirer-level fields above to callbacks. Off by default, so a handler must treat all of them as optional. --- ## Guides Source: https://www.centapay.com/docs/guides Last updated 6 August 2026 Guides# Task-oriented walkthroughs of the S2S CARD API. Each one takes a single job end to end - the request, every response it can return, the callback that settles it, and the mistakes that cost people time. Availability. CentaPay is pre-launch. Production is expected in Q4 2026, with sandbox access ahead of it. Coverage differs by market, and the payment methods available differ by market too. The coverage table on our main site is the single source for what is live where, and for current dates. Nothing in this documentation should be read as a service available today. Start with Take a payment even if your first job is a refund or a payout. It establishes the request shape, the four synchronous responses and the callback contract that every other guide assumes. If you have not made a call yet, the quickstart goes from credentials to a verified callback in about fifteen minutes. Take a payment SALE, currencies, AUTH and CAPTURE, tokenisation, sub-merchant routing. Handle 3D Secure The redirect flow. Refund and reverse CREDITVOID and VOID. Pay out to a card CREDIT2CARD, plus the CREDIT2VIRTUAL coming-soon note. Bill recurring RECURRING_SALE, RETRY, schedule operations. Accept wallets Apple Pay and Google Pay. Every guide prints worked requests, responses and callbacks with reproducible signatures. Any hash on these pages can be recomputed from the inputs shown. Technical questions go to support@centapay.com. --- ## Accept wallets Source: https://www.centapay.com/docs/guides/accept-wallets Last updated 6 August 2026 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 john@example.com 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. Copymd5(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: Copy5a3ad716b1b70d6e0a8d5f06548cf7ea 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 cURLPHPPythonNode.js Copycurl -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 "payer_email=john@example.com" \ -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" Copy$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' => 'john@example.com', '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); Copyimport 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': 'john@example.com', '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() Copyconst 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': 'john@example.com', '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. Copyaction=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# Take a payment covers the card sale, authorise and capture, and saving a card. Handle 3D Secure covers the redirect. Bill recurring covers scheduled charging, which interacts with the virtual and card flow choice above. The API reference lists every parameter of every action. Idempotency covers order IDs, timeouts and what to do when an outcome is unknown. Technical questions go to support@centapay.com. --- ## Bill recurring Source: https://www.centapay.com/docs/guides/bill-recurring Last updated 6 August 2026 Bill recurring# Charge a card you have already taken once, on your own schedule or on one the platform runs for you. 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. Recurring billing has two halves. RECURRING_SALE charges a stored card, and you decide when. Schedule operations hand that timing to the platform. Most integrations need only the first. Every example below uses the same sample credentials so the hashes are reproducible: payer_email of john@example.com, 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. Store three things, or you cannot charge again# Add recurring_init=Y to the initial SALE. On success, the response and the callback carry a recurring_token. Store that token with the card's first six and last four digits and the payer email. All three are needed to compute the hash on every subsequent charge, and only the first one is obviously a credential. This is the single most consequential paragraph on the page. RECURRING_SALE signs with Formula 1, which requires a card fragment, and RECURRING_SALE does not send card fields. The digits come from the mask on the initial callback. Discard that mask and the stored token is unusable, with no way to recover it from the platform. In the sandbox, only the test card with expiry 01/2038 returns a recurring_token. The other expiries will not give you one to test with. Charging the stored card# POSThttps://{PAYMENT_URL}/post cURLPHPPythonNode.js Copycurl -X POST https://{PAYMENT_URL}/post \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "action=RECURRING_SALE" \ -d "client_key={CLIENT_KEY}" \ -d "order_id=REC-4001" \ -d "order_amount=120000" \ -d "order_description=Subscription REC-4001" \ -d "recurring_first_trans_id=b8c9d0e1-2f3a-4b5c-9d6e-7f8a9b0c1d2e" \ -d "recurring_token={RECURRING_TOKEN}" \ -d "hash=c8b58f1a6a6083fd4f0bd17d3ef58a45" Copy$url = 'https://{PAYMENT_URL}/post'; $fields = [ 'action' => 'RECURRING_SALE', 'client_key' => '{CLIENT_KEY}', 'order_id' => 'REC-4001', 'order_amount' => '120000', 'order_description' => 'Subscription REC-4001', 'recurring_first_trans_id' => 'b8c9d0e1-2f3a-4b5c-9d6e-7f8a9b0c1d2e', 'recurring_token' => '{RECURRING_TOKEN}', '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); Copyimport requests from urllib.parse import urlencode url = 'https://{PAYMENT_URL}/post' fields = { 'action': 'RECURRING_SALE', 'client_key': '{CLIENT_KEY}', 'order_id': 'REC-4001', 'order_amount': '120000', 'order_description': 'Subscription REC-4001', 'recurring_first_trans_id': 'b8c9d0e1-2f3a-4b5c-9d6e-7f8a9b0c1d2e', 'recurring_token': '{RECURRING_TOKEN}', 'hash': 'c8b58f1a6a6083fd4f0bd17d3ef58a45', } body = urlencode(fields) response = requests.post( url, data=body, headers={'Content-Type': 'application/x-www-form-urlencoded'}, ) result = response.json() Copyconst url = 'https://{PAYMENT_URL}/post' const fields = { 'action': 'RECURRING_SALE', 'client_key': '{CLIENT_KEY}', 'order_id': 'REC-4001', 'order_amount': '120000', 'order_description': 'Subscription REC-4001', 'recurring_first_trans_id': 'b8c9d0e1-2f3a-4b5c-9d6e-7f8a9b0c1d2e', 'recurring_token': '{RECURRING_TOKEN}', '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() order_id must be new and unique for every charge. order_amount and order_description are required. recurring_first_trans_id is the trans_id of the initial transaction, not of the previous recurring charge. Optional: schedule_id to link the charge to a schedule, auth=Y for authorisation only, and custom_data, which overrides the initial sale's custom_data rather than merging with it. The response is identical to a SALE response but with action: RECURRING_SALE. Recurring bypasses 3DS Recurring transactions bypass 3D Secure. Two things follow from that, and neither is optional. Your agreement with the cardholder must authorise recurring charges. And your acquiring setup must support merchant-initiated transactions. The callback verifies with Formula 2 The request signs with Formula 1. The callback verifies with Formula 2, which adds the trans_id, and it carries the same parameters as a SALE callback. Copyaction=RECURRING_SALE result=SUCCESS status=SETTLED order_id=REC-4001 trans_id=b8c9d0e1-2f3a-4b5c-9d6e-7f8a9b0c1d2e amount=120000 currency=UZS card=411111****1111 hash=3864fecfc87b4767f7073eeed16f47c1 Two constructions again, one outbound and one inbound. The pattern holds across every operation in these guides. Retrying a soft decline# RETRY re-attempts a declined recurring transaction. Only soft declines will succeed. Hard declines, such as a stolen card or fraud, will not, and retrying them is wasted traffic that an acquirer notices. POSThttps://{PAYMENT_URL}/post cURLPHPPythonNode.js Copycurl -X POST https://{PAYMENT_URL}/post \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "action=RETRY" \ -d "client_key={CLIENT_KEY}" \ -d "trans_id=b8c9d0e1-2f3a-4b5c-9d6e-7f8a9b0c1d2e" \ -d "hash=c8b58f1a6a6083fd4f0bd17d3ef58a45" Copy$url = 'https://{PAYMENT_URL}/post'; $fields = [ 'action' => 'RETRY', 'client_key' => '{CLIENT_KEY}', 'trans_id' => 'b8c9d0e1-2f3a-4b5c-9d6e-7f8a9b0c1d2e', '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); Copyimport requests from urllib.parse import urlencode url = 'https://{PAYMENT_URL}/post' fields = { 'action': 'RETRY', 'client_key': '{CLIENT_KEY}', 'trans_id': 'b8c9d0e1-2f3a-4b5c-9d6e-7f8a9b0c1d2e', 'hash': 'c8b58f1a6a6083fd4f0bd17d3ef58a45', } body = urlencode(fields) response = requests.post( url, data=body, headers={'Content-Type': 'application/x-www-form-urlencoded'}, ) result = response.json() Copyconst url = 'https://{PAYMENT_URL}/post' const fields = { 'action': 'RETRY', 'client_key': '{CLIENT_KEY}', 'trans_id': 'b8c9d0e1-2f3a-4b5c-9d6e-7f8a9b0c1d2e', '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() trans_id is the declined recurring transaction. Signed with Formula 1, which is why the digest matches the RECURRING_SALE above: same email, same password, same card fragment, and Formula 1 takes nothing else. The synchronous response is result: ACCEPTED with order_id and trans_id. Callbacks are verified with Formula 2. Success carries status: SETTLED with amount and currency. Declined carries status: DECLINED with decline_reason alongside them. Letting the platform run the schedule# Four operations manage a schedule, and a fifth detaches a card from one. Creating a schedule POSThttps://{PAYMENT_URL}/post cURLPHPPythonNode.js Copycurl -X POST https://{PAYMENT_URL}/post \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "action=CREATE_SCHEDULE" \ -d "client_key={CLIENT_KEY}" \ -d "name=Monthly subscription" \ -d "interval_length=1" \ -d "interval_unit=month" \ -d "day_of_month=15" \ -d "payments_count=12" \ -d "hash=fc9e973f07b1a3f839991328a8d07487" Copy$url = 'https://{PAYMENT_URL}/post'; $fields = [ 'action' => 'CREATE_SCHEDULE', 'client_key' => '{CLIENT_KEY}', 'name' => 'Monthly subscription', 'interval_length' => '1', 'interval_unit' => 'month', 'day_of_month' => '15', 'payments_count' => '12', 'hash' => 'fc9e973f07b1a3f839991328a8d07487', ]; $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); Copyimport requests from urllib.parse import urlencode url = 'https://{PAYMENT_URL}/post' fields = { 'action': 'CREATE_SCHEDULE', 'client_key': '{CLIENT_KEY}', 'name': 'Monthly subscription', 'interval_length': '1', 'interval_unit': 'month', 'day_of_month': '15', 'payments_count': '12', 'hash': 'fc9e973f07b1a3f839991328a8d07487', } body = urlencode(fields) response = requests.post( url, data=body, headers={'Content-Type': 'application/x-www-form-urlencoded'}, ) result = response.json() Copyconst url = 'https://{PAYMENT_URL}/post' const fields = { 'action': 'CREATE_SCHEDULE', 'client_key': '{CLIENT_KEY}', 'name': 'Monthly subscription', 'interval_length': '1', 'interval_unit': 'month', 'day_of_month': '15', 'payments_count': '12', 'hash': 'fc9e973f07b1a3f839991328a8d07487', } 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() name, interval_length, interval_unit and payments_count are all required. interval_unit is day or month, and interval_length is a number greater than zero, so 15 with day means every fifteen days. day_of_month applies only when interval_unit is month. Set it to 29, 30 or 31 and a shorter month uses its last day. delays is the number of intervals to skip before the schedule starts. The response returns a schedule_id. Formula 3 is the whole signature, and it is the reversed password, uppercased, hashed. No account key, no schedule, no card. It is therefore identical for every CREATE_SCHEDULE you ever send. SANDBOX_PASSWORD is already uppercase, so the digest above is the same whether you uppercase the reversed password or not, and it cannot tell a correct implementation from one that skips strtoupper. Check yours against a mixed-case password too. With PASSWORD of Sandbox_Pass1 the correct construction gives 2bf5b69b57d942e1ec9d207129439d2e. Omitting the uppercasing gives 1230c4aa6b9db6317007ebe15cf9934a. Pausing, running, deleting, inspecting PAUSE_SCHEDULE, RUN_SCHEDULE, DELETE_SCHEDULE and SCHEDULE_INFO all take the same three fields: action, client_key and schedule_id. POSThttps://{PAYMENT_URL}/post cURLPHPPythonNode.js Copycurl -X POST https://{PAYMENT_URL}/post \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "action=PAUSE_SCHEDULE" \ -d "client_key={CLIENT_KEY}" \ -d "schedule_id=sch_9f2c41a8" \ -d "hash=e9dedc0029fed85cda02ae74b67f38dc" Copy$url = 'https://{PAYMENT_URL}/post'; $fields = [ 'action' => 'PAUSE_SCHEDULE', 'client_key' => '{CLIENT_KEY}', 'schedule_id' => 'sch_9f2c41a8', 'hash' => 'e9dedc0029fed85cda02ae74b67f38dc', ]; $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); Copyimport requests from urllib.parse import urlencode url = 'https://{PAYMENT_URL}/post' fields = { 'action': 'PAUSE_SCHEDULE', 'client_key': '{CLIENT_KEY}', 'schedule_id': 'sch_9f2c41a8', 'hash': 'e9dedc0029fed85cda02ae74b67f38dc', } body = urlencode(fields) response = requests.post( url, data=body, headers={'Content-Type': 'application/x-www-form-urlencoded'}, ) result = response.json() Copyconst url = 'https://{PAYMENT_URL}/post' const fields = { 'action': 'PAUSE_SCHEDULE', 'client_key': '{CLIENT_KEY}', 'schedule_id': 'sch_9f2c41a8', 'hash': 'e9dedc0029fed85cda02ae74b67f38dc', } 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() These sign with Formula 4, the reversed concatenation of schedule_id and password, uppercased. Because the schedule id is inside it, this digest changes per schedule where Formula 3 does not. PAUSE_SCHEDULE can only be used when the schedule's paused parameter is N. A paused schedule cannot take new recurring payments until it is released with RUN_SCHEDULE. SCHEDULE_INFO returns name, interval_length, interval_unit, day_of_month, payments_count, delays and paused. Detaching a card DESCHEDULE takes action, client_key, recurring_token and schedule_id, and signs with Formula 4 like the others. Which formula, at a glance# OperationSigns with RECURRING_SALEFormula 1 RETRYFormula 1 CREATE_SCHEDULEFormula 3 PAUSE_SCHEDULE, RUN_SCHEDULE, DELETE_SCHEDULE, SCHEDULE_INFOFormula 4 DESCHEDULEFormula 4 Every callback aboveFormula 2 What next# Take a payment covers the initial sale that produces the token, including req_token for merchant-initiated charging without a schedule. Accept wallets covers the virtual and card flow choice, which limits recurring on wallet payments. The API reference lists every parameter of every action. Idempotency covers order IDs, timeouts and what to do when an outcome is unknown. Technical questions go to support@centapay.com. --- ## Handle 3D Secure Source: https://www.centapay.com/docs/guides/handle-3d-secure Last updated 6 August 2026 Handle 3D Secure# Send a cardholder to their bank to authenticate, and pick the payment up again when they come back. 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. Most Kazakhstan and Uzbekistan card transactions trigger 3DS, so this is the path most of your live traffic takes rather than an edge case. The single-step sale in Take a payment is the exception. Every example below uses the same sample credentials so the hashes are reproducible: payer_email of john@example.com, 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. The shape of an authenticated payment# Six steps, and the last one is the only authoritative signal. You send a SALE. You get back result: REDIRECT with a redirect_url. You put the cardholder's browser on that URL. They authenticate with their issuer, by SMS, biometric, or frictionlessly with no interaction at all. They return to your term_url_3ds. The final result arrives by callback. The return leg is not the result. A cardholder can land back on your site before the callback arrives, and can land back on it having abandoned the challenge entirely. Treat the return as a signal to show a waiting state, and nothing more. The redirect response# A SALE that needs authentication returns result: REDIRECT and status: 3DS. The request itself is identical to a non-3DS sale, including its signature, so nothing about how you build it changes. The request One field differs from the single-step sale: an expiry of 05/2038, which selects the 3DS challenge from the test-card table below. POSThttps://{PAYMENT_URL}/post cURLPHPPythonNode.js Copycurl -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=TDS-2001" \ -d "order_amount=120000" \ -d "order_currency=UZS" \ -d "order_description=Order TDS-2001" \ -d "card_number=4111111111111111" \ -d "card_exp_month=05" \ -d "card_exp_year=2038" \ -d "card_cvv2=123" \ -d "payer_first_name=John" \ -d "payer_last_name=Smith" \ -d "payer_email=john@example.com" \ -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" Copy$url = 'https://{PAYMENT_URL}/post'; $fields = [ 'action' => 'SALE', 'client_key' => '{CLIENT_KEY}', 'order_id' => 'TDS-2001', 'order_amount' => '120000', 'order_currency' => 'UZS', 'order_description' => 'Order TDS-2001', 'card_number' => '4111111111111111', 'card_exp_month' => '05', 'card_exp_year' => '2038', 'card_cvv2' => '123', 'payer_first_name' => 'John', 'payer_last_name' => 'Smith', 'payer_email' => 'john@example.com', '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); Copyimport requests from urllib.parse import urlencode url = 'https://{PAYMENT_URL}/post' fields = { 'action': 'SALE', 'client_key': '{CLIENT_KEY}', 'order_id': 'TDS-2001', 'order_amount': '120000', 'order_currency': 'UZS', 'order_description': 'Order TDS-2001', 'card_number': '4111111111111111', 'card_exp_month': '05', 'card_exp_year': '2038', 'card_cvv2': '123', 'payer_first_name': 'John', 'payer_last_name': 'Smith', 'payer_email': 'john@example.com', '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() Copyconst url = 'https://{PAYMENT_URL}/post' const fields = { 'action': 'SALE', 'client_key': '{CLIENT_KEY}', 'order_id': 'TDS-2001', 'order_amount': '120000', 'order_currency': 'UZS', 'order_description': 'Order TDS-2001', 'card_number': '4111111111111111', 'card_exp_month': '05', 'card_exp_year': '2038', 'card_cvv2': '123', 'payer_first_name': 'John', 'payer_last_name': 'Smith', 'payer_email': 'john@example.com', '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() The signature is the one the non-3DS sale carries. Formula 1 reads payer_email, PASSWORD and the card's first six and last four digits, and none of those moved, so changing the expiry to select a 3DS outcome does not change the hash. A 3DS request that fails on the signature is failing for some other reason. term_url_3ds is where the cardholder lands after authenticating. Here it is load-bearing rather than a formality. Copy{ "action": "SALE", "result": "REDIRECT", "status": "3DS", "order_id": "TDS-2001", "trans_id": "e5f6a7b8-9c0d-4e1f-a2b3-c4d5e6f7a8b9", "trans_date": "2026-07-25 14:32:07", "descriptor": "CENTAPAY TDS", "amount": "120000", "currency": "UZS", "redirect_url": "https://acs.example/3ds/challenge", "redirect_method": "POST", "redirect_params": { "PaReq": "eJxVUt1...", "TermUrl": "https://yoursite.example/3ds-return" } } status can also be REDIRECT rather than 3DS. Both mean the same thing to your code: send the cardholder to redirect_url. Keep the trans_id. It is how you match the callback to the payment, and it is an input to the callback signature. redirect_params is not guaranteed redirect_params varies by acquirer. It may contain PaReq, TermUrl, or other values. It may be empty, and it may be absent altogether, most commonly when redirect_method is GET. Check for it before you iterate it. Code that assumes an array here fails on the acquirer that sends nothing, and it fails at the moment a real cardholder is waiting. Sending the cardholder# Two shapes, chosen by redirect_method. A self-submitting form, for POST Copy$html = '
'; if (!empty($response['redirect_params']) && is_array($response['redirect_params'])) { foreach ($response['redirect_params'] as $k => $v) { $html .= ''; } } $html .= '
'; echo $html; The !empty and is_array guard is the part that matters. It is what makes the same code work for an acquirer that sends no parameters. A location change, for GET Copydocument.location = response.redirect_url; Choosing your endpoint by how you parse# The two endpoints differ in one respect: the shape of redirect_params. /post returns it as a key-value object. Copy"redirect_params": { "PaReq": "eJxVUt1...", "TermUrl": "https://..." } /v2/post returns it as an array of objects. Copy"redirect_params": [ {"name": "PaReq", "value": "eJxVUt1..."}, {"name": "TermUrl", "value": "https://..."} ] Neither is more correct. Pick the one your form builder consumes without reshaping, and use it consistently, because a payload that changes shape between endpoints is a bug waiting for the day someone switches. Returning into an iframe# If your checkout runs inside an iframe, term_url_target controls where the cardholder lands when the issuer sends them back. Values are _blank, _self, _parent, _top, or a custom iframe name. _top is the default. The default breaks out of the iframe, which is usually what you want, because many issuers refuse to render their challenge inside a frame at all. Set it deliberately rather than inheriting it. The callback, and the field that is missing# The final outcome arrives at your callback URL. Copyaction=SALE result=SUCCESS status=SETTLED order_id=TDS-2001 trans_id=e5f6a7b8-9c0d-4e1f-a2b3-c4d5e6f7a8b9 trans_date=2026-07-25 14:41:55 descriptor=CENTAPAY TDS amount=120000 currency=UZS card=411111****1111 card_expiration_date=01/2038 hash=4008791549623c0d41f3cb0bd9816d79 Verify with Formula 2, over payer_email, PASSWORD, trans_id and the card's first six and last four characters. Here is the trap this flow sets, and it is specific to 3DS. You will receive more than one callback. A redirect callback arrives first, carrying result: REDIRECT and status: 3DS or REDIRECT, and the final callback follows it. Treat trans_id plus result as the unit of work and make your handler idempotent. A repeated callback must not create a second fulfilment. The redirect callback has no card field. It carries action, result, status, order_id, trans_id, trans_date, descriptor, amount, currency, redirect_url, redirect_params, redirect_method, custom_data, digital_wallet, pan_type and hash. It does not carry card or card_expiration_date. Formula 2 needs a card fragment, so verifying that callback means falling back to the mask you stored when you created the payment. The same is true of an undefined callback, which also omits both fields. The masked PAN behaves exactly like the full one. First six characters joined to the last four gives 4111111111, and the asterisks are never touched. trans_id is uppercased along with everything else before hashing. A lowercase UUID that is not uppercased is a silent mismatch, and it is the most common cause of a callback that fails verification while the request hash worked. Return the plain string OK once you have accepted the notification, or ERROR if you have not. Test cards# The expiry date selects the outcome. Sandbox scheme coverage is a testing convenience and does not indicate which schemes are enabled on your live account. ExpiryOutcome 05/20383DS challenge, then success 06/20383DS challenge, then decline 12/2038Redirect, then success 12/2039Redirect, then decline 05/2038 and 06/2038 return status: 3DS. 12/2038 and 12/2039 return status: REDIRECT. Exercise both, because your code branches on redirect_method and not on status, and the two pairs can differ. What next# Take a payment covers the single-step sale, authorise and capture, saving a card, and sub-merchant routing. Refund and reverse covers CREDITVOID and VOID. The API reference lists every parameter of every action, and operations covers callback delivery and the go-live checklist. Idempotency covers order IDs, timeouts and what to do when an outcome is unknown. Technical questions go to support@centapay.com. --- ## Pay out to a card Source: https://www.centapay.com/docs/guides/pay-out-to-a-card Last updated 6 August 2026 Pay out to a card# Push funds from your settlement balance to a recipient's card. Availability. CentaPay is pre-launch. Production is expected in Q4 2026, with sandbox access ahead of it. Coverage differs by market, and the payment methods available differ by market too. The coverage table on our main site is the single source for what is live where, and for current dates. Nothing in this documentation should be read as a service available today. CREDIT2CARD is the payout product. It runs on the same endpoint as everything else and looks like a sale turned around, but it signs differently at both ends and it is the one action whose callback does not use Formula 2. Every example below uses the same sample credentials so the hashes are reproducible: a PASSWORD of SANDBOX_PASSWORD and the sandbox payout test card. Payload values are illustrative, but every field name, its presence or absence, and every hash is exact. payer_* is your business, not a person# The request carries both payee_* and payer_* name and address blocks. The payee_* fields describe the recipient. The payer_* fields describe the sender. Populate payer_* with the funding business, never with an individual. A payout carrying an individual's name and address in the sender fields looks like a person-to-person transfer in the acquirer's data, whatever your intent. That is a different product with different rules, and the classification is made from what you send. Every one of these fields is optional. Sending nothing is safer than sending a consumer's details. The request# POSThttps://{PAYMENT_URL}/post cURLPHPPythonNode.js Copycurl -X POST https://{PAYMENT_URL}/post \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "action=CREDIT2CARD" \ -d "client_key={CLIENT_KEY}" \ -d "order_id=PAY-5001" \ -d "order_amount=240000" \ -d "order_currency=UZS" \ -d "order_description=Payout PAY-5001" \ -d "card_number=4601541833776519" \ -d "payee_first_name=Jane" \ -d "payee_last_name=Doe" \ -d "payer_first_name=Example" \ -d "payer_last_name=Trading LLC" \ -d "hash=96cac979f21c03eb2a3fc5c4415fbfce" Copy$url = 'https://{PAYMENT_URL}/post'; $fields = [ 'action' => 'CREDIT2CARD', 'client_key' => '{CLIENT_KEY}', 'order_id' => 'PAY-5001', 'order_amount' => '240000', 'order_currency' => 'UZS', 'order_description' => 'Payout PAY-5001', 'card_number' => '4601541833776519', 'payee_first_name' => 'Jane', 'payee_last_name' => 'Doe', 'payer_first_name' => 'Example', 'payer_last_name' => 'Trading LLC', 'hash' => '96cac979f21c03eb2a3fc5c4415fbfce', ]; $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); Copyimport requests from urllib.parse import urlencode url = 'https://{PAYMENT_URL}/post' fields = { 'action': 'CREDIT2CARD', 'client_key': '{CLIENT_KEY}', 'order_id': 'PAY-5001', 'order_amount': '240000', 'order_currency': 'UZS', 'order_description': 'Payout PAY-5001', 'card_number': '4601541833776519', 'payee_first_name': 'Jane', 'payee_last_name': 'Doe', 'payer_first_name': 'Example', 'payer_last_name': 'Trading LLC', 'hash': '96cac979f21c03eb2a3fc5c4415fbfce', } body = urlencode(fields) response = requests.post( url, data=body, headers={'Content-Type': 'application/x-www-form-urlencoded'}, ) result = response.json() Copyconst url = 'https://{PAYMENT_URL}/post' const fields = { 'action': 'CREDIT2CARD', 'client_key': '{CLIENT_KEY}', 'order_id': 'PAY-5001', 'order_amount': '240000', 'order_currency': 'UZS', 'order_description': 'Payout PAY-5001', 'card_number': '4601541833776519', 'payee_first_name': 'Jane', 'payee_last_name': 'Doe', 'payer_first_name': 'Example', 'payer_last_name': 'Trading LLC', 'hash': '96cac979f21c03eb2a3fc5c4415fbfce', } 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() Required: action, client_key, order_id, order_amount, order_currency, order_description, card_number and hash. Everything else is optional, including every payee_* and payer_* field. order_amount is an integer for KZT and UZS and a float formatted XX.XX for USD, exactly as on the acceptance side. channel_id routes the payout to a sub-account and accepts up to sixteen characters. Formula 5 has no email in it A payout signs with Formula 5: Copymd5(strtoupper( PASSWORD . strrev(substr(card_number, 0, 6) . substr(card_number, -4)) )) Password first, then the reversed card fragment. There is no email term, unlike Formula 1, and no trans_id, because the transaction does not exist yet. If you are paying out to a stored token rather than a PAN, the variant is md5(strtoupper(PASSWORD . strrev(card_token))). The digest above cannot prove your uppercasing is right. SANDBOX_PASSWORD is already uppercase and the card fragment is digits, so strtoupper changes nothing and an implementation that omits it produces the same value. Check yours against a mixed-case password. With PASSWORD of Sandbox_Pass1 and the same card, the correct construction gives ffaa6f8851f5c3f70e47f6c968ac13a0. Omitting the uppercasing gives 04f837678afe4237202262551c68fbba. The synchronous response# Every CREDIT2CARD synchronous response carries action, result, status, order_id, trans_id and trans_date. Unlike a SALE, the success response does not include amount or currency. If your handler reads those fields unconditionally it will break here, and it will break on the success path rather than the error path. ResultStatusAlso carries SUCCESSSETTLEDdescriptor DECLINEDDECLINEDdecline_reason UNDEFINEDPREPAREdescriptor, if available The callback uses Formula 6, and only this action does# This is the fact to take away from the page. Copymd5(strtoupper( PASSWORD . trans_id . strrev(substr(card_number, 0, 6) . substr(card_number, -4)) )) Formula 6 is Formula 5 with the trans_id inserted between the password and the card fragment. Still no email. CREDIT2CARD is the only action whose callback hash formula differs from Formula 2. An integration that verifies every callback with one shared routine will pass on all of them and fail on this one, which is a difficult failure to find because it looks like a payout-specific problem rather than a hashing one. Copyaction=CREDIT2CARD result=SUCCESS status=SETTLED order_id=PAY-5001 trans_id=c9d0e1f2-3a4b-4c5d-9e6f-7a8b9c0d1e2f trans_date=2026-07-25 18:12:44 hash=459f391fad455334d8e40d69539125df Declined callbacks add decline_reason. Undefined callbacks carry status: PREPARE. All three verify with Formula 6. If your account has extended data enabled, success callbacks also carry connector_name, rrn, approval_code and related acquirer fields. The two digests on this page share a password and a card and differ only because Formula 6 adds the trans_id. That is the check a reader can run against their own implementation. Testing# The sandbox payout card is 4601541833776519, which returns SUCCESS with status: SETTLED. Sandbox scheme coverage is a testing convenience and does not indicate which schemes are enabled on your live account. Paying out to something other than a card# CREDIT2VIRTUAL covers payouts to mobile money, bank transfer and other virtual account methods. It is not yet available on CentaPay. Contact support@centapay.com if you need it, so the requirement is on record. What next# Take a payment covers the acceptance side, and its channel_id section covers sub-merchant routing, which works the same way here. Operations covers callback delivery and reconciliation. The API reference lists every parameter of every action. Idempotency covers order IDs, timeouts and what to do when an outcome is unknown. Technical questions go to support@centapay.com. --- ## Refund and reverse Source: https://www.centapay.com/docs/guides/refund-and-reverse Last updated 6 August 2026 Refund and reverse# Give money back, or cancel a payment before the money ever moves. 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. Two operations do this, and choosing the wrong one is the most common mistake here. VOID cancels a transaction on the same financial day and moves no funds. CREDITVOID returns funds that have settled, or releases an authorisation hold. Every example below uses the same sample credentials so the hashes are reproducible: payer_email of john@example.com, 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. Which one to send# Use VOID for a same-day cancellation, where no funds movement is wanted at all. Use CREDITVOID for a refund after settlement day, or to reverse an authorisation hold. VOID is available only for transactions in SETTLED status that came from SALE, CAPTURE or RECURRING_SALE, and only on the same financial day they were taken. Outside that window the operation is CREDITVOID. Refunding with CREDITVOID# CREDITVOID handles both reversals, which cancel an authorisation hold, and refunds, which return settled funds. Full and partial are both supported, and unlike a capture, multiple partial refunds are allowed. That difference is worth holding on to. One partial capture is permitted and the remainder of the authorisation is then gone. Partial refunds have no such limit, so you can refund a basket line by line as goods are returned. The request Only four fields. The card is not among them. POSThttps://{PAYMENT_URL}/post cURLPHPPythonNode.js Copycurl -X POST https://{PAYMENT_URL}/post \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "action=CREDITVOID" \ -d "client_key={CLIENT_KEY}" \ -d "trans_id=f6a7b8c9-0d1e-4f2a-b3c4-d5e6f7a8b9c0" \ -d "hash=d2bd8cb00c527171af3812ff9aae9ef8" Copy$url = 'https://{PAYMENT_URL}/post'; $fields = [ 'action' => 'CREDITVOID', 'client_key' => '{CLIENT_KEY}', 'trans_id' => 'f6a7b8c9-0d1e-4f2a-b3c4-d5e6f7a8b9c0', 'hash' => 'd2bd8cb00c527171af3812ff9aae9ef8', ]; $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); Copyimport requests from urllib.parse import urlencode url = 'https://{PAYMENT_URL}/post' fields = { 'action': 'CREDITVOID', 'client_key': '{CLIENT_KEY}', 'trans_id': 'f6a7b8c9-0d1e-4f2a-b3c4-d5e6f7a8b9c0', 'hash': 'd2bd8cb00c527171af3812ff9aae9ef8', } body = urlencode(fields) response = requests.post( url, data=body, headers={'Content-Type': 'application/x-www-form-urlencoded'}, ) result = response.json() Copyconst url = 'https://{PAYMENT_URL}/post' const fields = { 'action': 'CREDITVOID', 'client_key': '{CLIENT_KEY}', 'trans_id': 'f6a7b8c9-0d1e-4f2a-b3c4-d5e6f7a8b9c0', 'hash': 'd2bd8cb00c527171af3812ff9aae9ef8', } 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 refund the full sum. Include it to refund less, and send the request again for each further partial. The signature is Formula 2, over payer_email, PASSWORD, trans_id and the card's first six and last four digits. You are not sending the card, so those digits come from your own record of the original payment. Store the mask when you take the payment and this is straightforward. Fail to store it and you cannot sign a refund at all. The synchronous response Copy{ "action": "CREDITVOID", "result": "ACCEPTED", "order_id": "TAP-1001", "trans_id": "f6a7b8c9-0d1e-4f2a-b3c4-d5e6f7a8b9c0" } ACCEPTED means the request was taken, not that the money moved. Wait for the callback. The callbacks Four shapes, all signed with Formula 2. Success, full refund.result: SUCCESS with status: REFUND or REVERSAL, plus order_id, trans_id, creditvoid_date, amount and hash. Which status you get tells you whether funds were returned or a hold was released. Success, partial refund. The same fields, but status: SETTLED, because the original transaction keeps its settled status. Do not read SETTLED here as "the refund did not happen". It means the original is still settled, which is exactly right when only part of it came back. Declined.result: DECLINED with order_id, trans_id, decline_reason and hash. No status. Undefined.result: UNDEFINED with status: SETTLED, order_id, trans_id, creditvoid_date, amount and hash. Copyaction=CREDITVOID result=SUCCESS status=REFUND order_id=TAP-1001 trans_id=f6a7b8c9-0d1e-4f2a-b3c4-d5e6f7a8b9c0 creditvoid_date=2026-07-25 16:20:04 amount=120000 hash=d2bd8cb00c527171af3812ff9aae9ef8 If your account has extended data enabled, success callbacks also carry connector_name, rrn, approval_code and related acquirer fields. The request hash and the callback hash are the same digest, because both hash the same email, password, trans_id and card fragment. Seeing the value you sent come back is correct, not a replay. Cancelling with VOID# VOID cancels the transaction entirely. It does not process a refund, and no funds move. The request POSThttps://{PAYMENT_URL}/post cURLPHPPythonNode.js Copycurl -X POST https://{PAYMENT_URL}/post \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "action=VOID" \ -d "client_key={CLIENT_KEY}" \ -d "trans_id=f6a7b8c9-0d1e-4f2a-b3c4-d5e6f7a8b9c0" \ -d "hash=d2bd8cb00c527171af3812ff9aae9ef8" Copy$url = 'https://{PAYMENT_URL}/post'; $fields = [ 'action' => 'VOID', 'client_key' => '{CLIENT_KEY}', 'trans_id' => 'f6a7b8c9-0d1e-4f2a-b3c4-d5e6f7a8b9c0', 'hash' => 'd2bd8cb00c527171af3812ff9aae9ef8', ]; $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); Copyimport requests from urllib.parse import urlencode url = 'https://{PAYMENT_URL}/post' fields = { 'action': 'VOID', 'client_key': '{CLIENT_KEY}', 'trans_id': 'f6a7b8c9-0d1e-4f2a-b3c4-d5e6f7a8b9c0', 'hash': 'd2bd8cb00c527171af3812ff9aae9ef8', } body = urlencode(fields) response = requests.post( url, data=body, headers={'Content-Type': 'application/x-www-form-urlencoded'}, ) result = response.json() Copyconst url = 'https://{PAYMENT_URL}/post' const fields = { 'action': 'VOID', 'client_key': '{CLIENT_KEY}', 'trans_id': 'f6a7b8c9-0d1e-4f2a-b3c4-d5e6f7a8b9c0', 'hash': 'd2bd8cb00c527171af3812ff9aae9ef8', } 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 2, exactly as CREDITVOID is. trans_id accepts up to 255 characters here. The synchronous response ResultStatusMeaning SUCCESSVOIDTransaction voided DECLINEDSETTLEDVoid rejected, with decline_reason. The original remains settled. UNDEFINEDPENDING or SETTLEDOutcome not yet known. Await the callback. A declined void leaves you with a settled transaction and a customer expecting their money back. Handle that branch explicitly: the follow-up is a CREDITVOID, not a retry of the void. The callbacks Success.result: SUCCESS, status: VOID, plus order_id, trans_id, trans_date and hash. Declined.result: DECLINED, status: SETTLED, plus order_id, trans_id, trans_date, decline_reason and hash. Undefined.result: UNDEFINED, status: PENDING or SETTLED, plus order_id, trans_id, trans_date and hash. The VOID callback signs differently# This is the one genuine trap on this page. Every other callback in this guide verifies with Formula 2. A VOID callback does not. It uses the Void signature: Copyhash = md5(strtoupper(strrev(trans_id)) . PASSWORD) Note what that is and is not. The uppercasing applies to the reversed trans_id only, and PASSWORD is appended after it, not folded into the uppercased string. There is no email and no card fragment. Unlike Formulas 1 to 8, it is a plain MD5 with no SHA1 wrapper. The VOID request itself still signs with Formula 2. So a single void involves two different hash constructions, one outbound and one inbound, and an integration that reuses the request hash to verify the callback will reject every void it ever receives. Copyaction=VOID result=SUCCESS status=VOID order_id=TAP-1001 trans_id=f6a7b8c9-0d1e-4f2a-b3c4-d5e6f7a8b9c0 trans_date=2026-07-25 16:22:31 hash=561b94d2dd370a7c3fc08d2f6675d6a4 Both digests on this page derive from the same trans_id, and they differ because the constructions differ. If yours match each other, you have used the wrong one twice. Check this one against a mixed-case password With an all-uppercase password the two constructions coincide, so the vector above cannot tell a correct implementation from a common mistake. Check yours against a mixed-case password as well. With PASSWORD of Sandbox_Pass1 and the same trans_id, the correct construction gives a435c9eec99409dee814616f62800808. Uppercasing the password along with the reversed trans_id gives d36684521d7ee51e935cf8f2c3fe1323, which is the error this vector exists to catch. What next# Take a payment covers the sale, authorise and capture, and saving a card. Handle 3D Secure covers the redirect. The API reference lists every parameter of every action, and operations covers callback delivery, chargebacks and the go-live checklist. Technical questions go to support@centapay.com. --- ## Take a payment Source: https://www.centapay.com/docs/guides/take-a-payment Last updated 6 August 2026 Take a payment# Accept a card payment from a customer in Uzbekistan. 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. 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 john@example.com, 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 POSThttps://{PAYMENT_URL}/post cURLPHPPythonNode.js Copycurl -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 "payer_email=john@example.com" \ -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" Copy$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' => 'john@example.com', '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); Copyimport 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': 'john@example.com', '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() Copyconst 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': 'john@example.com', '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. Copy{ "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. Copy{ "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 Copyaction=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# CurrencyCodeFormatExample Kazakhstani TengeKZTInteger, no decimal component5000 Uzbekistani SomUZSInteger, no decimal component120000 US DollarUSDFloat, XX.XX49.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. POSThttps://{PAYMENT_URL}/post cURLPHPPythonNode.js Copycurl -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 "payer_email=john@example.com" \ -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" Copy$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' => 'john@example.com', '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); Copyimport 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': 'john@example.com', '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() Copyconst 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': 'john@example.com', '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. POSThttps://{PAYMENT_URL}/post cURLPHPPythonNode.js Copycurl -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" Copy$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); Copyimport 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() Copyconst 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. Copyaction=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. Copy -d "req_token=Y" For later charges, send card_token in place of card_number, card_exp_month, card_exp_year and card_cvv2. POSThttps://{PAYMENT_URL}/post cURLPHPPythonNode.js Copycurl -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 "payer_email=john@example.com" \ -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" Copy$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' => 'john@example.com', '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); Copyimport 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': 'john@example.com', '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() Copyconst 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': 'john@example.com', '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. Copy -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 support@centapay.com. --- ## Operations Source: https://www.centapay.com/docs/operations Last updated 6 August 2026 Operations# Running a live integration: callback delivery and verification, status queries, chargebacks, settlement and reconciliation, testing and go-live. Availability. CentaPay is pre-launch. Production is expected in Q4 2026, with sandbox access ahead of it. Coverage differs by market, and the payment methods available differ by market too. The coverage table on our main site is the single source for what is live where, and for current dates. Nothing in this documentation should be read as a service available today. Settlement# Settlement Cycle CentaPay settles to clients on a T+1 (next business day) cycle. Transactions that reach SETTLED status before the daily cutoff are included in the next business day's settlement batch. ParameterValue CycleT+1 (next business day) Settlement currencyAgreed at onboarding (typically USD) Transaction Status Lifecycle How a transaction moves through the system toward settlement: StatusMeaning PENDINGAUTH hold placed. Funds reserved on cardholder's account but not yet captured PREPAREProcessing in progress. Final status not yet determined 3DSAwaiting cardholder 3DS authentication REDIRECTAwaiting redirect completion SETTLEDTransaction completed successfully. Funds will be included in the next settlement batch to you. DECLINEDTransaction rejected by issuer or risk engine VOIDSame-day cancellation. No funds movement REFUNDFull refund processed. Amount deducted from your settlement balance REVERSALAUTH hold released (no capture occurred) CHARGEBACKIssuer-initiated dispute. Amount debited from your settlement balance After partial refund: The original transaction retains SETTLED status. The partial refund amount is deducted from your settlement balance. Use GET_TRANS_DETAILS to see the full transaction history including partial refund entries. FX & Currency Conversion When the transaction currency differs from your settlement currency, CentaPay applies a foreign exchange conversion. The applicable rate is included in callback parameters: Callback FieldDescription exchange_rateFX rate applied to the transaction exchange_rate_baseBase conversion rate (if double conversion applies) exchange_currencyOriginal transaction currency exchange_amountOriginal transaction amount before conversion Settlement Reports Settlement reports will be available via the admin panel and will include transaction-level detail for reconciliation against your callback records. Contact support@centapay.com for details on report format, delivery schedule, and available export options for your account. Querying Settlement Status Use GET_TRANS_STATUS or GET_TRANS_DETAILS to check whether a transaction has settled. GET_TRANS_STATUS returns the current status field. A value of SETTLED confirms the transaction completed successfully and will be included in settlement. GET_TRANS_DETAILS returns the full order history including an array of all sub-transactions (sale, 3ds, auth, capture, credit, chargeback, reversal, refund) with individual dates, statuses, and amounts. Best practice: Do not poll GET_TRANS_STATUS for settlement confirmation. Use callbacks as the authoritative source. Reserve status queries for reconciliation or when a callback has not arrived within your expected window. Settlement & Reporting# PSP clients receive a single aggregated settlement per cycle covering all sub-merchant transactions. ParameterValue CycleT+1 (next business day) Settlement currencyAgreed at onboarding (typically USD) ScopeAll sub-merchants under your account, aggregated Each transaction callback includes order_id (your reference), trans_id (CentaPay reference), and channel_id (if provided) for sub-merchant-level reconciliation. Use GET_TRANS_DETAILS to retrieve the full history of any transaction, including payer details, masked card, and all status transitions. Tip: Enable Extended Data in your admin panel (Configuration → Protocol Mappings → "Add Extended Data to Callback") to receive rrn, approval_code, connector_name, and other acquirer-level fields in callbacks, useful for cross-referencing with upstream acquirer reports. For details on sub-merchant settlement breakdowns and report formats, contact support@centapay.com. Transaction Status (GET_TRANS_STATUS)# Query the current status of a transaction. Use when a callback hasn't arrived or for reconciliation. ParameterDescriptionRequired actionGET_TRANS_STATUSYes client_keyYour account keyYes trans_idCentaPay transaction IDYes hashFormula 2 (or Formula 6 for CREDIT2CARD)Yes Response includes status (one of: 3DS, REDIRECT, PENDING, PREPARE, DECLINED, SETTLED, REVERSAL, REFUND, VOID, CHARGEBACK), plus decline_reason if declined, and recurring_token / schedule_id / digital_wallet if applicable, arn* if configured. SETTLED is the terminal success status, the transaction has been authorised and captured, and will be included in the next settlement batch to you. For the full settlement lifecycle and status definitions, see Settlement. Transaction Details (GET_TRANS_DETAILS)# Returns full order history including payer details, card mask, and an array of all transactions in the order. ParameterDescriptionRequired actionGET_TRANS_DETAILSYes client_keyYour account keyYes trans_idCentaPay transaction IDYes hashFormula 2 (or Formula 6 for CREDIT2CARD)Yes Response includes: name, mail, ip, amount, currency, card (masked), decline_reason if declined, recurring_token, schedule_id, pan_type, digital_wallet, arn* if configured, and a transactions array with entries containing date, type (sale, 3ds, auth, capture, credit, chargeback, reversal, refund), status, and amount. Status by Order ID (GET_TRANS_STATUS_BY_ORDER)# Look up the most recent transaction status using your order_id instead of trans_id. Useful when trans_id is lost or for reconciliation. ParameterDescriptionRequired actionGET_TRANS_STATUS_BY_ORDERYes client_keyYour account keyYes order_idYour order IDYes hashFormula 7 (or Formula 6 for CREDIT2CARD)Yes Response: status (3DS/REDIRECT/PENDING/PREPARE/DECLINED/SETTLED/REVERSAL/REFUND/VOID/CHARGEBACK), decline_reason if declined, recurring_token, schedule_id, digital_wallet if applicable. With cascading enabled, returns most recent transaction only. Chargebacks# Chargebacks are initiated by the issuing bank, not by API request. CentaPay sends a callback notification when a chargeback occurs. Callback ParameterDescription actionCHARGEBACK resultSUCCESS statusCHARGEBACK order_idYour order ID trans_idCentaPay transaction ID amountChargeback amount chargeback_dateSystem date of the chargeback bank_dateBank date of the chargeback reason_codeChargeback reason code connector_name*Payment gateway name rrn*Retrieval Reference Number approval_code*Issuer authorisation code gateway_id* / extra_gateway_id*Gateway transaction identifiers merchant_name* / mid_name*Merchant and MID names issuer_country* / issuer_bank*Card issuer details hashFormula 2 * Extended data fields - included only if configured in admin panel (Configuration → Protocol Mappings → "Add Extended Data to Callback"). Callback Delivery Rules# CentaPay sends HTTP POST callbacks to your notification URL as transaction states change. Always use callbacks, not the synchronous response, as the authoritative result. Content Type Callbacks are sent as application/x-www-form-urlencoded. Required Response Your endpoint must return the plain string OK, nothing else. Any other content, HTML, or timeout is treated as a failure. Blocking: 5 consecutive timeouts within 5 minutes will block your callback URL for 15 minutes. All merchants sharing that URL are affected. The counter resets on any successful response. You can manually unblock via the admin panel (Configuration → Merchants → Edit Merchant). When Callbacks Are Sent Transaction TypeCallback Sent On SALE, CREDITVOID, RECURRING_SALESUCCESS, FAIL, WAITING, UNDEFINED CAPTURE, VOID, CREDIT2CARDSUCCESS, FAIL, UNDEFINED CHARGEBACKAlways (platform-initiated) Hash Verification# Always verify the callback hash before processing. For most actions, use Formula 2. For CREDIT2CARD, use Formula 6. For VOID callbacks, use the Void signature: md5(strtoupper(strrev(trans_id)) . PASSWORD). Formula 2 requires data not in the callback. The payer's email is not included in callback parameters. You must store it from the original SALE request. For SUCCESS callbacks, the card field provides the masked PAN (first 6 + last 4 visible). For DECLINED callbacks, card is not present, use the card mask stored from your original request. PHPPythonNode.js Copy$data = $_POST; // Use card from callback if present (SUCCESS), otherwise use stored mask (DECLINED) $cardMask = isset($data['card']) ? $data['card'] : $storedCardMask; $cardPart = substr($cardMask,0,6).substr($cardMask,-4); $expected = md5(strtoupper( strrev($storedEmail) . PASSWORD . $data['trans_id'] . strrev($cardPart) )); if (!hash_equals($expected, $data['hash'])) { http_response_code(400); exit('ERROR'); } // Process based on result if ($data['result'] === 'SUCCESS') { fulfillOrder($data['order_id'], $data['trans_id']); } echo 'OK'; Copyimport hashlib, hmac data = request.form.to_dict() # Use card from callback if present (SUCCESS), otherwise use stored mask (DECLINED) card_mask = data.get('card', stored_card_mask) card_part = card_mask[:6] + card_mask[-4:] raw = stored_email[::-1] + PASSWORD + data['trans_id'] + card_part[::-1] expected = hashlib.md5(raw.upper().encode()).hexdigest() if not hmac.compare_digest(expected, data.get('hash','')): return 'ERROR', 400 if data['result'] == 'SUCCESS': fulfill_order(data['order_id']) return 'OK' Copyconst rev = s => s.split('').reverse().join(''); app.post('/webhook', express.urlencoded({extended:false}), (req,res) => { const d = req.body; // Use card from callback if present (SUCCESS), otherwise use stored mask (DECLINED) const cardMask = d.card || storedCardMask; const cp = cardMask.slice(0,6) + cardMask.slice(-4); const raw = rev(storedEmail) + PASSWORD + d.trans_id + rev(cp); const exp = crypto.createHash('md5').update(raw.toUpperCase()).digest('hex'); if (!crypto.timingSafeEqual(Buffer.from(exp), Buffer.from(d.hash||''))) return res.status(400).send('ERROR'); if (d.result === 'SUCCESS') fulfillOrder(d.order_id); res.send('OK'); }); Cascading Behaviour# When cascading is enabled (auto-retry across MIDs on decline): General case: You receive only a callback for the last payment attempt with the final status. Particular case: If a redirect is required (e.g. 3DS), you also receive a callback for the first attempt with redirect data. The last-attempt callback includes only the final status. Intermediate attempts are not sent. The trans_id may differ between first and final callbacks. Do not assume you will receive a first-attempt callback. Extended Callback Data# Additional fields if configured in admin panel (Configuration → Protocol Mappings → "Add Extended Data to Callback"): connector_name, rrn, approval_code, gateway_id / extra_gateway_id, merchant_name / mid_name, issuer_country / issuer_bank, brand, arn, extended_data. Go-Live Checklist# Complete these items before requesting production credentials. Technical ItemDetail SALE tested. SuccessCard 4111111111111111 exp 01/2038 returns SUCCESS/SETTLED SALE tested. DeclineExp 02/2038 returns DECLINED 3DS redirect handledExp 05/2038 → redirect → return → SUCCESS. Exp 06/2038 → DECLINED Callback handler implementedEndpoint returns plain OK. Hash verified using Formula 2. Tested with all result types. CREDITVOID testedFull and partial refunds work correctly VOID tested (if used)Same-day cancellation on SETTLED transaction GET_TRANS_STATUS testedStatus polling works as fallback when callback delayed Error handling implementedAll error codes handled gracefully. User sees meaningful messages. Idempotent order IDsEach payment attempt uses a unique order_id. Duplicates handled. IP addresses providedProduction server IPs sent to CentaPay for whitelisting Callback URL configuredProduction callback URL registered with CentaPay HTTPS everywhereAll endpoints use TLS. No plain HTTP. If Using Recurring ItemDetail RECURRING_SALE testedInitial SALE with recurring_init=Y, then RECURRING_SALE with token RETRY testedSoft decline → RETRY → final result via callback Schedule ops tested (if used)CREATE, PAUSE, RUN, DELETE, SCHEDULE_INFO, DESCHEDULE If Using Payouts ItemDetail CREDIT2CARD testedTest card 4601541833776519 returns SUCCESS Formula 5 hash verifiedRequest hash uses Formula 5 (not Formula 1) Formula 6 callback hashCallback verification uses Formula 6 (not Formula 2) If Using Digital Wallets ItemDetail Apple Pay: Merchant ID configuredCertificates and keys uploaded to admin panel Apple Pay: Domains verifiedAll payment domains registered in Apple Developer Google Pay: Integration checklistCompleted per Google's requirements Google Pay: Domains verifiedVerified in Google Business Console Formula 8 hash usedWallet SALE uses email + PASSWORD only Compliance ItemDetail PCI DSS evidenceSAQ D or full assessment (for raw card S2S). Reduced scope for wallet-only. KYB completeBusiness verification documents submitted and approved Prohibited MCCs reviewedBusiness model confirmed against prohibited categories Client agreement signedExecuted with CentaPay Commercial ItemDetail Production credentials receivedSeparate CLIENT_KEY, PASSWORD, PAYMENT_URL for production Settlement currency confirmedUSD or preferred currency agreed with CentaPay MID configuredProduction MID mapped to your account by CentaPay ops Contact support@centapay.com when all items are complete to begin the go-live process. Which formula verifies which callback# The request and the callback almost never sign with the same construction. This is the fact that costs integrators the most time, because a single shared verification routine passes on most callbacks and fails on a few, which reads as a problem with those operations rather than with the hashing. CallbackRequest signs withCallback verifies with SALE, cardFormula 1Formula 2 SALE, stored tokenFormula 1, token variantFormula 2 SALE, walletFormula 8Formula 2 CAPTUREFormula 2Formula 2 CREDITVOIDFormula 2Formula 2 VOIDFormula 2the Void signature CREDIT2CARDFormula 5Formula 6 RECURRING_SALE, RETRYFormula 1Formula 2 CHARGEBACKplatform-initiatedFormula 2 When each callback fires is not documented. This table shows how to verify a callback once it has arrived. It does not say what causes one to arrive, and neither does anything else on this site, because the platform has not specified it. What is published: every callback names an action and an outcome, and all 19 callback messages with their full field sets are in the AsyncAPI document, generated from the same schemas as the API reference. What is not: the event that triggers each callback, and when it is sent. This has been raised with the platform and an answer is expected. Until it arrives this page states the gap rather than filling it, because a wrong statement about when money moves is worse than an incomplete one. Write your handler to be driven by what a callback contains rather than by when you expect it. Verify the signature, read the status, and treat the callback as the authoritative outcome of the transaction. The two marked rows are the ones a shared routine gets wrong. A VOID callback uses the Void signature, which is a plain MD5 with no email and no card fragment, and CREDIT2CARD is the only action whose callback formula differs from Formula 2 by adding the trans_id. Every formula, with a worked vector for each, is on the API reference. The field set each callback carries is under Callback Parameters by Action above. Where to go next# Operations covers what happens after a payment is taken. If something is not behaving as described here, troubleshooting lists the symptoms and their causes. Troubleshooting Symptoms you will actually hit, and what causes them. API reference Every parameter of every action, callback parameters and the error code table. Guides Task-oriented walkthroughs, one job end to end. Technical questions go to support@centapay.com. --- ## Troubleshooting Source: https://www.centapay.com/docs/operations/troubleshooting Last updated 6 August 2026 Troubleshooting# Symptoms you will actually hit, and what causes them. Availability. CentaPay is pre-launch. Production is expected in Q4 2026, with sandbox access ahead of it. Coverage differs by market, and the payment methods available differ by market too. The coverage table on our main site is the single source for what is live where, and for current dates. Nothing in this documentation should be read as a service available today. Every entry here is a failure mode documented in the reference. Nothing on this page is speculative: if a cause is not established, the symptom says so rather than offering a guess. Payloads below are illustrative and use the same sample credentials as the guides, so every hash on this page is reproducible: a payer_email of john@example.com, a PASSWORD of SANDBOX_PASSWORD, and the sandbox test card. Field names, their presence or absence, and every hash are exact. Requests get no response at all# Not an error, not a timeout you can distinguish from a network fault. Nothing. Requests from un-whitelisted IP addresses are rejected silently. That is the documented behaviour, and it is indistinguishable from your own connectivity failing. Before assuming a network problem, confirm every address your server can send from is on the whitelist, including the ones you forgot: a second data centre, a NAT gateway, a scheduled job running somewhere else. Separately, you cannot process payments at all until the S2S CARD protocol has been mapped to your account during onboarding. Both are configuration held by CentaPay, not something you can change from your side. Every request fails authentication# Almost always the wrong formula for the action, not a wrong password. The formulas are not interchangeable, and picking the wrong one produces an authentication failure that says nothing about which part was wrong. SendingSigns with SALE with card dataFormula 1 SALE with card_tokenFormula 1 variant, token replaces the card fragment SALE with a walletFormula 8, email and password only CAPTURE, CREDITVOID, VOIDFormula 2 RECURRING_SALE, RETRYFormula 1 CREDIT2CARDFormula 5 CREATE_SCHEDULEFormula 3 Other schedule operationsFormula 4 Three specific traps inside that table. A wallet sale signed with Formula 1 fails, because Formula 8 has no card term and a wallet sends no card fields. RECURRING_SALE signs with Formula 1, which needs the card's first six and last four digits, but the request itself sends no card fields. Those digits must come from the mask you stored at the initial sale. CREDIT2CARD signs with Formula 5, which has no email term at all. Callback verification fails but requests work# Three separate causes, all common. The formula differs between request and callback. This is the norm, not the exception. A wallet sale signs Formula 8 and verifies Formula 2. A VOID signs Formula 2 and verifies with the Void signature. A CREDIT2CARD signs Formula 5 and verifies Formula 6. One shared verification routine will pass on most callbacks and fail on those. trans_id is not uppercased. Every formula uppercases the whole concatenation. A lowercase UUID passed through unchanged produces a silent mismatch. The callback has no card field. Declined callbacks, 3DS and redirect callbacks, and undefined callbacks all omit card and card_expiration_date. Formula 2 needs a card fragment, so verification has to fall back to the mask you stored when you created the payment. If you did not store it, you cannot verify those callbacks at all. A redirect callback, with the two absent fields visible by their absence: Copyaction=SALE result=REDIRECT status=3DS order_id=TDS-2001 trans_id=e5f6a7b8-9c0d-4e1f-a2b3-c4d5e6f7a8b9 trans_date=2026-07-25 14:32:07 descriptor=CENTAPAY TDS amount=120000 currency=UZS hash=4008791549623c0d41f3cb0bd9816d79 That hash is the one the final callback for the same payment carries. Formula 2 reads the email, the password, trans_id and the card fragment, and none of those change between the two, so a verification routine that works on one works on the other once it has a card fragment to use. The masked PAN behaves exactly like a full one. First six characters joined to the last four, and the asterisks in the middle are never used. Callbacks stop arriving# Five consecutive timeouts within five minutes blocks your callback URL for fifteen minutes. Any successful response resets the counter. Unblocking early is done through the admin panel, under Configuration, Merchants, Edit Merchant. Two consequences worth designing around. Your endpoint must return the plain string OK. Any other content counts as a failure, and so does a timeout. Acknowledge first and process afterwards, so slow downstream work cannot trip the counter. All merchants sharing that URL are affected. One slow consumer takes the others down with it, which is the argument against a single shared callback endpoint across environments or entities. How many times a callback is delivered# Four attempts: one immediately, then at fifteen, thirty and forty-five minutes. A callback that is never acknowledged across all four is not delivered again. Read that as the platform's schedule for reaching you. It is not advice about retrying your own failed API requests, which is a different situation with a different answer, and one these pages do not give. There is no delivery log. Nothing exposes the individual attempts, their timings or their responses. A callback can be resent by hand from Transaction Details in the admin panel, and that is the only control over delivery you have. Design reconciliation around your own record of what you received, not around a platform log, because there is not one to query. A payment succeeded but the customer was not charged, or vice versa# You are reading the synchronous response as the outcome. It is not. The synchronous response tells you the request was accepted and what happened at that instant. result can be UNDEFINED, which is not an error and frequently settles. It can be REDIRECT, which means authentication is still ahead. The callback is authoritative in every case. Duplicate orders after a timeout# order_id must be unique per payment. Submitting one already used 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. POSThttps://{PAYMENT_URL}/post cURLPHPPythonNode.js Copycurl -X POST https://{PAYMENT_URL}/post \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "action=GET_TRANS_STATUS_BY_ORDER" \ -d "client_key={CLIENT_KEY}" \ -d "order_id=TAP-1001" \ -d "hash=a4593af2e3d7c92da965d39a5a2c27e7" Copy$url = 'https://{PAYMENT_URL}/post'; $fields = [ 'action' => 'GET_TRANS_STATUS_BY_ORDER', 'client_key' => '{CLIENT_KEY}', 'order_id' => 'TAP-1001', 'hash' => 'a4593af2e3d7c92da965d39a5a2c27e7', ]; $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); Copyimport requests from urllib.parse import urlencode url = 'https://{PAYMENT_URL}/post' fields = { 'action': 'GET_TRANS_STATUS_BY_ORDER', 'client_key': '{CLIENT_KEY}', 'order_id': 'TAP-1001', 'hash': 'a4593af2e3d7c92da965d39a5a2c27e7', } body = urlencode(fields) response = requests.post( url, data=body, headers={'Content-Type': 'application/x-www-form-urlencoded'}, ) result = response.json() Copyconst url = 'https://{PAYMENT_URL}/post' const fields = { 'action': 'GET_TRANS_STATUS_BY_ORDER', 'client_key': '{CLIENT_KEY}', 'order_id': 'TAP-1001', 'hash': 'a4593af2e3d7c92da965d39a5a2c27e7', } 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 query sends no card fields but Formula 7 signs with them, so the first six and last four digits come from the mask you stored, exactly as they do for RECURRING_SALE. The response carries status, plus decline_reason if the payment declined. With cascading enabled this returns the most recent transaction only. Blind resubmission creates duplicates, and where cascading is enabled a single payment request can already have generated several underlying transactions, so a retry can multiply rather than repeat. A capture or refund is rejected on amount# Both are arithmetic, and both are terminal for the request as sent. Capture amount exceeding the authorised amount returns 208004. Refund amount exceeding the payment returns 208006, and reversal amount exceeding it returns 208008. Copy{ "result": "ERROR", "error_message": "Description of the error", "error_code": 208004 } result: ERROR with an error_code is a validation failure, and it is a different shape from a declined transaction, which returns result: DECLINED with a decline_reason. A handler that branches on result alone sees these as the same thing. Note the asymmetry that catches people: one partial capture is allowed, and the remainder of the authorisation is then gone. Multiple partial refunds are allowed. Code written for one does not transfer to the other. A partial refund callback says SETTLED# That is correct and is not a failure. A partial refund reports status: SETTLED because the original transaction keeps its settled status. A full refund reports REFUND or REVERSAL. A VOID was declined# A declined VOID returns result: DECLINED with status: SETTLED, leaving the original transaction settled and a customer expecting their money back. VOID only applies to transactions in SETTLED status from SALE, CAPTURE or RECURRING_SALE, and only on the same financial day. Outside that window the operation is CREDITVOID. A payout response is missing fields# CREDIT2CARD success responses do not include amount or currency, unlike a SALE. A handler that reads them unconditionally breaks on the success path. A success response in full. Every field it has, and nothing where amount and currency would be: Copy{ "action": "CREDIT2CARD", "result": "SUCCESS", "status": "SETTLED", "order_id": "PAY-5001", "trans_id": "c9d0e1f2-3a4b-4c5d-9e6f-7a8b9c0d1e2f", "trans_date": "2026-07-25 18:12:44", "descriptor": "CENTAPAY PAYOUT" } Declined responses carry decline_reason in place of descriptor, and undefined ones carry status: PREPARE. CREDIT2CARD is also the one action whose callback does not verify with Formula 2. It uses Formula 6, and Pay out to a card carries the worked callback and both digests. A wallet payment declines after the customer has paid# allowedCardNetworks populates the Google Pay payment sheet. Any network listed is offered to the cardholder. If they pick one your account cannot acquire, Google returns a valid token, the sale declines, and the customer has already been told they paid. List only the networks enabled on your account, confirmed at onboarding. Also check that the Environment setting matches, TEST or PRODUCTION, between your Google Pay configuration and your CentaPay account. Recurring will not start in sandbox# Only the test card with expiry 01/2038 returns a recurring_token. The other expiries will not produce one. If a recurring charge then fails, note that RETRY applies to soft declines only. Hard declines such as a stolen card or fraud will not succeed on retry. The 3DS redirect does nothing# redirect_params varies by acquirer. It may contain PaReq, TermUrl or other values, it may be empty, and it may be absent altogether, most commonly when redirect_method is GET. Code that assumes an array here fails on the acquirer that sends nothing. Check for presence before iterating. What next# Operations covers callback delivery rules and the go-live checklist. The API reference carries the full error code table with retry classification. Each guide documents the failure modes specific to its operation. Technical questions go to support@centapay.com. --- ## PCI DSS scope Source: https://www.centapay.com/docs/pci-dss Last updated 6 August 2026 Availability. CentaPay is pre-launch. Production is expected in Q4 2026, with sandbox access ahead of it. Coverage differs by market, and the payment methods available differ by market too. The coverage table on our main site is the single source for what is live where, and for current dates. Nothing in this documentation should be read as a service available today. PCI DSS# Integration TypePCI Requirement S2S with raw card dataPCI DSS SAQ D or full on-site assessment S2S with Apple Pay / Google Pay tokensReduced scope. Confirm with your QSA S2S with CentaPay card token (card_token)Reduced scope for subsequent charges (initial charge still requires raw card data) --- ## Quickstart Source: https://www.centapay.com/docs/quickstart Last updated 6 August 2026 Quickstart# Take a card payment on the CentaPay sandbox and verify the callback that confirms it. Availability. CentaPay is pre-launch. Production is expected in Q4 2026, with sandbox access ahead of it. Coverage differs by market, and the payment methods available differ by market too. The coverage table on our main site is the single source for what is live where, and for current dates. Nothing in this documentation should be read as a service available today. Budget about fifteen minutes. Step 1 needs no credentials, so you can validate your hash implementation while your account is being set up. The API is server-to-server. Every operation is a POST to a single endpoint with Content-Type: application/x-www-form-urlencoded, and every response is JSON. There are no bearer tokens and no API key headers. Requests are signed with an MD5 hash. Before you begin# Credentials are issued by hand after we whitelist your infrastructure. Send three things to support@centapay.com: WhatWhy IP listThe addresses your server will send from. Requests from any other address are rejected without a response, so an incomplete list looks like a network fault. Callback URLWhere we POST transaction results. Maximum 255 characters. Mandatory if your account supports 3D Secure. Contact emailThe person who will monitor transactions, handle refunds and answer operational queries. You receive three values back: ValueUse CLIENT_KEYSent as a parameter on every request. PASSWORDUsed only to compute hashes. It never leaves your server and is never sent in a request. PAYMENT_URLYour endpoint host. Sandbox and production are different hosts. Two conditions must also be true before your first call succeeds. Your sending IP addresses must be whitelisted, and the S2S CARD protocol must be mapped to your account. Both are done by CentaPay during onboarding. If either is missing you cannot process payments. Throughout this guide, {PAYMENT_URL}, {CLIENT_KEY} and {PASSWORD} are placeholders for the values issued to you. 1. Check your hash implementation# Do this first. A wrong hash is the most common integration failure, and it is indistinguishable from a credentials problem when you are looking at an error response. You can complete this step offline, before your account exists. A SALE with card data is signed with Formula 1: Copymd5(strtoupper( strrev(payer_email) . PASSWORD . strrev(substr(card_number, 0, 6) . substr(card_number, -4)) )) Three things catch people out. The card component is the first six digits joined to the last four with nothing in between, and that ten-character string is reversed as a unit rather than reversing each part. The whole concatenated string is uppercased after assembly, not before. And if a formula references an optional parameter you are not sending, leave it out of the calculation entirely. Test vector Run your implementation against these inputs. If you do not get this digest, stop and fix it before going further. InputValue payer_emailjohn@example.com PASSWORDSANDBOX_PASSWORD card_number4111111111111111 StageValue Card component4111111111 Card component reversed1111111114 Concatenated and uppercasedMOC.ELPMAXE@NHOJSANDBOX_PASSWORD1111111114 Expected hashc8b58f1a6a6083fd4f0bd17d3ef58a45 Implementations Every sample on this site is executed against a real interpreter before it is published, and its fields are compared to the cURL it derives from. The versions they are verified on are PHP 8.1, Python 3.9, Node 18 and curl 7.68. Older interpreters are not tested and may differ, most likely in how they encode the form body. cURLPHPPythonNode.js Copy# cURL and shell EMAIL="john@example.com" PASSWORD="SANDBOX_PASSWORD" PAN="4111111111111111" rev() { echo -n "$1" | rev; } CARD_PART="${PAN:0:6}${PAN: -4}" RAW="$(rev "$EMAIL")${PASSWORD}$(rev "$CARD_PART")" HASH=$(printf '%s' "$RAW" | tr '[:lower:]' '[:upper:]' | openssl md5 -r | cut -d' ' -f1) echo "$HASH" Copy str: card_part = pan[:6] + pan[-4:] raw = email[::-1] + password + card_part[::-1] return hashlib.md5(raw.upper().encode()).hexdigest() print(sale_hash("john@example.com", "SANDBOX_PASSWORD", "4111111111111111")) Copyconst crypto = require('crypto'); const rev = (s) => s.split('').reverse().join(''); function saleHash(email, password, pan) { const cardPart = pan.slice(0, 6) + pan.slice(-4); const raw = rev(email) + password + rev(cardPart); return crypto.createHash('md5').update(raw.toUpperCase()).digest('hex'); } console.log(saleHash('john@example.com', 'SANDBOX_PASSWORD', '4111111111111111')); If you would rather check a single value by hand than wire up a test, the Hash Calculator further down this page computes Formulas 1, 2, 5 and 8 in the browser, and the Request Builder assembles a complete signed SALE for you. Neither sends anything anywhere. 2. Send your first payment# This is a UZS sale for 120,000 som. Amounts in UZS and KZT are integers with no decimal component. Amounts in USD are decimal, formatted as XX.XX. order_id must be unique for every payment you create. Reusing one returns error code 400. POSThttps://{PAYMENT_URL}/post cURLPHPPythonNode.js Copycurl -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=QS-0001" \ -d "order_amount=120000" \ -d "order_currency=UZS" \ -d "order_description=Quickstart test payment" \ -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 "payer_email=john@example.com" \ -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={CALCULATED_HASH}" Copy$url = 'https://{PAYMENT_URL}/post'; $fields = [ 'action' => 'SALE', 'client_key' => '{CLIENT_KEY}', 'order_id' => 'QS-0001', 'order_amount' => '120000', 'order_currency' => 'UZS', 'order_description' => 'Quickstart test payment', 'card_number' => '4111111111111111', 'card_exp_month' => '01', 'card_exp_year' => '2038', 'card_cvv2' => '123', 'payer_first_name' => 'John', 'payer_last_name' => 'Smith', 'payer_email' => 'john@example.com', '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' => '{CALCULATED_HASH}', ]; $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); Copyimport requests from urllib.parse import urlencode url = 'https://{PAYMENT_URL}/post' fields = { 'action': 'SALE', 'client_key': '{CLIENT_KEY}', 'order_id': 'QS-0001', 'order_amount': '120000', 'order_currency': 'UZS', 'order_description': 'Quickstart test payment', 'card_number': '4111111111111111', 'card_exp_month': '01', 'card_exp_year': '2038', 'card_cvv2': '123', 'payer_first_name': 'John', 'payer_last_name': 'Smith', 'payer_email': 'john@example.com', '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': '{CALCULATED_HASH}', } body = urlencode(fields) response = requests.post( url, data=body, headers={'Content-Type': 'application/x-www-form-urlencoded'}, ) result = response.json() Copyconst url = 'https://{PAYMENT_URL}/post' const fields = { 'action': 'SALE', 'client_key': '{CLIENT_KEY}', 'order_id': 'QS-0001', 'order_amount': '120000', 'order_currency': 'UZS', 'order_description': 'Quickstart test payment', 'card_number': '4111111111111111', 'card_exp_month': '01', 'card_exp_year': '2038', 'card_cvv2': '123', 'payer_first_name': 'John', 'payer_last_name': 'Smith', 'payer_email': 'john@example.com', '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': '{CALCULATED_HASH}', } 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() payer_ip is the cardholder's address, not your server's. term_url_3ds is required even on this non-3DS scenario. About the test card Every sandbox scenario uses the same test PAN, and the expiry date selects the outcome. Sandbox scheme coverage is a testing convenience and does not indicate which schemes are enabled on your live account. Your enabled schemes are confirmed during onboarding. ExpiryOutcome 01/2038Immediate success. The only expiry that returns a recurring token. 02/2038Declined by processing. 03/2038AUTH succeeds, CAPTURE declines. 05/20383D Secure challenge, then success. 06/20383D Secure challenge, then decline. 12/2038Redirect, then success. 12/2039Redirect, then decline. Any three-digit CVV works. No funds move. 3. Read the synchronous response# Copy{ "action": "SALE", "result": "SUCCESS", "status": "SETTLED", "order_id": "QS-0001", "trans_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "trans_date": "2026-07-25 14:32:07", "descriptor": "CENTAPAY QUICKSTART", "amount": "120000", "currency": "UZS" } Record trans_id. You will need it for captures, refunds, status queries and callback verification. Do not fulfil an order on this response. The synchronous response tells you the request was accepted and what happened at that instant. It is not the final outcome. result can also come back as REDIRECT when 3D Secure is required, or UNDEFINED when the outcome is not yet known. The callback is authoritative in every case, including this one. 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 generate several underlying transactions. 4. Receive and verify the callback# The callback is the result. Everything else is provisional. The contract We POST to your callback URL as application/x-www-form-urlencoded. Your endpoint returns the plain string OK if it accepted the notification, or ERROR if it did not. Anything else, including a timeout, counts as a failure. Failures are penalised at the URL level, not the account level. Five timeouts within five minutes block that callback URL for fifteen minutes, and every merchant sharing the URL stops receiving notifications for the duration. A single successful response resets the counter. The block lifts automatically, and can also be cleared immediately by CentaPay on request. Two consequences for your design. Return OK before doing slow work: acknowledge the callback, queue the job, process it out of band. And do not share one callback URL across environments or entities, because one broken consumer takes the others down with it. What arrives Field names, presence and formats below are exact. Values are illustrative. Copyaction=SALE result=SUCCESS status=SETTLED order_id=QS-0001 trans_id=a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d trans_date=2026-07-25 14:32:09 descriptor=CENTAPAY QUICKSTART amount=120000 currency=UZS card=411111****1111 card_expiration_date=01/2038 hash=9c49c27cea14b586c4ad8f0bc0dbda24 Three properties of this payload drive most integration bugs. The payer's email is not in it. Formula 2 needs the email, so you must store it against your order_id at the point you create the payment and retrieve it when the callback arrives. The card field is present on success and absent on decline. A declined callback carries a reduced field set: action, result, status, order_id, trans_id, trans_date, decline_reason, custom_data, digital_wallet, pan_type and hash. It has no card, descriptor, amount or currency. Verification still needs a card component, so fall back to the mask you stored from your own request. The callback for a 3D Secure payment arrives more than once. You will receive a redirect callback and then a final callback. Treat trans_id plus result as the unit of work and make your handler idempotent, because a repeated callback must not create a second fulfilment. Verifying the hash Callbacks for every action except CREDIT2CARD and VOID use Formula 2: Copymd5(strtoupper( strrev(payer_email) . PASSWORD . trans_id . strrev(substr(card, 0, 6) . substr(card, -4)) )) The masked PAN behaves exactly like the full PAN. First six characters joined to the last four gives 4111111111, and the asterisks in the middle are never used. trans_id is uppercased along with everything else. A lowercase UUID that is not uppercased before hashing is a silent mismatch, and it is the single most common cause of "my callback verification fails but my request hash works". Test vector InputValue payer_emailjohn@example.com PASSWORDSANDBOX_PASSWORD trans_ida1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d card411111****1111 StageValue Card component4111111111 Concatenated and uppercasedMOC.ELPMAXE@NHOJSANDBOX_PASSWORDA1B2C3D4-5E6F-4A7B-8C9D-0E1F2A3B4C5D1111111114 Expected hash9c49c27cea14b586c4ad8f0bc0dbda24 A working receiver PHPPythonNode.js Copy s.split('').reverse().join(''); app.post('/callback', express.urlencoded({ extended: false }), (req, res) => { const d = req.body; const storedEmail = lookupEmail(d.order_id); const storedMask = lookupCardMask(d.order_id); const cardMask = d.card || storedMask; const cardPart = cardMask.slice(0, 6) + cardMask.slice(-4); const raw = rev(storedEmail) + PASSWORD + d.trans_id + rev(cardPart); const expected = crypto.createHash('md5').update(raw.toUpperCase()).digest('hex'); const given = Buffer.from(d.hash || '', 'utf8'); const want = Buffer.from(expected, 'utf8'); if (given.length !== want.length || !crypto.timingSafeEqual(given, want)) { return res.status(400).send('ERROR'); } enqueue(d); res.send('OK'); }); Compare hashes in constant time and reject anything that does not match. An unverified callback is an unauthenticated instruction to release goods. 5. See a 3D Secure transaction# Most Kazakhstan and Uzbekistan card traffic is authenticated, so the flow in step 2 is the exception rather than the rule. Repeat your SALE with expiry 05/2038 and the response changes shape: Copy{ "action": "SALE", "result": "REDIRECT", "status": "3DS", "order_id": "QS-0002", "trans_id": "b2c3d4e5-6f7a-4b8c-9d0e-1f2a3b4c5d6e", "trans_date": "2026-07-25 14:41:55", "descriptor": "CENTAPAY QUICKSTART", "amount": "120000", "currency": "UZS", "redirect_url": "https://acs.example/3ds/challenge", "redirect_method": "POST", "redirect_params": { "PaReq": "eJxVUt1...", "TermUrl": "https://yoursite.example/3ds-return" } } You post the cardholder's browser to redirect_url using redirect_method, carrying redirect_params as form fields. They authenticate with their issuer and return to your term_url_3ds. The final outcome arrives by callback, not on the return leg. Treat the return purely as a signal to show a waiting state. redirect_params varies by acquirer and can be empty or absent, most often when redirect_method is GET. Always check before iterating it. Handling this properly, including the iframe and term_url_target cases, is covered in the 3D Secure guide. What next# Take a payment covers the full SALE surface, tokenisation and the AUTH plus CAPTURE flow. Handle 3D Secure covers the redirect in depth. Refund and reverse covers CREDITVOID and VOID. The API reference lists every parameter of every operation. Before you move to production, work through the go-live checklist. It covers callback verification, IP whitelisting on your production addresses, error handling and reconciliation. Technical questions go to support@centapay.com. Commercial questions go to info@centapay.com. --- ## API reference Source: https://www.centapay.com/docs/reference Last updated 6 August 2026 API reference# Credentials, hash formulas, every action and its parameters, callback parameters, error codes and PCI scope. Availability. CentaPay is pre-launch. Production is expected in Q4 2026, with sandbox access ahead of it. Coverage differs by market, and the payment methods available differ by market too. The coverage table on our main site is the single source for what is live where, and for current dates. Nothing in this documentation should be read as a service available today. Integration Model# CentaPay uses a Server-to-Server (S2S) API only. Your server submits payment requests directly to our platform. Results are delivered both synchronously (JSON response) and asynchronously (callback to your webhook). PCI DSS required. Because card data is handled server-to-server, you must be PCI DSS compliant or handle only tokenised card data. Contact support@centapay.com to discuss your compliance posture before going live. Credentials & Environments# Before you get an account, you must provide the following data to CentaPay: DataDescription IP listIP addresses from which your server will send requests. Requests from un-whitelisted IPs are rejected silently. Callback URLURL that will receive transaction result notifications (webhooks). Maximum 255 characters. Mandatory if your account supports 3D Secure. Contact emailEmail of the person who will monitor transactions, conduct refunds, and handle operational queries. You will receive the following credentials: CredentialDescriptionWhere Used CLIENT_KEYUnique key identifying your account (UUID format). Corresponds to the Merchant key field in the admin panel.Sent as a POST parameter on every request PASSWORDSecret used only to generate the hash signature. Corresponds to the Password field in the admin panel.Hash calculation only. Never sent over the wire PAYMENT_URLBase endpoint URL for your account (different for sandbox and production).All API requests Store PASSWORD securely. Use an environment variable or secrets manager. Never hardcode it in source files or commit it to version control. Credential rotation. There is no self-service PASSWORD rotation. To rotate your PASSWORD, following a personnel change, suspected compromise, or as a periodic security measure, contact support@centapay.com. CentaPay will issue a new PASSWORD and coordinate the switchover window with you. We recommend rotating credentials at least annually and immediately after any suspected exposure. Protocol Mapping# CentaPay maps specific protocol types to each merchant account. You cannot process payments until the relevant protocol has been mapped by CentaPay's operations team during onboarding. Confirm with your account manager before testing. ProtocolUsed ForEndpoint S2S CARDCard payments, Apple Pay, Google Pay, CREDIT2CARD payoutshttps://{PAYMENT_URL}/post All requests must use Content-Type: application/x-www-form-urlencoded. Responses are JSON-encoded. IP Whitelisting & Callbacks# Requests from un-whitelisted IP addresses are rejected without a response. If you are testing from a development machine with a dynamic IP, use a tunnelling tool like ngrok for callbacks and let your account manager know your IPs need frequent updating during sandbox testing. Authentication# CentaPay uses MD5-based request signing. Every API request includes a hash parameter, a signature computed from specific request fields and your account password. There are no Bearer tokens or API key headers. How It Works# The hash is computed by reversing specific field values, concatenating them with your PASSWORD, converting to uppercase, then taking the MD5 digest. The exact formula varies by action, see the table below. The PASSWORD never leaves your server. Only the resulting hash is transmitted. If a formula references optional parameters that you do not send in the request, omit them from the hash calculation. Formula Reference# In all formulas below, strrev() means reverse the string, strtoupper() means convert to uppercase, and md5() means compute the MD5 hex digest. Formula 1: SALE, RETRY, RECURRING_SALE If the formula contains optional parameters that you do not send in the request, please ignore that parameter for the hash. FormulaPHPPythonNode.js Copymd5(strtoupper( strrev(email) . PASSWORD . strrev(substr(card_number, 0, 6) . substr(card_number, -4)) )) Copyfunction hashFormula1(string $email, string $password, string $cardNumber): string { $cardPart = substr($cardNumber, 0, 6) . substr($cardNumber, -4); $raw = strrev($email) . $password . strrev($cardPart); return md5(strtoupper($raw)); } Copyimport hashlib def hash_formula_1(email: str, password: str, card_number: str) -> str: card_part = card_number[:6] + card_number[-4:] raw = email[::-1] + password + card_part[::-1] return hashlib.md5(raw.upper().encode()).hexdigest() Copyconst crypto = require('crypto'); function hashFormula1(email, password, cardNumber) { const cardPart = cardNumber.slice(0, 6) + cardNumber.slice(-4); const raw = strrev(email) + password + strrev(cardPart); return crypto.createHash('md5').update(raw.toUpperCase()).digest('hex'); } function strrev(s) { return s.split('').reverse().join(''); } When card_token is provided instead of card data: Formula Copymd5(strtoupper( strrev(email) . PASSWORD . strrev(card_token) )) When digital_wallet is used (Apple Pay / Google Pay), Formula 8: Formula Copymd5(strtoupper( strrev(email) . PASSWORD )) EMAILPASSWORDCARD NUMBER Compute Computed in your browser. Nothing typed here leaves the page, but use a sandbox password rather than a live one. Formula 2: CAPTURE, CREDITVOID, VOID, GET_TRANS_STATUS, Callback verification FormulaPHPPythonNode.js Copymd5(strtoupper( strrev(email) . PASSWORD . trans_id . strrev(substr(card_number, 0, 6) . substr(card_number, -4)) )) Copyfunction hashFormula2(string $email, string $password, string $transId, string $cardNumber): string { $cardPart = substr($cardNumber, 0, 6) . substr($cardNumber, -4); $raw = strrev($email) . $password . $transId . strrev($cardPart); return md5(strtoupper($raw)); } // For callback verification, use the card mask from the callback // (first 6 + last 4 digits visible in the masked PAN) Copydef hash_formula_2(email: str, password: str, trans_id: str, card_number: str) -> str: card_part = card_number[:6] + card_number[-4:] raw = email[::-1] + password + trans_id + card_part[::-1] return hashlib.md5(raw.upper().encode()).hexdigest() Copyfunction hashFormula2(email, password, transId, cardNumber) { const cardPart = cardNumber.slice(0, 6) + cardNumber.slice(-4); const raw = strrev(email) + password + transId + strrev(cardPart); return crypto.createHash('md5').update(raw.toUpperCase()).digest('hex'); } EMAILPASSWORDTRANS IDCARD NUMBER Compute Computed in your browser. Nothing typed here leaves the page, but use a sandbox password rather than a live one. Formula 3: CREATE_SCHEDULE Formula Copymd5(strtoupper(strrev(PASSWORD))) PASSWORD Compute Computed in your browser. Nothing typed here leaves the page, but use a sandbox password rather than a live one. Formula 4: PAUSE / RUN / DELETE / SCHEDULE_INFO / DESCHEDULE Formula Copymd5(strtoupper(strrev(schedule_id + PASSWORD))) SCHEDULE IDPASSWORD Compute Computed in your browser. Nothing typed here leaves the page, but use a sandbox password rather than a live one. Formula 5: CREDIT2CARD Formula Copymd5(strtoupper( PASSWORD . strrev(substr(card_number, 0, 6) . substr(card_number, -4)) )) // With card_token instead: md5(strtoupper(PASSWORD . strrev(card_token))) PASSWORDCARD NUMBER Compute Computed in your browser. Nothing typed here leaves the page, but use a sandbox password rather than a live one. Formula 6: CREDIT2CARD callbacks & GET_TRANS_STATUS (for CREDIT2CARD) Formula Copymd5(strtoupper( PASSWORD . trans_id . strrev(substr(card_number, 0, 6) . substr(card_number, -4)) )) PASSWORDTRANS IDCARD NUMBER Compute Computed in your browser. Nothing typed here leaves the page, but use a sandbox password rather than a live one. Formula 7: GET_TRANS_STATUS_BY_ORDER Formula Copymd5(strtoupper( strrev(email) . PASSWORD . order_id . strrev(substr(card_number, 0, 6) . substr(card_number, -4)) )) EMAILPASSWORDORDER IDCARD NUMBER Compute Computed in your browser. Nothing typed here leaves the page, but use a sandbox password rather than a live one. Void signature (VOID callbacks) Validates VOID callback hashes only - the VOID request itself signs with Formula 2. Unlike Formulas 1-8, the Void signature is a plain MD5 with no SHA1 wrapper. Formula Copyhash = md5(strtoupper(strrev(trans_id)) . PASSWORD) TRANS IDPASSWORD Compute Computed in your browser. Nothing typed here leaves the page, but use a sandbox password rather than a live one. Formula Quick Reference# ActionFormulaKey Inputs SALE (card)Formula 1email + PASSWORD + card first6/last4 SALE (card_token)Formula 1 variantemail + PASSWORD + card_token SALE (digital wallet)Formula 8email + PASSWORD only CAPTUREFormula 2email + PASSWORD + trans_id + card CREDITVOIDFormula 2email + PASSWORD + trans_id + card VOIDFormula 2email + PASSWORD + trans_id + card RECURRING_SALEFormula 1email + PASSWORD + card first6/last4 RETRYFormula 1email + PASSWORD + card first6/last4 CREDIT2CARDFormula 5PASSWORD + card first6/last4 CREDIT2CARD callbackFormula 6PASSWORD + trans_id + card GET_TRANS_STATUSFormula 2 (or 6 for CREDIT2CARD)See formula GET_TRANS_DETAILSFormula 2 (or 6 for CREDIT2CARD)See formula GET_TRANS_STATUS_BY_ORDERFormula 7 (or 6 for CREDIT2CARD)email + PASSWORD + order_id + card CREATE_SCHEDULEFormula 3PASSWORD only Other schedule opsFormula 4schedule_id + PASSWORD Callback (all except CREDIT2CARD, VOID)Formula 2email + PASSWORD + trans_id + card Callback (CREDIT2CARD)Formula 6PASSWORD + trans_id + card Callback (VOID)Void signaturetrans_id + PASSWORD The interactive Hash Calculator in the Testing section lets you generate test hashes during development. Order IDs and duplicates.order_id must be unique per payment. Submitting a request with an already-used order_id returns error_code 400 (duplicate request). If an outcome is uncertain - timeout, network failure - do not resubmit blindly: query GET_TRANS_STATUS_BY_ORDER first, and treat the callback as the authoritative outcome, especially when cascading is enabled, since one payment request can create multiple intermediate transactions. Rate limits. The platform does not impose fixed global rate limits. Transaction limits, including daily limits per merchant identifier, are configured for each client during onboarding and can be adjusted on request. Callback Parameters by Action# SALE Callback (Success) ParameterDescription actionSALE resultSUCCESS statusPENDING / PREPARE / SETTLED order_idYour order ID trans_idCentaPay transaction ID trans_dateTimestamp (YYYY-MM-DD hh:mm:ss) amountTransaction amount currencyCurrency code cardMasked PAN (e.g. 411111****1111). For wallets: decrypted token PAN. card_expiration_dateCard expiry descriptorStatement descriptor hashCallback signature. Verify with Formula 2 recurring_tokenIf recurring_init=Y was sent schedule_idIf schedule used card_tokenIf req_token=Y was sent digital_walletgooglepay or applepay (if wallet used) pan_typeDPAN or FPAN (wallet transactions) exchange_rateFX rate (if currency exchange applied) exchange_rate_baseBase conversion rate (double conversion) exchange_currencyOriginal currency exchange_amountOriginal amount custom_dataEchoed custom data from request connector_name*, rrn*, approval_code*, gateway_id*, merchant_name*, issuer_country*, brand*, arn*, extended_data*See Extended Callback Data SALE Callback (Declined) Reduced field set. Declined callbacks do not include card, card_expiration_date, descriptor, amount, currency, card_token, recurring_token, or extended data. You must use the card mask and email stored from your original request when verifying the callback hash. ParameterDescription actionSALE resultDECLINED statusDECLINED order_idYour order ID trans_idCentaPay transaction ID trans_dateTimestamp decline_reasonHuman-readable decline reason custom_dataEchoed custom data from request digital_walletgooglepay or applepay (if wallet used) pan_typeDPAN or FPAN (wallet transactions) hashCallback signature. Verify with Formula 2 CAPTURE Callback OutcomeParameters Successaction: CAPTURE, result: SUCCESS, status: SETTLED, order_id, trans_id, amount, trans_date, descriptor, currency, hash (Formula 2). Extended data fields* if configured. Declinedaction: CAPTURE, result: DECLINED, status: PENDING, order_id, trans_id, decline_reason, hash Undefinedaction: CAPTURE, result: UNDEFINED, status: PENDING, order_id, trans_id, trans_date, descriptor, amount, currency, hash CREDITVOID Callback Success:action, result: SUCCESS, status (REFUND/REVERSAL/SETTLED), order_id, trans_id, creditvoid_date, amount, hash (Formula 2). Extended data fields* if configured. Declined:action, result: DECLINED, order_id, trans_id, decline_reason, hash (Formula 2). Undefined:result: UNDEFINED, status: SETTLED, order_id, trans_id, creditvoid_date, amount, hash (Formula 2). VOID Callback OutcomeParameters Successaction: VOID, result: SUCCESS, status: VOID, order_id, trans_id, trans_date, hash. Extended data fields* if configured. Declinedaction: VOID, result: DECLINED, status: SETTLED, order_id, trans_id, trans_date, decline_reason, hash Undefinedaction: VOID, result: UNDEFINED, status: PENDING / SETTLED, order_id, trans_id, trans_date, hash Void signature (VOID callbacks):md5(strtoupper(strrev(trans_id)) . PASSWORD). Validates VOID callback hashes only - the VOID request itself signs with Formula 2. Unlike Formulas 1-8, the Void signature is a plain MD5 with no SHA1 wrapper. CREDIT2CARD Callback Uses Formula 6 for hash (not Formula 2). This is the only action with a different callback hash formula. OutcomeParameters Successaction: CREDIT2CARD, result: SUCCESS, status: SETTLED, order_id, trans_id, trans_date, hash (Formula 6). Extended data fields* if configured. Declinedaction: CREDIT2CARD, result: DECLINED, status: DECLINED, order_id, trans_id, trans_date, decline_reason, hash (Formula 6) Undefinedaction: CREDIT2CARD, result: UNDEFINED, status: PREPARE, order_id, trans_id, trans_date, hash (Formula 6) * Extended data fields are included if configured in admin panel (Configuration → Protocol Mappings → "Add Extended Data to Callback"). API Reference: SALE# POSThttps://{PAYMENT_URL}/post ParameterFormatReqNotes actionSALEY client_keyUUIDY channel_id≤16 charsNSub-account routing order_id≤255 charsYYour unique ID order_amountNumberYInteger for KZT/UZS. Float XX.XX for USD. 0 allowed with auth=Y order_currency3-letterYKZT, UZS, USD order_description≤1024 charsY card_numberPANY*Optional if card_token or payment_token card_exp_monthMMY* card_exp_yearYYYYY* card_cvv23-4 digitsY**Optional if payment_token card_token64 charsNReplaces card fields digital_walletgooglepay/applepayNPair with payment_token payment_tokenStringNFrom Apple/Google Pay payer_first_name≤32 charsY payer_last_name≤32 charsY payer_middle_name≤32 charsN payer_birth_dateyyyy-MM-ddN payer_address≤255 charsY payer_address2≤255 charsN payer_house_number≤9 charsN payer_country2-letterYISO 3166-1 payer_state≤32 charsN payer_city≤40 charsY payer_district≤32 charsN payer_zip≤10 charsY payer_email≤256 charsY payer_phone≤32 charsY payer_phone_country_codeStringN payer_ipIPv4/IPv6Y term_url_3dsURL ≤1024Y3DS return URL term_url_target≤1024N_blank/_self/_parent/_top/iframe name authY/NNY = AUTH only (DMS) req_tokenY/NNRequest card token recurring_initY/NNInit recurring sequence schedule_idStringNLink to schedule parametersObjectNAcquirer-specific extra fields custom_dataObjectNEchoed in callback hashMD5 hexYFormula 1 (cards), Formula 8 (wallets) * Optional if card_token or payment_token provided. ** Optional if payment_token provided. Parameter precedence: If card_token and card data are both sent, card_token is ignored. If req_token and card_token are both sent, req_token is ignored. If payment_token and card data are both sent, payment_token is ignored. If card_token is specified, payment_token is ignored. Response: Success resultstatusKey Response Fields SUCCESSSETTLED / PENDING / PREPAREorder_id, trans_id, trans_date, descriptor, amount, currency, card_token*, recurring_token*, schedule_id*, digital_wallet, pan_type Response: 3DS Redirect resultstatusKey Response Fields REDIRECT3DS / REDIRECTorder_id, trans_id, trans_date, descriptor, amount, currency, redirect_url, redirect_params, redirect_method, digital_wallet, pan_type Response: Declined resultstatusKey Response Fields DECLINEDDECLINEDorder_id, trans_id, trans_date, descriptor, amount, currency, decline_reason, digital_wallet, pan_type Response: Undefined resultstatusKey Response Fields UNDEFINEDPENDING / PREPAREorder_id, trans_id, trans_date, descriptor, amount, currency, digital_wallet, pan_type, await callback API Reference: CAPTURE# POSThttps://{PAYMENT_URL}/post ParameterFormatReqNotes actionCAPTUREY client_keyUUIDY trans_idUUIDYFrom SALE (auth) response amountNumberNOmit for full capture. One partial capture allowed. hashMD5 hexYFormula 2 Response SUCCESS → status: SETTLED. DECLINED → status: PENDING + decline_reason. UNDEFINED → status: PENDING. All responses include: action, result, status, order_id, trans_id, trans_date, descriptor, amount, currency. API Reference: CREDITVOID# POSThttps://{PAYMENT_URL}/post ParameterFormatReqNotes actionCREDITVOIDY client_keyUUIDY trans_idUUIDY amountNumberNOmit for full refund. Multiple partials allowed. hashMD5 hexYFormula 2 Response Synchronous: result: ACCEPTED. Callback: SUCCESS with status: REFUND/REVERSAL (full) or SETTLED (partial). DECLINED includes decline_reason. API Reference: VOID# POSThttps://{PAYMENT_URL}/post ParameterFormatReqNotes actionVOIDY client_keyUUIDY trans_id≤255 charsY hashMD5 hexYFormula 2 Response resultstatusNotes SUCCESSVOIDTransaction voided successfully DECLINEDSETTLEDVoid rejected. Includes decline_reason. Original transaction remains settled. UNDEFINEDPENDING / SETTLEDStatus undetermined. Await callback for final result Same-day only. Applies to SALE, CAPTURE, RECURRING_SALE in SETTLED status. Void signature (VOID callbacks):md5(strtoupper(strrev(trans_id)) . PASSWORD). Validates VOID callback hashes only - the VOID request itself signs with Formula 2. Unlike Formulas 1-8, the Void signature is a plain MD5 with no SHA1 wrapper. API Reference: CREDIT2CARD# POSThttps://{PAYMENT_URL}/post ParameterFormatReqNotes actionCREDIT2CARDY client_keyUUIDY channel_id≤16 charsNSub-account order_id≤255 charsY order_amountNumberY order_currency3-letterY order_description≤1024 charsY card_numberPANYRecipient card payee_first_name≤32 charsNRecipient name payee_last_name≤32 charsN payee_middle_name≤32 charsN payee_birth_dateyyyy-MM-ddN payee_address≤255 charsN payee_address2≤255 charsN payee_country2-letterN payee_state≤32 charsN payee_city≤32 charsN payee_zip≤10 charsN payee_email≤256 charsN payee_phone≤32 charsN payer_first_name≤32 charsNSender name payer_last_name≤32 charsN payer_middle_name≤32 charsN payer_birth_dateyyyy-MM-ddN payer_address≤255 charsN payer_address2≤255 charsN payer_country2-letterN payer_state≤32 charsN payer_city≤32 charsN payer_zip≤10 charsN payer_email≤256 charsN payer_phone≤32 charsN payer_ipIPv4N parametersObjectNAcquirer-specific hashMD5 hexYFormula 5. Callback uses Formula 6. Reduced response fields. Unlike SALE, the CREDIT2CARD synchronous response does not include amount or currency on SUCCESS. All responses return: action, result, status, order_id, trans_id, trans_date. resultstatusAdditional Fields SUCCESSSETTLEDdescriptor DECLINEDDECLINEDdecline_reason UNDEFINEDPREPAREdescriptor (if available) Test card: 4601541833776519. API Reference: GET_TRANS_STATUS# POSThttps://{PAYMENT_URL}/post ParameterFormatReqNotes actionGET_TRANS_STATUSY client_keyUUIDY trans_idUUIDY hashMD5 hexYFormula 2 (Formula 6 for CREDIT2CARD) Response: status (3DS/REDIRECT/PENDING/PREPARE/DECLINED/SETTLED/REVERSAL/REFUND/VOID/CHARGEBACK), decline_reason, recurring_token, schedule_id, digital_wallet, arn* if configured. API Reference: GET_TRANS_DETAILS# POSThttps://{PAYMENT_URL}/post ParameterFormatReqNotes actionGET_TRANS_DETAILSY client_keyUUIDY trans_idUUIDY hashMD5 hexYFormula 2 (Formula 6 for CREDIT2CARD) Response includes: name, mail, ip, amount, currency, card (masked), decline_reason if declined, recurring_token, schedule_id, pan_type, digital_wallet, arn* if configured, transactions[] array (each with date, type, status, amount). API Reference: GET_TRANS_STATUS_BY_ORDER# POSThttps://{PAYMENT_URL}/post ParameterFormatReqNotes actionGET_TRANS_STATUS_BY_ORDERY client_keyUUIDY order_id≤255 charsYYour order ID hashMD5 hexYFormula 7 (Formula 6 for CREDIT2CARD) Response: status (3DS/REDIRECT/PENDING/PREPARE/DECLINED/SETTLED/REVERSAL/REFUND/VOID/CHARGEBACK), decline_reason if declined, recurring_token, schedule_id, digital_wallet if applicable. With cascading enabled, returns most recent transaction only. API Reference: CHARGEBACK# Callback-only, not merchant-initiated. See Chargebacks for the full callback schema. API Reference: RECURRING_SALE# POSThttps://{PAYMENT_URL}/post ParameterFormatReqNotes actionRECURRING_SALEY client_keyUUIDY order_id≤255 charsYNew unique order ID order_amountNumberY order_description≤1024 charsY recurring_first_trans_idUUIDYtrans_id of initial transaction recurring_tokenUUIDYToken from initial transaction schedule_idStringNLink to a schedule authY/NNAUTH only (DMS) custom_dataObjectNOverrides initial SALE custom_data hashMD5 hexYFormula 1 Response identical to SALE but action=RECURRING_SALE. Bypasses 3DS. API Reference: RETRY# POSThttps://{PAYMENT_URL}/post ParameterFormatReqNotes actionRETRYY client_keyUUIDY trans_idUUIDYDeclined recurring trans_id hashMD5 hexYFormula 1 Sync: result: ACCEPTED, order_id, trans_id. Only for soft declines. Callback Parameters Success:action: RETRY, result: SUCCESS, status: SETTLED, order_id, trans_id, amount, currency, hash (Formula 2). Declined:action: RETRY, result: DECLINED, status: DECLINED, order_id, trans_id, amount, currency, decline_reason, hash (Formula 2). API Reference: Schedule Operations# All schedule actions use POST https://{PAYMENT_URL}/post. CREATE_SCHEDULE ParameterFormatReqNotes actionCREATE_SCHEDULEY client_keyUUIDY name≤100 charsYSchedule name interval_lengthNumber >0Ne.g. 15 for every 15 days interval_unitday/monthY day_of_month1-31NOnly if interval_unit=month. 29/30/31 → last day if month shorter. payments_countNumberNTotal payments in schedule delaysNumberNIntervals to skip before starting hashMD5 hexYFormula 3 Response: schedule_id. PAUSE_SCHEDULE ParameterReqNotes action = PAUSE_SCHEDULEY client_keyY schedule_idY hashYFormula 4 RUN_SCHEDULE Same parameters as PAUSE_SCHEDULE with action=RUN_SCHEDULE. Resumes paused schedule. DELETE_SCHEDULE Same parameters with action=DELETE_SCHEDULE. Permanently removes schedule. SCHEDULE_INFO Same parameters with action=SCHEDULE_INFO. Returns: name, interval_length, interval_unit, day_of_month, payments_count, delays, paused (Y/N). DESCHEDULE ParameterReqNotes action = DESCHEDULEY client_keyY recurring_tokenYFrom initial transaction schedule_idY hashYFormula 4 Hash Calculator# Generate test hashes during development. The PASSWORD is processed client-side only, do not use in production. Formula 8: Digital Wallets (Apple Pay / Google Pay) EMAIL PASSWORD Compute Hash Hash will appear here… Request Builder# Build a complete SALE request with auto-generated hash and cURL command. Enter your sandbox credentials and card details below. CLIENT_KEY PASSWORD PAYMENT_URL CARD NUMBER EXP (MM/YYYY) AMOUNT EMAIL TERM_URL_3DS Generate cURL Where to go next# This page is the parameter-level record. The guides show the same operations end to end, with worked requests, every response they can return and the callback that settles them. Take a payment The sale, authorise and capture, saving a card, and routing to a sub-merchant. Operations Callbacks, status queries, chargebacks, settlement and the go-live checklist. Troubleshooting Symptoms you will actually hit, and what causes them. Technical questions go to support@centapay.com. --- ## Support Source: https://www.centapay.com/docs/support Last updated 6 August 2026 Support Technical questions, including credential rotation, IP whitelisting, hash verification and callback delivery, go to support@centapay.com. Availability. CentaPay is pre-launch. Production is expected in Q4 2026, with sandbox access ahead of it. Coverage differs by market, and the payment methods available differ by market too. The coverage table on our main site is the single source for what is live where, and for current dates. Nothing in this documentation should be read as a service available today. Commercial questions, including pricing, onboarding and account structure, go to info@centapay.com. When reporting a transaction problem, quote your order_id and, if you have it, the trans_id from the synchronous response. Both appear in every callback. --- ## Testing Source: https://www.centapay.com/docs/testing Last updated 6 August 2026 Testing# Everything you can exercise before you have production credentials. The test cards below drive the sandbox engine, and the two procedures after them cover the cases no card can produce: a callback that fails to deliver, and a chargeback. Availability. CentaPay is pre-launch. Production is expected in Q4 2026, with sandbox access ahead of it. Coverage differs by market, and the payment methods available differ by market too. The coverage table on our main site is the single source for what is live where, and for current dates. Nothing in this documentation should be read as a service available today. Sandbox moves no money. Your PAYMENT_URL differs between sandbox and production and is issued with your credentials, so there is no address to publish here: see Credentials and environments. Testing callback failure handling# No feature forces a callback to time out, so the delivery schedule is exercised by making your own endpoint fail. Point the callback URL at an endpoint you control, have it delay past your timeout or return a status other than 200, and watch the four attempts arrive. The callback URL is configured per account, not per request. A callback_url sent as a request parameter on a server-to-server operation such as SALE is ignored. Producing a chargeback in sandbox# No test card produces one. Sandbox has no card network behind it, so there is nothing to raise a dispute. Support can create one by hand instead. Settle a sandbox payment, then ask support to trigger a chargeback against it, naming the payment. They record a CHARGEBACK transaction carrying a reason code and a date, and the platform then sends the CHARGEBACK callback exactly as it would in production. That makes the callback path testable end to end even though the dispute itself is manufactured. What it does not exercise is the timing, since a real chargeback arrives days or weeks after the payment and this one arrives when support creates it. What next# Handling a chargeback once it arrives, and the rest of running a live integration, are on Operations. When every item here passes, work through the go-live checklist. Technical questions go to support@centapay.com. Test Cards# Use these test values in the sandbox environment. All transactions are processed by the test engine, no real funds are moved. Use any 3-digit CVV. S2S CARD: Scenario Simulation All scenarios use card number 4111111111111111. The expiry date determines the outcome: ExpiryScenarioResponse 01/2038Successful SALE (also use for recurring init. Only card that returns recurring_token)result: SUCCESS, status: SETTLED AUTH: status: PENDING 02/2038Declined SALE / AUTHresult: DECLINED, status: DECLINED 03/2038Successful AUTH, then declined CAPTUREAUTH: SUCCESS/PENDING CAPTURE: DECLINED/PENDING 05/20383DS verification → SuccessSALE: REDIRECT/3DS → After ACS: SUCCESS/SETTLED 06/20383DS verification → DeclineSALE: REDIRECT/3DS → After ACS: DECLINED 12/2038Redirect → SuccessSALE: REDIRECT/REDIRECT → Return: SUCCESS/SETTLED 12/2039Redirect → DeclineSALE: REDIRECT/REDIRECT → Return: DECLINED CREDIT2CARD Test Card Card NumberScenarioResponse 4601541833776519Successful card payoutresult: SUCCESS, status: SETTLED Recurring token generation only works with 4111111111111111 expiry 01/2038. Other test cards will process the SALE but will not return a recurring_token.