# Inovio Developer (complete) Built 2026-09-29. Source pages: https://developer.inoviopay.com/llms.txt --- # Inovio Developer Inovio is a global payment gateway. Integrate with the Gateway API, a server-side SDK in PHP, Node, Python or Java, or a plugin for WooCommerce, Magento 2, PrestaShop 9 or OpenCart 4. Source: https://developer.inoviopay.com/index.html Markdown: https://developer.inoviopay.com/index.md

Inovio: your global payment gateway.

Secure, intelligent processing with global scalability. Flexible APIs, four server-side SDKs and ready-made shopping cart plugins make the integration fast, and every page here is also plain Markdown so your AI assistant can read it too.

Get started API reference SDKs Shopping carts
## Three ways to integrate

01Shopping cart plugin

Install, enter five credentials, sell. WooCommerce, Magento 2, PrestaShop 9 and OpenCart 4. Tokenized checkout: the card number never touches your store.

02Server-side SDK

One object model in PHP, Node, Python and Java. Sale, authorize, capture, refund, void, tokenize, 3D Secure, idempotent retries. Zero runtime dependencies.

03Gateway API

A single form-encoded POST endpoint. Every action, parameter and response code, with copy-ready cURL, SDK examples and a Postman collection.

## What the platform does

International processing

Full currency conversion and intelligent transaction routing for global eCommerce. Request a transaction in one currency and read back the settled currency and exchange rate.

Currency fields

3D Secure and fraud prevention

3D Secure 2 authentication, AVS and CVV checks, blacklists and real-time monitoring identify suspicious activity before fraud occurs.

3D Secure · Fraud mitigation

Recurring billing

Secure card storage, card-on-file indicators and account-updater notifications for long-term subscription and membership billing.

Card on file · Memberships

Chargeback mitigation

Chargebacks are matched to their original transaction instantly, and Order Insight answers issuer inquiries before a dispute is raised.

Order Insight · Postback events

Multi-layer security

Tokenization and encryption keep card data off your systems. With the cart plugins the card number never reaches your server at all.

Tokenization

Robust reporting

Order detail and fast-filter reporting through the API, plus real-time webhooks for every lifecycle event, feed your own business intelligence.

Order Detail API
## Machine-readable
llms.txt llms-full.txt openapi.yaml postman.json Using these docs with AI
Every page has a Markdown twin at the same address with a `.md` extension. The OpenAPI document describes the form-encoded API, and the Postman collection is generated from every cURL example on this site. ## Source code | Project | Repository | |---|---| | PHP SDK | https://github.com/Inoviopay/inovio-gateway-sdk-php | | Node / TypeScript SDK | https://github.com/Inoviopay/inovio-gateway-sdk-node | | Python SDK | https://github.com/Inoviopay/inovio-gateway-sdk-python | | Java SDK | https://github.com/Inoviopay/inovio-gateway-sdk-java | | WooCommerce plugin | https://github.com/Inoviopay/inovio-gateway-woocommerce | | Magento 2 / Mage-OS module | https://github.com/Inoviopay/inovio-gateway-magento2 | | PrestaShop 9 module | https://github.com/Inoviopay/inovio-gateway-prestashop | | OpenCart 4 extension | https://github.com/Inoviopay/inovio-gateway-opencart | --- # Get started What you need from Inovio, how test and production work, and which integration path to pick. Source: https://developer.inoviopay.com/get-started.html Markdown: https://developer.inoviopay.com/get-started.md ## What you need from Inovio Every integration path uses the same five values. Inovio support issues them when your account is boarded. | Credential | Wire name | Used by | |---|---|---| | API username | `REQ_USERNAME` | API, SDKs, plugins | | API password | `REQ_PASSWORD` | API, SDKs, plugins | | Site ID | `SITE_ID` | API, SDKs, plugins | | Site Key | (HMAC secret, never sent) | Tokenization: SDKs' `tokenize()`, and the plugins' browser direct-post | | Gateway Product ID | `LI_PROD_ID` | Plugins (the product each order bills under) | > **The Site Key is not the API password** > It is a separate per-site HMAC secret used only to sign tokenization requests. Without it the token service answers error 121 and a plugin checkout cannot proceed. You cannot generate it yourself. An optional **Merchant Account ID** (`MERCH_ACCT_ID`) pins transactions to one merchant account. Leave it out to let the gateway route by currency and country. ## Test and production are the same endpoint There is no sandbox host. You use the same credentials and the same URLs for testing and for live processing. What changes is how your site is configured in the Inovio portal: during testing your site points at a Test Bank merchant ID, and when you go live Inovio repoints it at your production merchant IDs. No code change is needed. **POST** `https://api.inoviopay.com/payment/pmt_service.cfm` Verify credentials with `TESTAUTH` and gateway availability with `TESTGW`; both are on the [overview page](https://developer.inoviopay.com/api/overview.md). Test card numbers and the scripted decline scenarios are on [Testing](https://developer.inoviopay.com/api/testing.md). ## Pick a path | If you… | Use | Time to first approved transaction | |---|---|---| | Run WooCommerce, Magento 2, PrestaShop 9 or OpenCart 4 | A [shopping cart plugin](https://developer.inoviopay.com/carts/index.md) | Minutes. Install, enter the five credentials, enable. | | Have your own checkout in PHP, Node, Python or Java | A [server-side SDK](https://developer.inoviopay.com/sdks/index.md) | An hour. `client.sale(request)` and handle a five-state result. | | Need something the SDKs do not cover yet (ACH, wallets, subscriptions) or use another language | The [Gateway API](https://developer.inoviopay.com/api/overview.md) directly | The API is one form-encoded POST; the SDK pages show the exact fields each method sends. | ## Where the card number goes The gateway supports three ways the card number can reach it, from the most to the least card data on your server: 1. **Raw PAN to the API.** Your server sends `PMT_NUMB`. Simple, but your server handles card data. 2. **Server-side tokenization.** Your server calls the token service, gets a single-use `TOKEN_GUID`, and uses that instead of the PAN. The SDKs' `tokenize()` does this. The PAN still transits your server. 3. **Browser direct-post.** The shopper's browser posts the PAN straight to the token service, signed with an HMAC your server minted from the Site Key. Only the token reaches you. This is what all four cart plugins do. A browser Hosted Fields library for custom checkouts is planned; until it ships, the plugins are the reference implementation of level 3. See [Tokenization](https://developer.inoviopay.com/api/tokenization.md). ## Next - [Sale (CCAUTHCAP)](https://developer.inoviopay.com/api/sale.md): the one request most integrations start with. - [How the SDKs think](https://developer.inoviopay.com/sdks/concepts.md): the five-state result, decimal money, idempotency. - [Shopping cart plugins](https://developer.inoviopay.com/carts/index.md): the capability matrix and the shared refund design. --- # Use cases Find the integration that matches what you need to do, and jump to the pages and sample code that cover it. Source: https://developer.inoviopay.com/use-cases.html Markdown: https://developer.inoviopay.com/use-cases.md Inovio covers the entire payment transaction process for any business, in any industry, from any bank source, with any endpoint. Find what you need below; each card links to the documentation and sample code for it. ## Explore use cases

Payments

I want to be able to accept payments.

One request authorizes and captures a card. Authorize-only, delayed capture, credit and reversal are the same endpoint with a different action.

Payment security

I need secure payment processing.

Exchange the card number for a single-use token, or let the shopper's browser post it straight to Inovio so it never reaches your server.

3D Secure

I want to utilize 3D Secure authentication.

Device data collection, frictionless and challenge flows through the gateway, or bring your own 3DS provider's result.

Anti-fraud tools

I want to prevent fraudulent transactions.

AVS and CVV enforcement, risk scrubbing, blacklists, and Order Insight for issuer inquiries.

Keep card data off your systems

I don't want to handle raw card data.

Tokenization keeps card data off your systems. With the cart plugins the card number posts straight to Inovio and never reaches your server, with no Inovio-hosted infrastructure required. See also Easy PCI Compliance.

Shopping carts

I need a third-party shopping cart.

First-party plugins for WooCommerce, Magento 2, PrestaShop 9 and OpenCart 4, all on the same tokenized checkout.

International processing

I need to process in multiple currencies.

Request any supported currency and read back the settled amount, currency and exchange rate on the response.

CRM integration

I want to connect data into my CRM.

Postback webhooks deliver purchase, rebill, subscription and chargeback events to your system in real time.

Recurring billing

I want easy billing for repeat subscriptions.

Card-on-file indicators for merchant-driven billing, or gateway-managed memberships with rebills and account updater.

Reporting

I need detailed transaction reporting.

Order Detail and fast-filter reports through the API, with every field documented.

## Ready to build? Know which of the above apply? [Get started](https://developer.inoviopay.com/get-started.md) lists what you need from Inovio and how test and production work. To talk to someone first, [connect with an expert](https://www.inoviopay.com/contact). --- # Solutions The products on the Inovio platform, and which of them this site documents. Source: https://developer.inoviopay.com/solutions.html Markdown: https://developer.inoviopay.com/solutions.md With flexible APIs and compatibility with any programming language, the Inovio platform makes it easy to set up and customize your desired payment experience. This page lists the products; the rest of the site documents the gateway, the SDKs and the cart plugins. For the others, contact your Inovio representative. ## Featured solutions

Global eCommerce gateway

Flexible, intelligent and 3D Secure. Multi-currency processing with intelligent routing across credit cards, US ACH, direct debit, SMS and electronic cash methods.

API reference · SDKs

Shopping cart plugins

First-party plugins for WooCommerce, Magento 2, PrestaShop 9 and OpenCart 4 with tokenized checkout, 3D Secure, saved cards and settlement-aware refunds.

Shopping carts

Recurring billing

Repeat customers and subscriptions with secure card storage, card-on-file indicators, gateway-managed memberships and account updater notifications.

Card on file · Memberships

Risk mitigation and fraud prevention

3D Secure, encryption, tokenization, AVS and CVV enforcement, and real-time risk monitoring.

Fraud mitigation · 3D Secure

Keep card data off your systems

Tokenization and the direct-post cart plugins keep raw card data off your systems, so the card number need never touch your servers.

Tokenization · PCI compliance

Product catalog

Online product catalogs with linked pricing and monthly subscription rebills, managed in the Inovio portal. Transactions reference catalog products by gateway product id.

Line items

Hosted checkout and hosted payment form

A checkout page hosted by Inovio, or a customizable iframe form that keeps your branding. Contact Inovio for the integration guide.

Contact Inovio

Virtual POS

A web-based application for phone, mail or online transactions without dedicated hardware, available in the Inovio portal.

Contact Inovio
## Third-party integrations Inovio integrates with chargeback and fraud partners including Verifi (Order Insight), Ethoca, and CRM platforms through the postback service. See [Order Insight](https://developer.inoviopay.com/api/order-insight.md) and [Postback service](https://developer.inoviopay.com/api/postback.md). --- # Using these docs with AI How this site is built to be read by AI assistants and coding agents, and how to point one at it. Source: https://developer.inoviopay.com/ai.html Markdown: https://developer.inoviopay.com/ai.md ## Every page is Markdown Every HTML page on this site has a Markdown twin at the same path with a `.md` extension. The Markdown is the source; the HTML is generated from it. Both carry the same headings, tables, code samples and anchors. | You want | Fetch | |---|---| | One page | `https://developer.inoviopay.com/api/sale.md` | | The site index with one-line summaries | `https://developer.inoviopay.com/llms.txt` | | Every page in a single file | `https://developer.inoviopay.com/llms-full.txt` | | The API as a machine-readable spec | `https://developer.inoviopay.com/openapi.yaml` | | Every example request, importable | `https://developer.inoviopay.com/postman.json` | Clients that send `Accept: text/markdown` get the Markdown for an `.html` URL without changing the path. Each HTML page also declares its twin with ``, carries `TechArticle` JSON-LD, and has a **Copy for AI** button in the header that copies the page as Markdown to your clipboard. **curl** ```bash curl -s -H "Accept: text/markdown" https://developer.inoviopay.com/api/sale.html ``` **Python** ```python import urllib.request md = urllib.request.urlopen("https://developer.inoviopay.com/api/sale.md").read().decode() ``` ## Point an assistant at the site **Claude Code, Cursor, Copilot, Windsurf and similar.** Paste the `llms.txt` URL into your project rules or a `docs` reference, or add this line to your project's `CLAUDE.md` / `.cursorrules`: ```markdown Inovio gateway docs: https://developer.inoviopay.com/llms.txt (fetch the .md pages it lists before answering questions about the Inovio API or SDKs). ``` **ChatGPT, Claude.ai, Gemini.** Paste `https://developer.inoviopay.com/llms-full.txt` and ask your question; the full site is small enough to fit in a single context window. **Code generation from the spec.** `openapi.yaml` is OpenAPI 3.1. The API is not REST: it is one form-encoded `POST` whose `request_action` field selects the operation, so use the spec for parameter names, enums and response fields rather than for generating a client. The SDKs are the supported clients. ## What agents should know about this API These are the facts an assistant most often gets wrong when reasoning from generic payment-gateway knowledge. Each links to the page that proves it. - **There is no sandbox host.** Test and production use the same URL and credentials; the portal configuration decides which merchant ID is hit. [Get started](https://developer.inoviopay.com/get-started.md) - **A decline is a normal response, not an error.** `TRANS_STATUS_NAME=DECLINED` with `SERVICE_RESPONSE` and `PROCESSOR_RESPONSE` explaining why. The SDKs return it; they do not throw. [How the SDKs think](https://developer.inoviopay.com/sdks/concepts.md) - **The tokenization HMAC does not include the card number.** The PDF says it does; the gateway disagrees. Sign `timestamp || unique_id || site_id` with the Site Key. [Tokenization](https://developer.inoviopay.com/api/tokenization.md) - **A token replaces the PAN only.** You still send `pmt_expiry` and, where required, `pmt_key`. [Tokenization](https://developer.inoviopay.com/api/tokenization.md) - **Send `CREDIT_ON_FAIL=1` on a reversal** and the gateway itself re-routes to a credit if the order has already settled. Never write a client-side "try reverse, then credit" fallback. [Reversal](https://developer.inoviopay.com/api/reversal.md) - **`XTL_ORDER_ID` gives you idempotent retries.** A retried request with the same id returns the original result instead of charging twice. After a timeout, call `CCSTATUS` before retrying. [Order status](https://developer.inoviopay.com/api/status.md) - **Amounts are decimal strings**, never floats. The SDKs reject `1.25` as a number and accept `"1.25"`. [How the SDKs think](https://developer.inoviopay.com/sdks/concepts.md) - **The statement descriptor must not contain a space, underscore or slash.** The whole transaction is rejected otherwise. [Shopping cart plugins](https://developer.inoviopay.com/carts/index.md) ## Structure that makes the site easy to parse - Stable, slugified section ids (`/api/sale.html#request-parameters`), unchanged from the previous site where they existed. - One `h1` per page, `h2` per section, `h3` inside a section. No deeper nesting. - Parameter tables always have the parameter in the first column with a `Required` or `Optional` badge (`(required)` / `(optional)` in the Markdown). - Code samples are fenced with a language tag; example requests are complete and use the placeholder credentials `api_user` / `P@ssw0rd!` / site `12345`. - `robots.txt` explicitly allows AI crawlers; `sitemap.xml` lists every page. --- # API Overview Introduction to the Inovio Payment Service API, its transport and encoding requirements, and how testing and production environments are separated. Source: https://developer.inoviopay.com/api/overview.html Markdown: https://developer.inoviopay.com/api/overview.md The Inovio Payment Service API is designed to allow merchants to communicate and process online transactions with the payment gateway's transaction processing system. All cardholder and transaction data are sent over the internet using the TLS 1.2 cryptographic protocol. The API expects all data to be URL and UTF-8 encoded, using character set ISO-8859-1 over HTTPS. **Testing & Production Environments:** At Inovio, merchants use the exact same account credentials and API endpoints for both testing and production. The distinction lies purely in how your site is configured within the Inovio portal. During testing, your site's transactions will be pointed to a "Test Bank" Merchant ID (MID), allowing you to safely simulate payments. When you are ready to process real transactions, this configuration is simply updated in the portal to point to your live production MID(s), requiring zero changes to your code. **POST** `https://api.inoviopay.com/payment/pmt_service.cfm` Authentication parameters (`REQ_USERNAME`, `REQ_PASSWORD`, `SITE_ID`) are required on every request. You can verify your credentials are working with the `TESTAUTH` action, documented on the [Authentication](https://developer.inoviopay.com/api/authentication.md#auth) page. ## Service Availability Use the `TESTGW` action to check if the Payment Service is available to process requests. This is useful for uptime monitoring and health checks. | Parameter | Description | |---|---| | `REQUEST_ACTION` (required) | Must be set to `TESTGW`. | | `REQ_USERNAME` (required) | Merchant's Service username. | | `REQ_PASSWORD` (required) | Merchant's Service password. | | `SITE_ID` (required) | Merchant's website ID. | | `REQUEST_API_VERSION` (required) | Must be sent as `4.14`. | | `REQUEST_RESPONSE_FORMAT` (optional) | Format of the response. Accepts `XML` or `JSON`. | **cURL** ```bash curl -X POST "https://api.inoviopay.com/payment/pmt_service.cfm" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "request_action=TESTGW&req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&request_response_format=JSON" ``` **PHP** ```php use Inovio\Gateway\{Credentials, InovioClient}; $client = new InovioClient(new Credentials('api_user', 'P@ssw0rd!', '12345'), 'SANDBOX'); $health = $client->testAvailability(); echo $health->ok, "\n"; echo $health->outcome->service->code, ' ', $health->outcome->service->advice, "\n"; ``` **Node** ```ts import { InovioClient } from '@inovio/gateway-sdk'; const client = new InovioClient( { reqUsername: 'api_user', reqPassword: 'P@ssw0rd!', siteId: '12345' }, { environment: 'SANDBOX' } ); const health = await client.testAvailability(); console.log(health.ok); console.log(health.outcome.service.code, health.outcome.service.advice); ``` **Python** ```python from inovio_gateway import Credentials, InovioClient client = InovioClient(Credentials("api_user", "P@ssw0rd!", "12345"), environment="SANDBOX") health = client.test_availability() print(health.ok) print(health.outcome.service.code, health.outcome.service.advice) ``` **Java** ```java InovioClient client = new InovioClient( new InovioClient.Credentials("api_user", "P@ssw0rd!", "12345")); HealthResult health = client.testAvailability(); System.out.println(health.ok()); System.out.println(health.outcome().service().code() + " " + health.outcome().service().advice()); ``` **Response** ```json { "REQUEST_ACTION": "TESTGW", "TRANS_STATUS_NAME": "", "TRANS_VALUE": "", "TRANS_ID": "", "CUST_ID": "", "XTL_CUST_ID": "", "MERCH_ACCT_ID": "", "CARD_BRAND_NAME": "", "PMT_L4": "", "API_RESPONSE": "0", "API_ADVICE": " ", "SERVICE_RESPONSE": 101, "SERVICE_ADVICE": "Service Available", "PROCESSOR_RESPONSE": 0, "PROCESSOR_ADVICE": " ", "INDUSTRY_RESPONSE": 0, "INDUSTRY_ADVICE": " ", "REF_FIELD": "", "PROC_NAME": "", "AVS_RESPONSE": "", "CVV_RESPONSE": "", "REQUEST_API_VERSION": "4.14", "TRANS_NTOKEN_USED": 0 } ``` --- # Authentication Required credential parameters sent with every gateway request, and how to verify them with the TESTAUTH action. Source: https://developer.inoviopay.com/api/authentication.html Markdown: https://developer.inoviopay.com/api/authentication.md Authentication parameters are required on every request to the gateway. You can use the `TESTAUTH` action to verify your credentials are functioning correctly. | Parameter | Description | |---|---| | `REQ_USERNAME` (required) | Merchant's Service username. | | `REQ_PASSWORD` (required) | Merchant's Service password. | | `SITE_ID` (required) | Merchant's website ID. | | `REQUEST_API_VERSION` (required) | Must be sent as `4.14` on all requests. | | `REQUEST_RESPONSE_FORMAT` (optional) | Format of the response. Accepts `XML` (default), `JSON`, or `PIPES`. | **cURL** ```bash curl -X POST "https://api.inoviopay.com/payment/pmt_service.cfm" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "request_action=TESTAUTH&req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&request_response_format=JSON" ``` **PHP** ```php use Inovio\Gateway\{Credentials, InovioClient}; use Inovio\Gateway\Errors\AuthenticationException; $client = new InovioClient(new Credentials('api_user', 'P@ssw0rd!', '12345'), 'SANDBOX'); try { $health = $client->testAuth(); echo $health->ok, "\n"; echo $health->outcome->service->code, ' ', $health->outcome->service->advice, "\n"; } catch (AuthenticationException $e) { // Bad credentials raise an exception (API tier 101), never a decline. echo 'rejected: ', $e->getMessage(), "\n"; } ``` **Node** ```ts import { InovioClient, AuthenticationError } from '@inovio/gateway-sdk'; const client = new InovioClient( { reqUsername: 'api_user', reqPassword: 'P@ssw0rd!', siteId: '12345' }, { environment: 'SANDBOX' } ); try { const health = await client.testAuth(); console.log(health.ok); console.log(health.outcome.service.code, health.outcome.service.advice); } catch (e) { // Bad credentials raise an error (API tier 101), never a decline. if (e instanceof AuthenticationError) console.log('rejected:', e.message); else throw e; } ``` **Python** ```python from inovio_gateway import AuthenticationError, Credentials, InovioClient client = InovioClient(Credentials("api_user", "P@ssw0rd!", "12345"), environment="SANDBOX") try: health = client.test_auth() print(health.ok) print(health.outcome.service.code, health.outcome.service.advice) except AuthenticationError as e: # Bad credentials raise an exception (API tier 101), never a decline. print("rejected:", e.message) ``` **Java** ```java import com.inoviopay.gateway.errors.AuthenticationException; InovioClient client = new InovioClient( new InovioClient.Credentials("api_user", "P@ssw0rd!", "12345")); try { HealthResult health = client.testAuth(); System.out.println(health.ok()); System.out.println(health.outcome().service().code() + " " + health.outcome().service().advice()); } catch (AuthenticationException e) { // Bad credentials raise an exception (API tier 101), never a decline. System.out.println("rejected: " + e.getMessage()); } ``` **Response** ```json { "REQUEST_ACTION": "TESTAUTH", "TRANS_STATUS_NAME": "", "TRANS_VALUE": "", "TRANS_ID": "", "CUST_ID": "", "XTL_CUST_ID": "", "MERCH_ACCT_ID": "", "CARD_BRAND_NAME": "", "PMT_L4": "", "API_RESPONSE": "0", "API_ADVICE": " ", "SERVICE_RESPONSE": 100, "SERVICE_ADVICE": "User Authorized", "PROCESSOR_RESPONSE": 0, "PROCESSOR_ADVICE": " ", "INDUSTRY_RESPONSE": 0, "INDUSTRY_ADVICE": " ", "REF_FIELD": "", "PROC_NAME": "", "AVS_RESPONSE": "", "CVV_RESPONSE": "", "REQUEST_API_VERSION": "4.14", "TRANS_NTOKEN_USED": 0 } ``` --- # Tokenization Service Exchange a card number for a one-time-use TOKEN_GUID, with verified HMAC signing and follow-up sale request requirements. Source: https://developer.inoviopay.com/api/tokenization.html Markdown: https://developer.inoviopay.com/api/tokenization.md Rather than submitting the credit card number (`PMT_ID`) with a transaction, a token ID may be used instead. This one time use, unique ID is acquired by sending a request to the token service endpoint. **POST** `https://api.inoviopay.com/payment/token_service.cfm` | Parameter | Description | |---|---| | `card_pan` (required) | Customer card number to be tokenized. | | `request_api_version` (required) | API Version (e.g., `4.14`). | | `site_id` (required) | Merchant's website ID. | | `unique_id` (required) | Alphanumeric ID linked to the request (Max 32 chars). | | `x-timestamp` (required) | Format: `YYYYMMDDHHMMSS` (Uses UTC). Open for 5 minutes. | | `x-signature` (required) | HMAC_SHA256 Base16 signature generated using x-timestamp and unique_id with the secret key. | > **Verified: the HMAC signature excludes the card number** > The v4.14 PDF, section 4.8.1.1, documents `x-signature` as generated from `x-timestamp`, `unique_id`, `card_pan`, and `site_id`. We verified against the gateway directly, and the gateway actually validates `hmac_sha256(timestamp || unique_id || site_id, site_key)`, with the PAN excluded from the signed message. Signing with the PAN included fails with error 121. > > The PDF and the verified gateway behavior disagree here. This page documents the verified, working behavior: sign `x-timestamp`, `unique_id`, and `site_id` only, and omit `card_pan` from the signed string. > **The site key is not your API password** > The site key used to compute the HMAC is a separate per-site HMAC secret issued by Inovio support. It is not the same as your `req_password` API password used on `pmt_service.cfm` requests. Contact your gateway support representative to obtain it. > **TOKEN_GUID still requires pmt_expiry** > A `TOKEN_GUID` replaces `PMT_NUMB` only, in a subsequent sale or auth request. You must still send `pmt_expiry` (and `pmt_key`, when the processor requires it) alongside `TOKEN_GUID`. Omitting `pmt_expiry` causes the API to respond with error 110, "Required field". **cURL** ```bash curl -X POST "https://api.inoviopay.com/payment/token_service.cfm" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "card_pan=4111111111111111&request_api_version=4.14&site_id=12345&unique_id=REQ999&x-timestamp=20241209230900&x-signature=8fa1f..." ``` **PHP** ```php use Inovio\Gateway\{Credentials, InovioClient}; use Inovio\Gateway\Model\{LineItem, Money, PaymentMethods}; use Inovio\Gateway\Request\TransactionRequest; // The site key is a separate constructor argument, not your req_password. $client = new InovioClient( new Credentials('api_user', 'P@ssw0rd!', '12345'), 'SANDBOX', siteKey: 'your-hmac-site-key' ); $token = $client->tokenize(PaymentMethods::card('4111111111111111', '122026', '123')); echo $token->token->guid(), "\n"; // The token replaces the PAN only — expiry travels with it automatically. $sale = $client->sale((new TransactionRequest( $token->token, [new LineItem('SKU-1', 1, Money::of('10.00', 'USD'))] ))->withIdempotency('TOK-ORDER-1')); echo $sale->status, "\n"; ``` **Node** ```ts import { InovioClient, Money, PaymentMethods } from '@inovio/gateway-sdk'; // The site key is passed via client options, not req_password. const client = new InovioClient( { reqUsername: 'api_user', reqPassword: 'P@ssw0rd!', siteId: '12345' }, { environment: 'SANDBOX', siteKey: 'your-hmac-site-key' } ); const token = await client.tokenize(PaymentMethods.card('4111111111111111', '122026', '123')); console.log(token.token.guid); // The token replaces the PAN only — expiry travels with it automatically. const sale = await client.sale({ paymentMethod: token.token, lineItems: [{ productId: 'SKU-1', count: 1, value: Money.of('10.00', 'USD') }], idempotency: { xtlOrderId: 'TOK-ORDER-1' }, }); console.log(sale.status); ``` **Python** ```python from inovio_gateway import Credentials, InovioClient, Money, PaymentMethods from inovio_gateway.model import Idempotency, LineItem from inovio_gateway.request import TransactionRequest # The site key is a client constructor argument, not the API password. client = InovioClient( Credentials("api_user", "P@ssw0rd!", "12345"), environment="SANDBOX", site_key="your-hmac-site-key", ) token = client.tokenize(PaymentMethods.card("4111111111111111", "122026", "123")) print(token.token.guid) # The token replaces the PAN only — expiry travels with it automatically. req = TransactionRequest( payment_method=token.token, line_items=[LineItem("SKU-1", 1, Money.of("10.00", "USD"))], idempotency=Idempotency(xtl_order_id="TOK-ORDER-1"), ) sale = client.sale(req) print(sale.status.value) ``` **Java** ```java // The site key is set on InovioClient.Options, not the req_password field. InovioClient.Options options = new InovioClient.Options(); options.siteKey = "your-hmac-site-key"; InovioClient client = new InovioClient( new InovioClient.Credentials("api_user", "P@ssw0rd!", "12345"), options); Tokenize.Result token = client.tokenize( PaymentMethods.card("4111111111111111", "122026", "123")); System.out.println(token.token().guid()); // The token replaces the PAN only — expiry travels with it automatically. TransactionRequest req = new TransactionRequest( token.token(), new LineItem("SKU-1", 1, Money.of("10.00", "USD"))) .idempotency("TOK-ORDER-1"); TransactionResult sale = client.sale(req); System.out.println(sale.status()); ``` **Response** ```json { "TOKEN_GUID": "3A393EBC3A266B9842FC55DB44B5401C697A5768", "TOKEN_IP": "192.168.1.1", "TOKEN_REQID": "73393758223" } ``` --- # Easy PCI Compliance Inovio's solutions, including hosted checkout and payment pages, are PCI compliant, providing unsurpassed security for customer account data. We make PCI compliance easy. Source: https://developer.inoviopay.com/api/pci.html Markdown: https://developer.inoviopay.com/api/pci.md *I want to avoid having a PCI compliance burden.* Inovio's Hosted Checkout page and Hosted Payment Form make it easy for merchants to accept payments online without actually interacting with customer payment data themselves. Since we are PCI compliant, merchants with our Hosted Checkout page are PCI compliant, providing unsurpassed security for customer account data. ## Sample code [View a sample hosted payment page](https://svc.arguspayments.com/paymentpage/load/10191/16854/34538) ## Ready to get started? [Get started](https://developer.inoviopay.com/get-started.md) or [connect with an expert](https://developer.inoviopay.com/contact.md). --- # Authorize (CCAUTHORIZE) Place a temporary hold on a cardholder's account to confirm funds are available, without capturing them. Source: https://developer.inoviopay.com/api/authorize.html Markdown: https://developer.inoviopay.com/api/authorize.md An authorization request is used to confirm the availability of funds in the cardholder's bank account. This type of transaction places a temporary hold or pending "auth" on the cardholder's account and does not guarantee payment. For this type of transaction, merchants must send the service request action `CCAUTHORIZE`. An authorization is the first part of a two-stage process of "authorizing" and "capturing" funds. This two-stage process is commonly used by merchants who fulfill partial orders or physical goods that have not yet shipped. In order to capture the Authorization request, use the `CCCAPTURE` action. **POST** `https://api.inoviopay.com/payment/pmt_service.cfm` > **WAIT TIME REQUIREMENT** > Please allow a wait time of **120 seconds** for `CCAUTHORIZE`, `CCAUTHCAP`, and other requests. Even though the gateway usually processes requests in less than a second, downstream processors can take much longer to get a response from issuing banks. > **EARLY TIMEOUT WARNING** > If a merchant chooses to time out their request earlier than 120 seconds: > - The gateway is not responsible for lost transactions, transaction records, or other failed functions. > - The merchant may use `CCSTATUS` at a later time to find the final outcome of the request (provided an `XTL_ORDER_ID` was included in the original request). See [Order Status](https://developer.inoviopay.com/api/status.md#status). > - Webhooks/postbacks may or may not trigger when the request is finalized at the gateway and should not be strictly relied upon. ### Request Parameters The table below includes all required authentication, payment, and optional fields permitted during an authorization. | Parameter | Description | |---|---| | `REQUEST_ACTION` (required) | Must be set to `CCAUTHORIZE`. | | `REQ_USERNAME` (required) | Service Request Username. | | `REQ_PASSWORD` (required) | Service Request Password. | | `REQUEST_RESPONSE_FORMAT` (optional) | Accepted values: "XML", "PIPES" and "JSON" (default: XML). | | `REQUEST_API_VERSION` (required) | API Version (Must be 4.14). | | `SITE_ID` (required) | Merchant's Website ID. | | `CUST_FNAME` (optional) | Cardholder's First Name. | | `CUST_LNAME` (optional) | Cardholder's Last Name. | | `CUST_EMAIL` (optional) | Cardholder's Email Address. | | `LI_COUNT_1` (required) | Line Item Count (Max value is "99"). | | `LI_PROD_ID_1` (required) | Line Item Product ID 1. | | `LI_VALUE_1` (required) | Line Item Transaction Amount 1. | | `XTL_ORDER_ID` (optional) | Merchant's Order ID. | | `BILL_ADDR` (optional) | Cardholder Billing Street Address (may be required by the bank for AVS). | | `BILL_ADDR_CITY` (optional) | Cardholder's Billing City. | | `BILL_ADDR_STATE` (optional) | Cardholder's Billing State (2-letter State or Territory Code). | | `BILL_ADDR_ZIP` (optional) | Cardholder's Billing Postal/ZIP code. | | `BILL_ADDR_COUNTRY` (optional) | Cardholder's Billing Country (2-letter Country Code ISO 3166-1 alpha-2). | | `PMT_NUMB` (required) | Credit Card Number. | | `TOKEN_GUID` (optional) | Token ID used in place of pmt_numb. See [Tokenization](https://developer.inoviopay.com/api/tokenization.md#tokenization). | | `PMT_KEY` (required) | Credit Card CVV2 or CVC2 Code. | | `PMT_EXPIRY` (required) | Credit Card Expiration Date (Format: `MMYYYY`, e.g. "122026"). | | `CUST_LOGIN` (optional) | Cardholder's Login or User Name. | | `CUST_PASSWORD` (optional) | Cardholder's Password. | | `MERCH_ACCT_ID` (optional) | Merchant Account ID. If null, the system will follow merchant's bank settings. | | `REQUEST_CURRENCY` (required) | 3-letter Currency Code (e.g., USD). | | `PMT_DESCRIPTOR` (optional) | Dynamic Descriptor. This parameters will replace the static statement descriptor that appears in the cardholder's bank statement. (Not supported by all Processors) | | `PMT_DESCRIPTOR_PHONE` (optional) | Bank Dynamic Customer Support Phone Number. This parameters will replace the static statement descriptor that appears in the cardholder's bank statement. (Not supported by all Processors) | | `PMT_DESCRIPTOR_CITY` (optional) | Bank Dynamic Customer Support City (Applicable to MasterCard only). | | `CUST_PHONE` (optional) | Cardholder's Phone Number. | | `REQUEST_AFF_ID` (optional) | External Affiliate ID. | | `REQUEST_AFF_ID_SUB` (optional) | External Sub-affiliate ID. | | `UNIQUE_XTL_ORDER_ID` (optional) | Enforces External Order ID uniqueness ("0" - disables, "1" - declines, "2" - returns Approval). | | `PMT_ID_XTL` (optional) | External Payment Unique Identifier. | | `MBSHP_ID_XTL` (optional) | External Membership ID. | | `TRANS_REBILL_TYPE` (optional) | Rebill Type (NONE, TRIAL, INITIAL, REBILL). | | `REQUEST_INITIATOR` (optional) | CIT (C) and MIT (M) Used to identify transaction origination. | | `REQUEST_INSTALLMENT` (optional) | 0 (not used), 1 (used) Used to denote if the rebill is an installment. | | `PMT_NUMB_COF` (optional) | 0 (not used), 1 (used) Used to denote use of payment number stored by Merchant. | | `REQUEST_REBILL` (optional) | 1 (yes for rebill), 2 (Start Subscription), not used. | | `TRANS_TRIAL_REBILL_CUSTOMER_CONSENT` (optional) | Customer Consent for rebill after trial period. Required for first REBILL following TRIAL. | | `TRANS_CUSTOMER_RECEIPT` (optional) | Payment Receipt content sent to the customer. | | `CARD_ON_FILE_FLAG` (optional) | Flags if COF or New PMT Card (0=COF, 1=New). | ### Handling the Response While the gateway returns a comprehensive payload for data modeling, you should primarily monitor the following fields to determine the outcome of an authorization, especially in cases of partial or variable authorizations: | Field Name | Description | |---|---| | `TRANS_STATUS_NAME` | Check this to ensure the transaction was `APPROVED`. | | `TRANS_VALUE` | The exact amount the issuing bank authorized. If this is less than your requested amount, it was a partial authorization. | | `PO_ID` | The Purchase Order ID. You must pass this exact value into your subsequent `CCCAPTURE` request to settle the funds. | | `TRANS_ID` | The unique Transaction ID for the authorization. | | `PROC_AUTH_RESPONSE` | The authorization code generated by the processor. | | `AVS_RESPONSE / CVV_RESPONSE` | Indicates if the address and security codes matched, allowing you to apply internal risk modeling. | **cURL** ```bash curl -X POST "https://api.inoviopay.com/payment/pmt_service.cfm" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "request_action=CCAUTHORIZE&req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&request_response_format=JSON&pmt_numb=4111111111111111&pmt_expiry=122026&pmt_key=123&request_currency=USD&li_value_1=84.50&cust_fname=John&cust_lname=Doe&bill_addr=123+Main+St&bill_addr_city=Los+Angeles&bill_addr_state=CA&bill_addr_zip=90001&bill_addr_country=US" ``` **PHP** ```php use Inovio\Gateway\{Credentials, InovioClient}; use Inovio\Gateway\Model\{Address, Customer, LineItem, Money, PaymentMethods}; use Inovio\Gateway\Request\TransactionRequest; $client = new InovioClient(new Credentials('api_user', 'P@ssw0rd!', '12345'), 'SANDBOX'); $req = new TransactionRequest( PaymentMethods::card('4111111111111111', '122026', '123'), [new LineItem('SKU-492', 1, Money::of('84.50', 'USD'))] ); $req->customer = new Customer(); $req->customer->firstName = 'John'; $req->customer->lastName = 'Doe'; $req->billingAddress = new Address(); $req->billingAddress->line1 = '123 Main St'; $req->billingAddress->city = 'Los Angeles'; $req->billingAddress->state = 'CA'; $req->billingAddress->zip = '90001'; $req->billingAddress->country = 'US'; $auth = $client->authorize($req); echo $auth->status, ' order=', $auth->orderRef?->poId() ?? '-', "\n"; // Keep $auth->orderRef — capture() and reverse() both consume it. ``` **Node** ```ts import { InovioClient, Money, PaymentMethods } from '@inovio/gateway-sdk'; const client = new InovioClient( { reqUsername: 'api_user', reqPassword: 'P@ssw0rd!', siteId: '12345' }, { environment: 'SANDBOX' } ); const auth = await client.authorize({ paymentMethod: PaymentMethods.card('4111111111111111', '122026', '123'), lineItems: [{ productId: 'SKU-492', count: 1, value: Money.of('84.50', 'USD') }], customer: { firstName: 'John', lastName: 'Doe' }, billingAddress: { line1: '123 Main St', city: 'Los Angeles', state: 'CA', zip: '90001', country: 'US' }, }); console.log(auth.status, 'order=', auth.orderRef?.poId ?? '-'); // Keep auth.orderRef — capture() and reverse() both consume it. ``` **Python** ```python from inovio_gateway import Credentials, InovioClient, Money, PaymentMethods from inovio_gateway.model import Address, Customer, LineItem from inovio_gateway.request import TransactionRequest client = InovioClient(Credentials("api_user", "P@ssw0rd!", "12345"), environment="SANDBOX") req = TransactionRequest( payment_method=PaymentMethods.card("4111111111111111", "122026", "123"), line_items=[LineItem("SKU-492", 1, Money.of("84.50", "USD"))], ) req.customer = Customer(first_name="John", last_name="Doe") req.billing_address = Address( line1="123 Main St", city="Los Angeles", state="CA", zip="90001", country="US" ) auth = client.authorize(req) print(auth.status.value, "order=", auth.order_ref.po_id if auth.order_ref else "-") # Keep auth.order_ref — capture() and reverse() both consume it. ``` **Java** ```java TransactionRequest req = new TransactionRequest( PaymentMethods.card("4111111111111111", "122026", "123"), new LineItem("SKU-492", 1, Money.of("84.50", "USD"))); Customer customer = new Customer(); customer.firstName = "John"; customer.lastName = "Doe"; req.customer = customer; Address billing = new Address(); billing.line1 = "123 Main St"; billing.city = "Los Angeles"; billing.state = "CA"; billing.zip = "90001"; billing.country = "US"; req.billingAddress = billing; TransactionResult auth = client.authorize(req); System.out.println(auth.status() + " order=" + (auth.orderRef() == null ? "-" : auth.orderRef().poId())); // Keep auth.orderRef() — capture() and reverse() both consume it. ``` **Response** ```json { "REQUEST_ACTION": "CCAUTHORIZE", "REQ_ID": "83920174", "TRANS_STATUS_NAME": "APPROVED", "TRANS_VALUE": 84.50, "CURR_CODE_ALPHA": "USD", "TRANS_VALUE_SETTLED": 84.50, "CURR_CODE_ALPHA_SETTLED": "USD", "TRANS_EXCH_RATE": "", "TRANS_ID": 9482710345, "CUST_ID": 1029384, "XTL_CUST_ID": "xTr92mPqL1", "PO_ID": 55829102, "XTL_ORDER_ID": "", "BATCH_ID": 883921, "PROC_NAME": "Inovio Primary", "MERCH_ACCT_ID": 449201, "CARD_BRAND_NAME": "Mastercard", "CARD_TYPE": "MASTERCARD WORLD", "CARD_CLASS": "Consumer Credit", "CARD_PREPAID": 0, "CARD_BANK": "CAPITAL ONE", "CARD_COUNTRY": "USA", "CARD_DETAIL": "Credit", "CARD_BALANCE": "", "PMT_L4": "5521", "PMT_ID": 8839201, "PMT_ID_XTL": "", "PMT_AAU_UPDATE_DT": "", "PMT_AAU_UPDATE_DESC": "", "PROC_UDF01": "", "ANI_RESP_DECISION": "", "PROC_UDF02": "", "PROC_AUTH_RESPONSE": "APV992", "PROC_RETRIEVAL_NUM": "9A8B7C6D-5E4F-3A2B-1C0D-E9F8A7B6C5D4", "PROC_REFERENCE_NUM": "REF88291039", "PROC_REDIRECT_URL": "", "AVS_RESPONSE": "Y", "CVV_RESPONSE": "M", "CARD_BRAND_TRANSID": "", "REQUEST_API_VERSION": "4.14", "P3DS_VENDOR": "", "P3DS_RESPONSE": "", "PO_LI_ID_1": "9928172", "PO_LI_COUNT_1": 1, "PO_LI_AMOUNT_1": "84.50", "PO_LI_PROD_ID_1": "SKU-492", "MBSHP_ID_1": "", "TRANS_NTOKEN_USED": 0 } ``` --- # Sale (CCAUTHCAP) Authorize and capture funds in a single request. Source: https://developer.inoviopay.com/api/sale.html Markdown: https://developer.inoviopay.com/api/sale.md The Authorization and Capture request will authorize and request capture of the funds in a single request to the Payment Service. See [Authorize](https://developer.inoviopay.com/api/authorize.md#authorize) for the full parameter table; the fields required for `CCAUTHCAP` are identical to those required for `CCAUTHORIZE`. **POST** `https://api.inoviopay.com/payment/pmt_service.cfm` ### Request Parameters | Parameter | Description | |---|---| | `REQUEST_ACTION` (required) | Must be set to `CCAUTHCAP`. | | `XTL_ORDER_ID` (optional) | Merchant's Order ID to associate with the transaction. | ### Handling the Response Monitor these key fields to verify the sale was successfully authorized and captured. If a membership was created during this transaction, you will also receive the new membership ID. | Field Name | Description | |---|---| | `TRANS_STATUS_NAME` | Check this to ensure the transaction was `APPROVED`. | | `TRANS_VALUE_SETTLED` | The final amount that has been captured and settled. | | `TRANS_ID / PO_ID` | Unique identifiers for the transaction and the overarching order. | | `PROC_AUTH_RESPONSE` | The authorization code generated by the processor. | | `MBSHP_ID_x` | If the sale triggered a membership creation, the newly generated Membership ID will be returned here. | | `TRANS_NTOKEN_USED` | Indicates if a Network Token (1) or raw PAN (0) was used for the transaction. | **cURL** ```bash curl -X POST "https://api.inoviopay.com/payment/pmt_service.cfm" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "request_action=CCAUTHCAP&req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&request_response_format=JSON&pmt_numb=4111111111111111&pmt_expiry=122026&pmt_key=123&request_currency=USD&li_value_1=49.99&xtl_order_id=INV-999" ``` **PHP** ```php use Inovio\Gateway\{Credentials, InovioClient}; use Inovio\Gateway\Model\{LineItem, Money, PaymentMethods}; use Inovio\Gateway\Request\TransactionRequest; $client = new InovioClient(new Credentials('api_user', 'P@ssw0rd!', '12345'), 'SANDBOX'); $req = (new TransactionRequest( PaymentMethods::card('4111111111111111', '122026', '123'), [new LineItem('SKU-992', 1, Money::of('49.99', 'USD'))] ))->withIdempotency('INV-999'); // retry-safe by default $sale = $client->sale($req); switch ($sale->status) { case 'APPROVED': echo 'fulfil order ', $sale->orderRef?->poId(), "\n"; break; case 'DECLINED': echo 'declined: ', $sale->outcome->service->code, "\n"; break; case 'PENDING': echo 'pending: ', $sale->nextAction->kind ?? '?', "\n"; break; } ``` **Node** ```ts import { InovioClient, Money, PaymentMethods } from '@inovio/gateway-sdk'; const client = new InovioClient( { reqUsername: 'api_user', reqPassword: 'P@ssw0rd!', siteId: '12345' }, { environment: 'SANDBOX' } ); const sale = await client.sale({ paymentMethod: PaymentMethods.card('4111111111111111', '122026', '123'), lineItems: [{ productId: 'SKU-992', count: 1, value: Money.of('49.99', 'USD') }], idempotency: { xtlOrderId: 'INV-999' }, // retry-safe by default }); switch (sale.status) { case 'APPROVED': console.log('fulfil order', sale.orderRef?.poId); break; case 'DECLINED': console.log('declined:', sale.outcome.service.code); break; case 'PENDING': console.log('pending:', sale.nextAction?.kind); break; } ``` **Python** ```python from inovio_gateway import Credentials, InovioClient, Money, PaymentMethods, TransactionStatus from inovio_gateway.model import Idempotency, LineItem from inovio_gateway.request import TransactionRequest client = InovioClient(Credentials("api_user", "P@ssw0rd!", "12345"), environment="SANDBOX") req = TransactionRequest( payment_method=PaymentMethods.card("4111111111111111", "122026", "123"), line_items=[LineItem("SKU-992", 1, Money.of("49.99", "USD"))], idempotency=Idempotency(xtl_order_id="INV-999"), # retry-safe by default ) sale = client.sale(req) if sale.status is TransactionStatus.APPROVED: print("fulfil order", sale.order_ref.po_id if sale.order_ref else "-") elif sale.status is TransactionStatus.DECLINED: print("declined:", sale.outcome.service.code) elif sale.status is TransactionStatus.PENDING: print("pending:", sale.next_action.kind if sale.next_action else "?") ``` **Java** ```java TransactionRequest req = new TransactionRequest( PaymentMethods.card("4111111111111111", "122026", "123"), new LineItem("SKU-992", 1, Money.of("49.99", "USD"))) .idempotency("INV-999"); // retry-safe by default TransactionResult sale = client.sale(req); switch (sale.status()) { case APPROVED: System.out.println("fulfil order " + sale.orderRef().poId()); break; case DECLINED: System.out.println("declined: " + sale.outcome().service().code()); break; case PENDING: System.out.println("pending: " + sale.nextAction().kind()); break; default: break; } ``` **Response** ```json { "REQUEST_ACTION": "CCAUTHCAP", "REQ_ID": "68192033", "TRANS_STATUS_NAME": "APPROVED", "TRANS_VALUE": 49.99, "CURR_CODE_ALPHA": "USD", "TRANS_VALUE_SETTLED": 49.99, "CURR_CODE_ALPHA_SETTLED": "USD", "TRANS_EXCH_RATE": "", "TRANS_ID": 8839201112, "CUST_ID": 9928102, "XTL_CUST_ID": "cUsT992xP", "PO_ID": 77281920, "XTL_ORDER_ID": "INV-999", "BATCH_ID": 882910, "PROC_NAME": "Inovio Primary", "MERCH_ACCT_ID": 110203, "CARD_BRAND_NAME": "Mastercard", "CARD_TYPE": "MASTERCARD BLACK CARD", "CARD_CLASS": "Consumer Credit", "CARD_PREPAID": 0, "CARD_BANK": "CREDOMATIC INTERNATIONAL", "CARD_COUNTRY": "CRI", "CARD_DETAIL": "Credit", "CARD_BALANCE": "", "PMT_L4": "3535", "PMT_ID": 8829102, "PMT_ID_XTL": "", "PMT_AAU_UPDATE_DT": "", "PMT_AAU_UPDATE_DESC": "", "PROC_UDF01": "", "ANI_RESP_DECISION": "", "PROC_UDF02": "", "PROC_AUTH_RESPONSE": "AUTH99", "PROC_RETRIEVAL_NUM": "1029384A-B8C7-D6E5-F4G3-H2I1J0K9L8M7", "PROC_REFERENCE_NUM": "REF10293847", "PROC_REDIRECT_URL": "", "AVS_RESPONSE": "M", "CVV_RESPONSE": "M", "CARD_BRAND_TRANSID": "", "REQUEST_API_VERSION": "4.14", "P3DS_VENDOR": "", "P3DS_RESPONSE": "", "PO_LI_ID_1": "8829103", "PO_LI_COUNT_1": 1, "PO_LI_AMOUNT_1": "49.99", "PO_LI_PROD_ID_1": "SKU-992", "MBSHP_ID_1": "88291", "TRANS_NTOKEN_USED": 1 } ``` --- # Delayed Capture (CCCAPTURE) Capture funds against a previously placed authorization, in full or in partial amounts. Source: https://developer.inoviopay.com/api/capture.html Markdown: https://developer.inoviopay.com/api/capture.md In order to capture a pending authorization, merchants must send a capture request. The Payment Service allows multiple partial captures as long as the original authorized amount is not exceeded. **POST** `https://api.inoviopay.com/payment/pmt_service.cfm` ### Request Parameters | Parameter | Description | |---|---| | `REQUEST_ACTION` (required) | Must be set to `CCCAPTURE`. | | `REQUEST_REF_PO_ID` (required) | Reference Order ID (`PO_ID`) of the original authorization. | | `LI_VALUE_1` (required) | Amount to capture. This value can be less than or equal to the originally authorized amount. | ### Handling the Response Monitor these key fields to verify the funds from your original authorization have been successfully captured and queued for settlement. | Field Name | Description | |---|---| | `TRANS_STATUS_NAME` | Check this to ensure the capture was `APPROVED`. | | `TRANS_VALUE_SETTLED` | The exact amount that was successfully captured and will be settled to your account. | | `TRANS_ID` | The unique Transaction ID for this specific capture event (this will be different from the original authorization's TRANS_ID). | | `PO_ID` | The overarching Purchase Order ID tying this capture to the original authorization. | **cURL** ```bash curl -X POST "https://api.inoviopay.com/payment/pmt_service.cfm" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "request_action=CCCAPTURE&req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&request_response_format=JSON&request_ref_po_id=18103630&li_value_1=49.99" ``` **PHP** ```php use Inovio\Gateway\{Credentials, InovioClient}; use Inovio\Gateway\Model\Money; use Inovio\Gateway\Refs\Refs; $client = new InovioClient(new Credentials('api_user', 'P@ssw0rd!', '12345'), 'SANDBOX'); $cap = $client->capture(Refs::order('18103630'), Money::of('49.99', 'USD')); echo $cap->status, "\n"; echo 'settled=', var_export($cap->settled, true), "\n"; // batch flips this later — not a failure // Omit the amount to capture the full authorized amount instead: // $client->capture(Refs::order('18103630')); ``` **Node** ```ts import { InovioClient, Money, Refs } from '@inovio/gateway-sdk'; const client = new InovioClient( { reqUsername: 'api_user', reqPassword: 'P@ssw0rd!', siteId: '12345' }, { environment: 'SANDBOX' } ); const cap = await client.capture(Refs.order('18103630'), Money.of('49.99', 'USD')); console.log(cap.status); console.log('settled=', cap.settled); // batch flips this later — not a failure // Omit the amount to capture the full authorized amount instead: // await client.capture(Refs.order('18103630')); ``` **Python** ```python from inovio_gateway import Credentials, InovioClient, Money, Refs client = InovioClient(Credentials("api_user", "P@ssw0rd!", "12345"), environment="SANDBOX") cap = client.capture(Refs.order("18103630"), Money.of("49.99", "USD")) print(cap.status.value) print("settled=", cap.settled) # batch flips this later — not a failure # Omit the amount to capture the full authorized amount instead: # client.capture(Refs.order("18103630")) ``` **Java** ```java import com.inoviopay.gateway.refs.Refs; TransactionResult cap = client.capture(Refs.order("18103630"), Money.of("49.99", "USD")); System.out.println(cap.status()); System.out.println("settled=" + cap.settled()); // batch flips this later — not a failure // Omit the amount to capture the full authorized amount instead: // client.capture(Refs.order("18103630")); ``` **Response** ```json { "REQUEST_ACTION": "CCCAPTURE", "REQ_ID": "74839201", "TRANS_STATUS_NAME": "APPROVED", "TRANS_VALUE": 49.99, "CURR_CODE_ALPHA": "USD", "TRANS_VALUE_SETTLED": 49.99, "CURR_CODE_ALPHA_SETTLED": "USD", "TRANS_EXCH_RATE": "", "TRANS_ID": 3948572109, "CUST_ID": 7382910, "XTL_CUST_ID": "xT883mP", "PO_ID": 18103630, "XTL_ORDER_ID": "INV-999", "BATCH_ID": 883921, "PROC_NAME": "Inovio Primary", "MERCH_ACCT_ID": 110203, "CARD_BRAND_NAME": "Visa", "CARD_TYPE": "VISA CLASSIC", "CARD_CLASS": "Consumer Debit", "CARD_PREPAID": 1, "CARD_BANK": "BOFI FEDERAL BANK", "CARD_COUNTRY": "USA", "CARD_DETAIL": "Debit", "CARD_BALANCE": "", "PMT_L4": "2921", "PMT_ID": 4895565, "PMT_ID_XTL": "", "PMT_AAU_UPDATE_DT": "", "PMT_AAU_UPDATE_DESC": "", "PROC_UDF01": "", "ANI_RESP_DECISION": "", "PROC_UDF02": "", "PROC_AUTH_RESPONSE": "AUTH881", "PROC_RETRIEVAL_NUM": "7D8A9F0E-1B2C-3D4E-5F6A-7B8C9D0E1F2A", "PROC_REFERENCE_NUM": "REF44920193", "PROC_REDIRECT_URL": "", "AVS_RESPONSE": "M", "CVV_RESPONSE": "M", "CARD_BRAND_TRANSID": "", "REQUEST_API_VERSION": "4.14", "P3DS_VENDOR": "", "P3DS_RESPONSE": "", "PO_LI_ID_1": "9928173", "PO_LI_COUNT_1": 1, "PO_LI_AMOUNT_1": 49.99, "PO_LI_PROD_ID_1": 111205, "MBSHP_ID_1": "", "TRANS_NTOKEN_USED": 0 } ``` --- # Credit (CCCREDIT) Return funds for a captured or settled authorization. Source: https://developer.inoviopay.com/api/credit.html Markdown: https://developer.inoviopay.com/api/credit.md Merchants may request to return funds for a captured authorization. Merchants may only credit transactions that have been captured or settled. **POST** `https://api.inoviopay.com/payment/pmt_service.cfm` ### Request Parameters | Parameter | Description | |---|---| | `REQUEST_ACTION` (required) | Must be set to `CCCREDIT`. | | `REQUEST_REF_PO_ID` (required) | Reference Order ID (`PO_ID`) of the original transaction. | | `LI_VALUE_1` (required) | Amount to credit. The amount may not exceed the original authorization's total amount. | ### Handling the Response When processing a refund, monitor the response to confirm the credit was applied to the original order successfully. | Field Name | Description | |---|---| | `TRANS_STATUS_NAME` | Check this to ensure the credit was `APPROVED`. | | `TRANS_VALUE` | The approved credit amount. Note that refunds will return as a negative value (e.g., `-10.00`). | | `TRANS_ID` | The unique Transaction ID assigned specifically to this refund event. | | `PO_ID` | The original Purchase Order ID, linking this credit back to the initial transaction. | **cURL** ```bash curl -X POST "https://api.inoviopay.com/payment/pmt_service.cfm" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "request_action=CCCREDIT&req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&request_response_format=JSON&request_ref_po_id=18103630&li_value_1=10.00" ``` **PHP** ```php use Inovio\Gateway\{Credentials, InovioClient}; use Inovio\Gateway\Model\Money; use Inovio\Gateway\Refs\Refs; $client = new InovioClient(new Credentials('api_user', 'P@ssw0rd!', '12345'), 'SANDBOX'); // You can only refund what was captured. $refund = $client->refund(Refs::order('18103630'), Money::of('10.00', 'USD')); echo $refund->status, "\n"; echo $refund->amount?->toWire(), "\n"; // negative on the wire // Omit the amount to refund the full order instead: // $client->refund(Refs::order('18103630')); ``` **Node** ```ts import { InovioClient, Money, Refs } from '@inovio/gateway-sdk'; const client = new InovioClient( { reqUsername: 'api_user', reqPassword: 'P@ssw0rd!', siteId: '12345' }, { environment: 'SANDBOX' } ); // You can only refund what was captured. const refund = await client.refund(Refs.order('18103630'), Money.of('10.00', 'USD')); console.log(refund.status); console.log(refund.amount?.amount); // negative on the wire // Omit the amount to refund the full order instead: // await client.refund(Refs.order('18103630')); ``` **Python** ```python from inovio_gateway import Credentials, InovioClient, Money, Refs client = InovioClient(Credentials("api_user", "P@ssw0rd!", "12345"), environment="SANDBOX") # You can only refund what was captured. refund = client.refund(Refs.order("18103630"), Money.of("10.00", "USD")) print(refund.status.value) print(refund.amount.to_wire() if refund.amount else "-") # negative on the wire # Omit the amount to refund the full order instead: # client.refund(Refs.order("18103630")) ``` **Java** ```java import com.inoviopay.gateway.refs.Refs; // You can only refund what was captured. TransactionResult refund = client.refund(Refs.order("18103630"), Money.of("10.00", "USD")); System.out.println(refund.status()); System.out.println(refund.amount() == null ? "-" : refund.amount().toWire()); // negative on the wire // Omit the amount to refund the full order instead: // client.refund(Refs.order("18103630")); ``` **Response** ```json { "REQUEST_ACTION": "CCCREDIT", "REQ_ID": "84729103", "TRANS_STATUS_NAME": "APPROVED", "TRANS_VALUE": -10.00, "CURR_CODE_ALPHA": "USD", "TRANS_VALUE_SETTLED": -10.00, "CURR_CODE_ALPHA_SETTLED": "USD", "TRANS_EXCH_RATE": "", "TRANS_ID": 3948572110, "CUST_ID": 7382910, "XTL_CUST_ID": "xT883mP", "PO_ID": 18103630, "XTL_ORDER_ID": "INV-999", "BATCH_ID": 883922, "PROC_NAME": "Inovio Primary", "MERCH_ACCT_ID": 110203, "CARD_BRAND_NAME": "Visa", "CARD_TYPE": "VISA CLASSIC", "CARD_CLASS": "Consumer Debit", "CARD_PREPAID": 1, "CARD_BANK": "BOFI FEDERAL BANK", "CARD_COUNTRY": "USA", "CARD_DETAIL": "Debit", "CARD_BALANCE": "", "PMT_L4": "2921", "PMT_ID": 4895565, "PMT_ID_XTL": "", "PMT_AAU_UPDATE_DT": "", "PMT_AAU_UPDATE_DESC": "", "PROC_UDF01": "", "ANI_RESP_DECISION": "", "PROC_UDF02": "", "PROC_AUTH_RESPONSE": "REF81371", "PROC_RETRIEVAL_NUM": "A1B2C3D4-E5F6-7A8B-9C0D-E1F2A3B4C5D6", "PROC_REFERENCE_NUM": "REF755033258", "PROC_REDIRECT_URL": "", "AVS_RESPONSE": "M", "CVV_RESPONSE": "M", "CARD_BRAND_TRANSID": "", "REQUEST_API_VERSION": "4.14", "P3DS_VENDOR": "", "P3DS_RESPONSE": "", "PO_LI_ID_1": "9928174", "PO_LI_COUNT_1": 1, "PO_LI_AMOUNT_1": -10.00, "PO_LI_PROD_ID_1": "111205", "MBSHP_ID_1": "", "TRANS_NTOKEN_USED": 0 } ``` --- # Reversal (CCREVERSE) Reverse or void an authorization before it settles, with an option to auto-reroute to a credit if it already has. Source: https://developer.inoviopay.com/api/reversal.html Markdown: https://developer.inoviopay.com/api/reversal.md Merchants may request to reverse or void the original authorization before it is settled. **POST** `https://api.inoviopay.com/payment/pmt_service.cfm` ### Request Parameters | Parameter | Description | |---|---| | `REQUEST_ACTION` (required) | Must be set to `CCREVERSE`. | | `REQUEST_REF_PO_ID` (required) | Reference Order ID (`PO_ID`) of the original transaction. | | `CREDIT_ON_FAIL` (optional) | If set to `1`, the system will automatically attempt to credit the transaction if the reversal request fails (e.g., if it already settled). | ### Handling the Response Monitor the response to verify the authorization was successfully voided before settlement. | Field Name | Description | |---|---| | `TRANS_STATUS_NAME` | Check this to ensure the reversal was `APPROVED`. | | `TRANS_VALUE` | The approved reversal amount. Similar to credits, this will return as a negative value (e.g., `-25.00`). | | `TRANS_ID` | The unique Transaction ID assigned specifically to this reversal event. | | `PO_ID` | The original Purchase Order ID linking this void back to the initial authorization. | **cURL** ```bash curl -X POST "https://api.inoviopay.com/payment/pmt_service.cfm" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "request_action=CCREVERSE&req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&request_response_format=JSON&request_ref_po_id=18103630&credit_on_fail=1" ``` **PHP** ```php use Inovio\Gateway\{Credentials, InovioClient}; use Inovio\Gateway\Refs\Refs; $client = new InovioClient(new Credentials('api_user', 'P@ssw0rd!', '12345'), 'SANDBOX'); // creditOnFail: true sets CREDIT_ON_FAIL=1 — if the order already settled, // the gateway re-routes this call to CCCREDIT instead of declining. $reversed = $client->reverse(Refs::order('18103630'), creditOnFail: true); echo $reversed->status, "\n"; echo $reversed->amount?->toWire(), "\n"; // negative on the wire ``` **Node** ```ts import { InovioClient, Refs } from '@inovio/gateway-sdk'; const client = new InovioClient( { reqUsername: 'api_user', reqPassword: 'P@ssw0rd!', siteId: '12345' }, { environment: 'SANDBOX' } ); // NOTE: CREDIT_ON_FAIL is not exposed on reverse() yet — call the API // directly (request_action=CCREVERSE&credit_on_fail=1) for the auto-credit reroute. const reversed = await client.reverse(Refs.order('18103630')); console.log(reversed.status); console.log(reversed.amount?.amount); // negative on the wire ``` **Python** ```python from inovio_gateway import Credentials, InovioClient, Refs client = InovioClient(Credentials("api_user", "P@ssw0rd!", "12345"), environment="SANDBOX") # NOTE: CREDIT_ON_FAIL is not exposed on reverse() yet — call the API # directly (request_action=CCREVERSE&credit_on_fail=1) for the auto-credit reroute. reversed_tx = client.reverse(Refs.order("18103630")) print(reversed_tx.status.value) print(reversed_tx.amount.to_wire() if reversed_tx.amount else "-") # negative on the wire ``` **Java** ```java import com.inoviopay.gateway.refs.Refs; // NOTE: CREDIT_ON_FAIL is not exposed on reverse() yet — call the API // directly (request_action=CCREVERSE&credit_on_fail=1) for the auto-credit reroute. TransactionResult reversed = client.reverse(Refs.order("18103630")); System.out.println(reversed.status()); System.out.println(reversed.amount() == null ? "-" : reversed.amount().toWire()); // negative on the wire ``` **Response** ```json { "REQUEST_ACTION": "CCREVERSE", "REQ_ID": "93827164", "TRANS_STATUS_NAME": "APPROVED", "TRANS_VALUE": -25.00, "CURR_CODE_ALPHA": "USD", "TRANS_VALUE_SETTLED": -25.00, "CURR_CODE_ALPHA_SETTLED": "USD", "TRANS_EXCH_RATE": "", "TRANS_ID": 3948572111, "CUST_ID": 7382910, "XTL_CUST_ID": "xT883mP", "PO_ID": 18103630, "XTL_ORDER_ID": "", "BATCH_ID": 883922, "PROC_NAME": "Inovio Primary", "MERCH_ACCT_ID": 110203, "CARD_BRAND_NAME": "Visa", "CARD_TYPE": "VISA CLASSIC", "CARD_CLASS": "Consumer Debit", "CARD_PREPAID": 1, "CARD_BANK": "BOFI FEDERAL BANK", "CARD_COUNTRY": "USA", "CARD_DETAIL": "Debit", "CARD_BALANCE": "", "PMT_L4": "2921", "PMT_ID": 4895565, "PMT_ID_XTL": "", "PMT_AAU_UPDATE_DT": "", "PMT_AAU_UPDATE_DESC": "", "PROC_UDF01": "", "ANI_RESP_DECISION": "", "PROC_UDF02": "", "PROC_AUTH_RESPONSE": "VOID813", "PROC_RETRIEVAL_NUM": "B2C3D4E5-F6A7-8B9C-0D1E-2F3A4B5C6D7E", "PROC_REFERENCE_NUM": "REF187604789", "PROC_REDIRECT_URL": "", "AVS_RESPONSE": "M", "CVV_RESPONSE": "M", "CARD_BRAND_TRANSID": "", "REQUEST_API_VERSION": "4.14", "P3DS_VENDOR": "", "P3DS_RESPONSE": "", "PO_LI_ID_1": "9928175", "PO_LI_COUNT_1": 1, "PO_LI_AMOUNT_1": -25.00, "PO_LI_PROD_ID_1": "111205", "MBSHP_ID_1": "", "TRANS_NTOKEN_USED": 0 } ``` ## Reverse or credit in one call: CREDIT_ON_FAIL `CCREVERSE` reverses the original authorization; `CCREVERSECAP` reverses a successful `CCCAPTURE` transaction instead. Both actions accept `CREDIT_ON_FAIL`. Setting `CREDIT_ON_FAIL=1` on `CCREVERSE` (or `CCREVERSECAP`) tells the gateway that if the order is already settled, and therefore cannot be reversed, the gateway itself re-routes the request to `CCCREDIT` instead of just failing. You send one request and the gateway decides whether a reversal or a credit is the correct operation for the order's current state. > **Verified against the gateway** > This re-route behavior was verified against the gateway directly, not just claimed in the spec. The response for a re-routed request comes back with `REQUEST_ACTION` equal to `CCCREDIT`, not `CCREVERSE`. Check `REQUEST_ACTION` in the response, not just `TRANS_STATUS_NAME`, if you need to know server-side whether the re-route happened. > > Without `CREDIT_ON_FAIL`, attempting to reverse an already-settled order returns gateway service code 515 (a decline) instead of succeeding. Because the re-route is transparent, do not write your own settlement pre-check before deciding whether to call `CCREVERSE` or `CCCREDIT`. Send `CCREVERSE` with `CREDIT_ON_FAIL=1` and read `REQUEST_ACTION` off the response to see which operation the gateway actually performed. --- # Order Status (CCSTATUS) Look up the full transaction history of an order by the gateway's order ID or your own external reference. Source: https://developer.inoviopay.com/api/status.html Markdown: https://developer.inoviopay.com/api/status.md Check the status of an order. You can query using the Gateway's Order ID or your external reference ID. **POST** `https://api.inoviopay.com/payment/pmt_service.cfm` ### Request Parameters | Parameter | Description | |---|---| | `REQUEST_ACTION` (required) | Must be set to `CCSTATUS`. | | `REQUEST_REF_PO_ID` (optional) | Lookup using the Gateway's `PO_ID`. | | `REQUEST_REF_PO_ID_XTL` (optional) | Lookup using the Merchant's `XTL_ORDER_ID`. | ### Handling the Response Unlike standard transaction responses, `CCSTATUS` returns a matrix format to accommodate the entire lifecycle of an order. The response contains two arrays: a `COLUMNS` array defining the fields, and a `DATA` array containing one or more rows of transaction events (e.g., the original authorization, subsequent captures, refunds, or chargebacks). | Array Name | Description | |---|---| | `COLUMNS` | An ordered list of string values representing the field names (e.g., `REQUEST_ACTION`, `TRANS_STATUS_NAME`, `TRANS_VALUE`). | | `DATA` | An array of arrays. Each inner array represents a distinct transaction event tied to the order. To parse the data, map the index of a value in the inner array to the corresponding index in the `COLUMNS` array. | For example, in the response below, `COLUMNS[0]` is `REQUEST_ACTION` and `COLUMNS[1]` is `TRANS_STATUS_NAME`, so `DATA[0][0]` (`"CCAUTHCAP"`) and `DATA[0][1]` (`"APPROVED"`) describe the same event: the first row's request action and its status. The response below has three rows: the original `CCAUTHCAP`, a subsequent `CCCREDIT`, and a `CCCHARGEBACK`, each identified the same way by walking `COLUMNS` alongside the row. **cURL** ```bash curl -X POST "https://api.inoviopay.com/payment/pmt_service.cfm" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "request_action=CCSTATUS&req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&request_response_format=JSON&request_ref_po_id_xtl=INV-999" ``` **PHP** ```php use Inovio\Gateway\{Credentials, InovioClient}; use Inovio\Gateway\Refs\Refs; $client = new InovioClient(new Credentials('api_user', 'P@ssw0rd!', '12345'), 'SANDBOX'); $s = $client->status(Refs::xtlOrder('INV-999')); echo 'legs=', count($s->transactions), "\n"; echo 'authorized=', $s->authorized->toWire(), "\n"; echo 'captured=', $s->captured->toWire(), "\n"; echo 'refunded=', $s->refunded->toWire(), "\n"; echo 'net=', $s->net->toWire(), "\n"; // captured - refunded echo 'outstanding=', $s->outstanding->toWire(), "\n"; // authorized - captured ``` **Node** ```ts import { InovioClient, Refs } from '@inovio/gateway-sdk'; const client = new InovioClient( { reqUsername: 'api_user', reqPassword: 'P@ssw0rd!', siteId: '12345' }, { environment: 'SANDBOX' } ); const s = await client.status(Refs.xtlOrder('INV-999')); console.log('legs=', s.transactions.length); console.log('authorized=', s.authorized?.amount); console.log('captured=', s.captured?.amount); console.log('refunded=', s.refunded?.amount); console.log('net=', s.net?.amount); // captured - refunded console.log('outstanding=', s.outstanding?.amount); // authorized - captured ``` **Python** ```python from inovio_gateway import Credentials, InovioClient, Refs client = InovioClient(Credentials("api_user", "P@ssw0rd!", "12345"), environment="SANDBOX") s = client.status(Refs.xtl_order("INV-999")) print("legs=", len(s.transactions)) print("authorized=", s.authorized.to_wire()) print("captured=", s.captured.to_wire()) print("refunded=", s.refunded.to_wire()) print("net=", s.net.to_wire()) # captured - refunded print("outstanding=", s.outstanding.to_wire()) # authorized - captured ``` **Java** ```java import com.inoviopay.gateway.refs.Refs; import com.inoviopay.gateway.result.OrderStatus; OrderStatus s = client.status(Refs.xtlOrder("INV-999")); System.out.println("legs=" + s.transactions().size()); System.out.println("authorized=" + s.authorized().toWire()); System.out.println("captured=" + s.captured().toWire()); System.out.println("refunded=" + s.refunded().toWire()); System.out.println("net=" + s.net().toWire()); // captured - refunded System.out.println("outstanding=" + s.outstanding().toWire()); // authorized - captured ``` **Response** ```json { "COLUMNS": [ "REQUEST_ACTION", "TRANS_STATUS_NAME", "TRANS_VALUE", "TRANS_ID", "CUST_ID", "XTL_CUST_ID", "PO_ID", "XTL_ORDER_ID", "BATCH_ID", "PROC_NAME", "MERCH_ACCT_ID", "CARD_BRAND_NAME", "PMT_ID", "PMT_L4", "PROC_UDF01", "PROC_UDF02", "PROC_AUTH_RESPONSE", "PROC_RETRIEVAL_NUM", "PROC_REFERENCE_NUM", "AVS_RESPONSE", "CVV_RESPONSE", "CURR_CODE_ALPHA", "CURR_NAME", "TRANS_ID" ], "DATA": [ [ "CCAUTHCAP", "APPROVED", 125.50, 485729103, 9820193, "xtLCust88", 8819203, "INV-882", 88291, "Inovio Primary", 110203, "Visa", 3920192, "1111", "", "", "AUTH881", "B03C2B04-00A5-4179-A9D94F0CDD76197D", "REF847156508", "M", "M", "USD", "US Dollar", 485729103 ], [ "CCCREDIT", "APPROVED", -25.00, 485729104, 9820193, "xtLCust88", 8819203, "INV-882", 88292, "Inovio Primary", 110203, "Visa", 3920192, "1111", "", "", "REF291", "C14D3C15-11B6-5280-B0E05G1DEE87208E", "REF847156509", "", "", "USD", "US Dollar", 485729104 ], [ "CCCHARGEBACK", "APPROVED", -100.50, 485729105, 9820193, "xtLCust88", 8819203, "INV-882", 88293, "Inovio Primary", 110203, "Visa", 3920192, "1111", "", "", "", "", "", "", "", "USD", "US Dollar", 485729105 ] ] } ``` --- # ACH / eCheck API Process electronic funds transfers directly from customer bank accounts, including the ACH transaction lifecycle, supported actions, request parameters, and status values. Source: https://developer.inoviopay.com/api/ach.html Markdown: https://developer.inoviopay.com/api/ach.md ## ACH & eCheck Transactions Process electronic funds transfers directly from customer bank accounts. ACH authorization requests will typically return a `PENDING` status while the funds clear through the banking network. The following steps describe the lifecycle of a transaction processed with an ACH account: 1. **Customer initiates purchase** on the payment page. 2. **Merchant submits transaction request** to the Payments Service. This should include the following data: - Customer and payment information. - Merchant gateway credentials and other gateway parameters as needed. 3. **Payments Service transmits** the transaction request to the bank. 4. **Merchant receives gateway response.** The transaction status should be `PENDING`. 5. **Post-processing transaction status update** (both methods are optional): - **Postback API:** Payments Service sends a Postback to the merchant as the transaction state changes. Postbacks are real-time. - **Order Detail Report:** Merchant downloads order and transaction data using the Order Detail Report service. > **Note on status updates** > If you choose not to use the Postback API or Order Detail Report to receive updates on the status of an ACH order, you can log into our portal and check the status manually by searching for the order ID. > > To keep merchants updated on the status of their pending ACH transactions, we can send realtime Postbacks to the merchant. > > Merchants may also download transaction status updates as an alternative to Postback by using Order Detail Report service. | Parameter | Description | |---|---| | `REQUEST_ACTION` (required) | Set to `ACHAUTHCAP` for Auth/Capture, or `ACHAUTHORIZE` for validation without capture. | | `PMT_NUMB` (required) | Customer Bank Account Number. | | `BANK_IDENTIFIER` (required) | Bank Routing Number. | | `LI_VALUE_1` (required) | Transaction Amount. | ## ACH Actions Below are the only supported gateway actions for ACH. Any other gateway action or feature will not work. | REQUEST_ACTION | Description | |---|---| | `ACHAUTHCAP` | Used for authorization and capture requests. | | `ACHAUTHORIZE` | Used for Authorizations without Capture. | | `ACHREVERSE` | Used for Authorization Capture Reversal. | | `ACHCREDIT` | Used for transaction credit requests. | | `ACHPAYOUT` | Used for ACH direct deposit payout. | ### Handling ACH Responses Because ACH transactions rely on the banking network to clear funds, initial responses will typically return a `PENDING` status rather than an immediate approval or decline. | Field Name | Description | |---|---| | `TRANS_STATUS_NAME` | Check this to ensure the transaction is `PENDING` (or `APPROVED` if the bank clears it instantly). | | `TRANS_VALUE` | The amount requested for the transaction. | | `TRANS_ID` | The unique Transaction ID for the gateway event. | | `PO_ID` | The Purchase Order ID linking to the transaction. | | `PROC_AUTH_RESPONSE` | The authorization string generated by the processor. | ### Auth & capture example > **Not in the SDKs yet** > ACH is not implemented in any of the four SDKs. `BankAccount` exists as a declared-but-unconstructible payment method variant (`PaymentMethods` only builds `card`, `token`, `savedCard` in v1; a `BankAccount` reaches the wire only by hand-assembling parameters outside the client), and none of `sale()`/`authorize()`/`reverse()`/`forceCredit()` recognize `ACHAUTHCAP`, `ACHAUTHORIZE`, `ACHREVERSE`, `ACHCREDIT` or `ACHPAYOUT` as actions, those five constants appear only as decorative labels in the generated enum files, not as request builders. Use the cURL example directly, or see [the SDKs](https://developer.inoviopay.com/sdks/index.md) for the v1 card surface. ### Authorize only example > **Not in the SDKs yet** > No SDK builds an `ACHAUTHORIZE` request. See [the SDKs](https://developer.inoviopay.com/sdks/index.md). ### Reversal example > **Not in the SDKs yet** > `reverse()` exists in all four SDKs, but it only builds `CCREVERSE` (card reversal), there is no `ACHREVERSE` path. See [the SDKs](https://developer.inoviopay.com/sdks/index.md). ### Credit example > **Not in the SDKs yet** > `forceCredit()` exists in all four SDKs, but it only builds `CCCREDIT` (card credit), there is no `ACHCREDIT` path. See [the SDKs](https://developer.inoviopay.com/sdks/index.md). ### Payout example > **Not in the SDKs yet** > `ACHPAYOUT` has no SDK equivalent in any of the four languages. See [the SDKs](https://developer.inoviopay.com/sdks/index.md). **cURL** ```bash curl -X POST "https://api.inoviopay.com/payment/pmt_service.cfm" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "request_action=ACHAUTHCAP&req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&request_response_format=JSON&pmt_numb=1234567890&bank_identifier=987654321&li_value_1=50.00" ``` **Response** ```json { "REQUEST_ACTION": "ACHAUTHCAP", "REQ_ID": "67392811", "TRANS_STATUS_NAME": "PENDING", "TRANS_VALUE": 50.00, "CURR_CODE_ALPHA": "USD", "TRANS_VALUE_SETTLED": 50.00, "CURR_CODE_ALPHA_SETTLED": "USD", "TRANS_EXCH_RATE": "", "TRANS_ID": 3948572115, "CUST_ID": 9827163, "XTL_CUST_ID": "", "PO_ID": 99281736, "XTL_ORDER_ID": "", "BATCH_ID": 883925, "PROC_NAME": "Inovio ACH Primary", "MERCH_ACCT_ID": 141630, "CARD_BRAND_NAME": "ACH", "CARD_TYPE": "", "CARD_PREPAID": 0, "CARD_BANK": "", "CARD_DETAIL": "", "CARD_BALANCE": "", "PMT_L4": "7890", "PMT_ID": 8839210, "PMT_ID_XTL": "", "PMT_AAU_UPDATE_DT": "", "PMT_AAU_UPDATE_DESC": "", "PROC_UDF01": "", "PROC_UDF02": "", "PROC_AUTH_RESPONSE": "PEND992", "PROC_RETRIEVAL_NUM": "A1B2C3D4-E5F6-7A8B-9C0D-E1F2A3B4C5D6", "PROC_REFERENCE_NUM": "REF88291039", "PROC_REDIRECT_URL": "", "AVS_RESPONSE": "M", "CVV_RESPONSE": "M", "CARD_BRAND_TRANSID": "", "REQUEST_API_VERSION": "4.14", "P3DS_VENDOR": "", "P3DS_RESPONSE": "", "PO_LI_ID_1": "9928178", "PO_LI_COUNT_1": 1, "PO_LI_AMOUNT_1": "50.00", "PO_LI_PROD_ID_1": "136381", "MBSHP_ID_1": "", "TRANS_NTOKEN_USED": 0 } ``` **cURL** ```bash curl -X POST "https://api.inoviopay.com/payment/pmt_service.cfm" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "request_action=ACHAUTHORIZE&req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&request_response_format=JSON&pmt_numb=1234567890&bank_identifier=987654321&li_value_1=50.00" ``` **Response** ```json { "REQUEST_ACTION": "ACHAUTHORIZE", "REQ_ID": "67392812", "TRANS_STATUS_NAME": "PENDING", "TRANS_VALUE": 50.00, "CURR_CODE_ALPHA": "USD", "TRANS_VALUE_SETTLED": 50.00, "CURR_CODE_ALPHA_SETTLED": "USD", "TRANS_EXCH_RATE": "", "TRANS_ID": 3948572116, "CUST_ID": 9827163, "PO_ID": 99281737, "BATCH_ID": 883925, "PROC_NAME": "Inovio ACH Primary", "MERCH_ACCT_ID": 141630, "CARD_BRAND_NAME": "ACH", "PMT_L4": "7890", "PMT_ID": 8839210, "PROC_AUTH_RESPONSE": "PEND993", "PROC_RETRIEVAL_NUM": "B2C3D4E5-F6A7-8B9C-0D1E-2F3A4B5C6D7E", "REQUEST_API_VERSION": "4.14", "PO_LI_ID_1": "9928179", "PO_LI_AMOUNT_1": "50.00" } ``` **cURL** ```bash curl -X POST "https://api.inoviopay.com/payment/pmt_service.cfm" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "request_action=ACHREVERSE&req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&request_response_format=JSON&request_ref_po_id=99281736" ``` **Response** ```json { "REQUEST_ACTION": "ACHREVERSE", "TRANS_STATUS_NAME": "APPROVED", "TRANS_VALUE": -10, "CURR_CODE_ALPHA": "USD", "TRANS_VALUE_SETTLED": -10, "CURR_CODE_ALPHA_SETTLED": "USD", "TRANS_EXCH_RATE": "", "TRANS_ID": 3948572120, "CUST_ID": 9827164, "XTL_CUST_ID": "", "PO_ID": 99281739, "XTL_ORDER_ID": "", "BATCH_ID": 883926, "PROC_NAME": "ACHProcessor", "MERCH_ACCT_ID": 141630, "CARD_BRAND_NAME": "", "CARD_DETAIL": "", "CARD_TYPE": "", "CARD_PREPAID": "", "CARD_BANK": "", "PMT_L4": "4567", "PMT_ID": 8839211, "PMT_ID_XTL": "", "PROC_UDF01": "", "PROC_UDF02": "", "PROC_AUTH_RESPONSE": "", "PROC_RETRIEVAL_NUM": "", "PROC_REFERENCE_NUM": "", "PROC_REDIRECT_URL": "", "AVS_RESPONSE": "", "CVV_RESPONSE": "", "REQ_ID": "84729106", "REQUEST_API_VERSION": "4.14", "PO_LI_ID_1": "9928183", "PO_LI_COUNT_1": 1, "PO_LI_AMOUNT_1": -10, "PO_LI_PROD_ID_1": "12345", "MBSHP_ID_1": "" } ``` **cURL** ```bash curl -X POST "https://api.inoviopay.com/payment/pmt_service.cfm" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "request_action=ACHCREDIT&req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&request_response_format=JSON&request_ref_po_id=99281736&li_value_1=15.00" ``` **Response** ```json { "REQUEST_ACTION": "ACHCREDIT", "TRANS_STATUS_NAME": "", "TRANS_VALUE": "", "TRANS_ID": "", "REQ_ID": "84729105", "CUST_ID": "9928104", "XTL_CUST_ID": "xT883mP", "PMT_ID": "", "MERCH_ACCT_ID": "141630", "CARD_BRAND_NAME": "", "PMT_L4": "", "API_RESPONSE": "0", "API_ADVICE": " ", "SERVICE_RESPONSE": 512, "SERVICE_ADVICE": "Order not found", "PROCESSOR_RESPONSE": 0, "PROCESSOR_ADVICE": " ", "INDUSTRY_RESPONSE": 0, "INDUSTRY_ADVICE": " ", "REF_FIELD": "", "PROC_NAME": "", "AVS_RESPONSE": "", "CVV_RESPONSE": "", "PROC_REDIRECT_URL": "", "REQUEST_API_VERSION": "4.14", "TRANS_NTOKEN_USED": 0 } ``` **cURL** ```bash curl -X POST "https://api.inoviopay.com/payment/pmt_service.cfm" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "request_action=ACHPAYOUT&req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&request_response_format=JSON&pmt_numb=1234567890&bank_identifier=987654321&li_value_1=250.00" ``` **Response** ```json { "REQUEST_ACTION": "ACHPAYOUT", "TRANS_STATUS_NAME": "", "TRANS_VALUE": "", "TRANS_ID": "", "REQ_ID": "84729104", "CUST_ID": 9928103, "XTL_CUST_ID": "xT883mP", "PMT_ID": 8829103, "MERCH_ACCT_ID": "110203", "CARD_BRAND_NAME": "ACH", "PMT_L4": "7890", "API_RESPONSE": "0", "API_ADVICE": " ", "SERVICE_RESPONSE": 522, "SERVICE_ADVICE": "Unsupported card brand", "PROCESSOR_RESPONSE": 0, "ANI_RESP_DECISION": "", "PROCESSOR_ADVICE": " ", "INDUSTRY_RESPONSE": 0, "INDUSTRY_ADVICE": " ", "REF_FIELD": "", "PROC_NAME": "", "AVS_RESPONSE": "", "CVV_RESPONSE": "", "PROC_REDIRECT_URL": "", "REQUEST_API_VERSION": "4.14", "TRANS_NTOKEN_USED": 0 } ``` ## ACH Request Parameters The table below describes parameters needed in sending ACH purchase request to the gateway. Merchants may send additional parameters, as described in the Payments Payment Service API, such as customer billing address, email, website login and other pass-through data. | Field Name | Description | |---|---| | `SITE_ID` | Merchant's Website ID. | | `LI_PROD_ID_1` | Line Item Product ID 1. | | `LI_VALUE_1` | Line Item Transaction Amount 1. | | `PMT_NUMB` | Bank Account Number. | | `BANK_IDENTIFIER` | Routing Number. | | `MERCH_ACCT_ID` | Merchant Account ID. | | `REQUEST_CURRENCY` | 3-letter Currency Code. | | `CUST_FNAME` | Customer First Name. | | `CUST_LNAME` | Customer Last Name. | | `BILL_ADDR` | Billing Street Address. | | `BILL_ADDR_CITY` | Billing City Name. | | `BILL_ADDR_STATE` | Billing State 2-letter Code. | | `BILL_ADDR_COUNTRY` | Billing Country 2-letter Code. | | `BILL_ADDR_ZIP` | Billing ZIP or Postal Code. | ## ACH Transaction Status | TRANS_STATUS_NAME | Description | |---|---| | `APPROVED` | Transaction has been approved. | | `PENDING` | Transaction is in pending status. | | `RUNNING` | Transaction processing was not completed or is waiting completion usually because of gateway error. | --- # Partial Authorization Accept an authorization for less than the full requested amount when a prepaid or debit card can't cover the order total. Source: https://developer.inoviopay.com/api/partial-auth.html Markdown: https://developer.inoviopay.com/api/partial-auth.md Partial Authorization is a processor-specific feature that allows merchants to accept an authorization for a portion of the total requested amount. This is common when customers use prepaid or debit cards with a balance lower than the order total. ### How it Works - You submit a standard `CCAUTHCAP` or `CCAUTHORIZE` request. - You include the `PARTIAL_AUTH` flag and define a `PARTIAL_AUTH_MIN` (the lowest amount you are willing to accept to proceed). - If the card has at least the minimum amount, the gateway returns an `APPROVED` status. | Field Name | Description | |---|---| | PARTIAL_AUTH | Set to `1` to enable. Set to `0` or omit to disable. | | PARTIAL_AUTH_MIN | The minimum numeric amount you will accept (e.g., `10.00`). If the card balance is below this, the transaction is declined. | > **Critical Implementation Note** > When Partial Authorization is enabled, you **must** parse the `TRANS_VALUE` field in the response. Do not assume an `APPROVED` status means the full amount was captured. Your system must handle the remaining balance via a secondary payment method. **cURL** ```bash curl -X POST "https://api.inoviopay.com/payment/pmt_service.cfm" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "request_action=CCAUTHCAP&req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&request_response_format=JSON&pmt_numb=4111111111111111&pmt_expiry=122026&pmt_key=123&request_currency=USD&li_value_1=75.00&xtl_order_id=INV-999&partial_auth=1&partial_auth_min=10.00" ``` **PHP** ```php use Inovio\Gateway\{Credentials, InovioClient}; use Inovio\Gateway\Model\{LineItem, Money, PartialAuth, PaymentMethods}; use Inovio\Gateway\Request\TransactionRequest; $client = new InovioClient(new Credentials('api_user', 'P@ssw0rd!', '12345'), 'SANDBOX'); $req = (new TransactionRequest( PaymentMethods::card('4111111111111111', '122026', '123'), [new LineItem('SKU-992', 1, Money::of('75.00', 'USD'))] ))->withIdempotency('INV-999'); $req->partialAuth = new PartialAuth(enabled: true); $req->partialAuth->minimumAmount = Money::of('10.00', 'USD'); $result = $client->sale($req); match ($result->status) { 'APPROVED' => /* $result->raw['TRANS_VALUE'] may be less than 75.00 — reconcile the remainder */, 'DECLINED' => /* card balance was below PARTIAL_AUTH_MIN */, default => /* PENDING | RUNNING | FAILED */, }; ``` **Node** ```ts import { InovioClient, Money, PaymentMethods } from '@inovio/gateway-sdk'; const client = new InovioClient( { reqUsername: 'api_user', reqPassword: 'P@ssw0rd!', siteId: '12345' }, { environment: 'SANDBOX' } ); const result = await client.sale({ paymentMethod: PaymentMethods.card('4111111111111111', '122026', '123'), lineItems: [{ productId: 'SKU-992', count: 1, value: Money.of('75.00', 'USD') }], idempotency: { xtlOrderId: 'INV-999' }, partialAuth: { enabled: true, minimumAmount: Money.of('10.00', 'USD') }, }); switch (result.status) { case 'APPROVED': /* result.raw.TRANS_VALUE may be less than 75.00 */ break; case 'DECLINED': /* card balance was below the minimum */ break; default: /* PENDING | RUNNING | FAILED */ break; } ``` **Python** ```python from inovio_gateway import Credentials, InovioClient, Money, PaymentMethods, TransactionStatus from inovio_gateway.model import Idempotency, LineItem, PartialAuth from inovio_gateway.request import TransactionRequest client = InovioClient(Credentials("api_user", "P@ssw0rd!", site_id="12345"), environment="SANDBOX") result = client.sale(TransactionRequest( payment_method=PaymentMethods.card("4111111111111111", "122026", "123"), line_items=[LineItem("SKU-992", 1, Money.of("75.00", "USD"))], idempotency=Idempotency(xtl_order_id="INV-999"), partial_auth=PartialAuth(enabled=True, minimum_amount=Money.of("10.00", "USD")), )) if result.status is TransactionStatus.APPROVED: pass # result.raw["TRANS_VALUE"] may be less than 75.00 elif result.status is TransactionStatus.DECLINED: pass # card balance was below the minimum ``` **Java** ```java InovioClient client = new InovioClient( new InovioClient.Credentials("api_user", "P@ssw0rd!", "12345")); TransactionRequest req = new TransactionRequest( PaymentMethods.card("4111111111111111", "122026", "123"), new LineItem("SKU-992", 1, Money.of("75.00", "USD"))) .idempotency("INV-999"); RequestParts.PartialAuth partialAuth = new RequestParts.PartialAuth(true); partialAuth.minimumAmount = Money.of("10.00", "USD"); req.partialAuth = partialAuth; TransactionResult result = client.sale(req); switch (result.status()) { case APPROVED -> { /* result.raw().get("TRANS_VALUE") may be less than 75.00 */ } case DECLINED -> { /* card balance was below the minimum */ } default -> { /* PENDING | RUNNING | FAILED */ } } ``` **Response** ```json { "REQUEST_ACTION": "CCAUTHCAP", "REQ_ID": "68192033", "TRANS_STATUS_NAME": "APPROVED", "TRANS_VALUE": 32.50, "CURR_CODE_ALPHA": "USD", "TRANS_VALUE_SETTLED": 32.50, "CURR_CODE_ALPHA_SETTLED": "USD", "TRANS_EXCH_RATE": "", "TRANS_ID": 8839201112, "CUST_ID": 9928102, "XTL_CUST_ID": "cUsT992xP", "PO_ID": 77281920, "XTL_ORDER_ID": "INV-999", "BATCH_ID": 882910, "PROC_NAME": "Inovio Primary", "MERCH_ACCT_ID": 110203, "CARD_BRAND_NAME": "Mastercard", "CARD_TYPE": "MASTERCARD BLACK CARD", "CARD_CLASS": "Consumer Credit", "CARD_PREPAID": 0, "CARD_BANK": "CREDOMATIC INTERNATIONAL", "CARD_COUNTRY": "CRI", "CARD_DETAIL": "Credit", "CARD_BALANCE": "", "PMT_L4": "3535", "PMT_ID": 8829102, "PMT_ID_XTL": "", "PMT_AAU_UPDATE_DT": "", "PMT_AAU_UPDATE_DESC": "", "PROC_UDF01": "", "ANI_RESP_DECISION": "", "PROC_UDF02": "", "PROC_AUTH_RESPONSE": "AUTH99", "PROC_RETRIEVAL_NUM": "1029384A-B8C7-D6E5-F4G3-H2I1J0K9L8M7", "PROC_REFERENCE_NUM": "REF10293847", "PROC_REDIRECT_URL": "", "AVS_RESPONSE": "M", "CVV_RESPONSE": "M", "CARD_BRAND_TRANSID": "", "REQUEST_API_VERSION": "4.14", "P3DS_VENDOR": "", "P3DS_RESPONSE": "", "PO_LI_ID_1": "8829103", "PO_LI_COUNT_1": 1, "PO_LI_AMOUNT_1": "49.99", "PO_LI_PROD_ID_1": "SKU-992", "MBSHP_ID_1": "88291", "TRANS_NTOKEN_USED": 1 } ``` --- # Card on File (Recurring) Charge a previously saved payment method by Customer ID without resubmitting the card number. Source: https://developer.inoviopay.com/api/card-on-file.html Markdown: https://developer.inoviopay.com/api/card-on-file.md To charge a customer using a previously saved payment method, you do not need to submit the full credit card number again. Instead, you can make a call without the card number by using only the Customer ID (`CUST_ID`). This will use the most recent payment account or credit card that was used. The merchant may also specify which payment method to use by sending Customer ID (`CUST_ID`) paired with either the Payment ID (`PMT_ID`) or the last 4 digits of the card (`PMT_L4`). | Parameter | Description | |---|---| | `CUST_ID` (required) | The Customer ID generated from the original authorization. | | `PMT_ID / PMT_L4` (required) | Provide *either* the unique Payment ID (`PMT_ID`) or the last 4 digits of the card (`PMT_L4`) to specify which saved account to charge. | | `REQUEST_REBILL` (optional) | Set to `1` to explicitly flag the transaction as a rebill/renewal. Set to `2` for the first transaction in a subscription. | All four SDKs model a saved payment method as `SavedCard` (constructed with `pmtId` and/or `pmtIdXtl`, plus `custId`) and the `REQUEST_REBILL` / `REQUEST_INITATOR` compliance flags as a `Recurring` block on the request. `PMT_L4` card selection (the cURL example below) is not modeled as an SDK constructor; use `pmtId` from a prior result's `PMT_ID` instead. **cURL** ```bash curl -X POST "https://api.inoviopay.com/payment/pmt_service.cfm" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "request_action=CCAUTHCAP&req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&request_response_format=JSON&cust_id=987654&pmt_l4=1111&li_value_1=25.00&request_rebill=1" ``` **PHP** ```php use Inovio\Gateway\{Credentials, InovioClient}; use Inovio\Gateway\Model\{LineItem, Money, PaymentMethods, Recurring}; use Inovio\Gateway\Request\TransactionRequest; $client = new InovioClient(new Credentials('api_user', 'P@ssw0rd!', '12345'), 'SANDBOX'); $req = new TransactionRequest( PaymentMethods::savedCard(pmtId: '8839213', custId: '987654'), [new LineItem('111205', 1, Money::of('25.00', 'USD'))] ); $req->recurring = new Recurring(); $req->recurring->rebill = 'REBILL'; // REQUEST_REBILL=1 — a renewal, not the first charge $result = $client->sale($req); match ($result->status) { 'APPROVED' => /* funds captured against the saved card */, 'DECLINED' => /* $result->serviceClassification->stopRecurring — stop the dunning cycle? */, default => /* PENDING | RUNNING | FAILED */, }; ``` **Node** ```ts import { InovioClient, Money, PaymentMethods } from '@inovio/gateway-sdk'; const client = new InovioClient( { reqUsername: 'api_user', reqPassword: 'P@ssw0rd!', siteId: '12345' }, { environment: 'SANDBOX' } ); const result = await client.sale({ paymentMethod: PaymentMethods.savedCard({ pmtId: '8839213', custId: '987654' }), lineItems: [{ productId: '111205', count: 1, value: Money.of('25.00', 'USD') }], recurring: { rebill: 'REBILL' }, // REQUEST_REBILL=1 — a renewal, not the first charge }); switch (result.status) { case 'APPROVED': /* funds captured against the saved card */ break; case 'DECLINED': /* result.serviceClassification?.stopRecurring — stop the dunning cycle? */ break; default: /* PENDING | RUNNING | FAILED */ break; } ``` **Python** ```python from inovio_gateway import Credentials, InovioClient, Money, PaymentMethods, TransactionStatus from inovio_gateway.model import LineItem, Recurring from inovio_gateway.request import TransactionRequest client = InovioClient(Credentials("api_user", "P@ssw0rd!", site_id="12345"), environment="SANDBOX") result = client.sale(TransactionRequest( payment_method=PaymentMethods.saved_card(pmt_id="8839213", cust_id="987654"), line_items=[LineItem("111205", 1, Money.of("25.00", "USD"))], recurring=Recurring(rebill="REBILL"), # REQUEST_REBILL=1 — a renewal, not the first charge )) if result.status is TransactionStatus.APPROVED: pass # funds captured against the saved card elif result.status is TransactionStatus.DECLINED: pass # result.service_classification.stop_recurring — stop the dunning cycle? ``` **Java** ```java InovioClient client = new InovioClient( new InovioClient.Credentials("api_user", "P@ssw0rd!", "12345")); TransactionRequest req = new TransactionRequest( PaymentMethods.savedCard("8839213", null, "987654"), new LineItem("111205", 1, Money.of("25.00", "USD"))); RequestParts.Recurring recurring = new RequestParts.Recurring(); recurring.rebill = "REBILL"; // REQUEST_REBILL=1 — a renewal, not the first charge req.recurring = recurring; TransactionResult result = client.sale(req); switch (result.status()) { case APPROVED -> { /* funds captured against the saved card */ } case DECLINED -> { /* result.serviceClassification().stopRecurring() — stop the dunning cycle? */ } default -> { /* PENDING | RUNNING | FAILED */ } } ``` **Response** ```json { "REQUEST_ACTION": "CCAUTHCAP", "REQ_ID": "84729108", "TRANS_STATUS_NAME": "APPROVED", "TRANS_VALUE": 0, "CURR_CODE_ALPHA": "USD", "TRANS_VALUE_SETTLED": 0, "CURR_CODE_ALPHA_SETTLED": "USD", "TRANS_EXCH_RATE": "", "TRANS_ID": 3948572122, "CUST_ID": 9827166, "XTL_CUST_ID": "", "PO_ID": 99281741, "XTL_ORDER_ID": "", "BATCH_ID": 883928, "PROC_NAME": "Test Processor", "MERCH_ACCT_ID": 110203, "CARD_BRAND_NAME": "Visa", "CARD_TYPE": "", "CARD_CLASS": "", "CARD_PREPAID": 0, "CARD_BANK": "", "CARD_COUNTRY": "", "CARD_DETAIL": "", "CARD_BALANCE": "", "PMT_L4": "2345", "PMT_ID": 8839213, "PMT_ID_XTL": "", "PMT_AAU_UPDATE_DT": "", "PMT_AAU_UPDATE_DESC": "", "PROC_UDF01": "", "ANI_RESP_DECISION": "", "PROC_UDF02": "", "PROC_AUTH_RESPONSE": "TEST18263", "PROC_RETRIEVAL_NUM": "803EECE4-7F22-4837-841C615C7D1E59D4", "PROC_REFERENCE_NUM": "TEST469812895", "PROC_REDIRECT_URL": "", "AVS_RESPONSE": "M", "CVV_RESPONSE": "M", "CARD_BRAND_TRANSID": "", "REQUEST_API_VERSION": "4.14", "P3DS_VENDOR": "", "P3DS_RESPONSE": "", "PO_LI_ID_1": "9928185", "PO_LI_COUNT_1": 1, "PO_LI_AMOUNT_1": "0", "PO_LI_PROD_ID_1": "111205", "MBSHP_ID_1": "", "TRANS_NTOKEN_USED": 0 } ``` --- # Multiple Line Items Submit purchase orders with more than one line item using the indexed LI_ parameters. Source: https://developer.inoviopay.com/api/line-items.html Markdown: https://developer.inoviopay.com/api/line-items.md The Payment Service supports purchase orders with more than one line item. Use the Line Item Parameters: `LI_PROD_ID_X`, `LI_COUNT_X`, and `LI_VALUE_X`. The "X" indicates a dynamic number depending on how many line items you want to send. | Parameter | Description | |---|---| | `LI_PROD_ID_X` (required) | Product ID for the line item. Replace 'X' with an incrementing integer. | | `LI_COUNT_X` (required) | Quantity for the line item. Max value is 99. | | `LI_VALUE_X` (required) | Transaction Amount for the line item. | Capturing a single line item later (partial shipment, split fulfillment) uses `captureLineItem(order, lineItemRef, amount)` in all four SDKs, taking a `LineItemRef` from `result.lineItemRefs` (or `result.line_item_refs` in Python) rather than a raw `PO_LI_ID` string. **cURL** ```bash curl -X POST "https://api.inoviopay.com/payment/pmt_service.cfm" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "request_action=CCAUTHORIZE&req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&request_response_format=JSON&pmt_numb=4111111111111111&pmt_expiry=122026&pmt_key=123&request_currency=USD&li_prod_id_1=1001&li_value_1=19.95&li_prod_id_2=2001&li_value_2=10.50" ``` **PHP** ```php use Inovio\Gateway\{Credentials, InovioClient}; use Inovio\Gateway\Model\{LineItem, Money, PaymentMethods}; use Inovio\Gateway\Request\TransactionRequest; $client = new InovioClient(new Credentials('api_user', 'P@ssw0rd!', '12345'), 'SANDBOX'); $req = new TransactionRequest( PaymentMethods::card('4111111111111111', '122026', '123'), [ new LineItem('1001', 1, Money::of('19.95', 'USD')), new LineItem('2001', 1, Money::of('10.50', 'USD')), ] ); $result = $client->authorize($req); match ($result->status) { 'APPROVED' => /* $result->lineItemRefs — one LineItemRef per LI_, for later captureLineItem() */, 'DECLINED' => /* $result->outcome->service */, default => /* PENDING | RUNNING | FAILED */, }; ``` **Node** ```ts import { InovioClient, Money, PaymentMethods } from '@inovio/gateway-sdk'; const client = new InovioClient( { reqUsername: 'api_user', reqPassword: 'P@ssw0rd!', siteId: '12345' }, { environment: 'SANDBOX' } ); const result = await client.authorize({ paymentMethod: PaymentMethods.card('4111111111111111', '122026', '123'), lineItems: [ { productId: '1001', count: 1, value: Money.of('19.95', 'USD') }, { productId: '2001', count: 1, value: Money.of('10.50', 'USD') }, ], }); switch (result.status) { case 'APPROVED': /* result.lineItemRefs — one LineItemRef per LI_ */ break; case 'DECLINED': /* result.outcome.service */ break; default: /* PENDING | RUNNING | FAILED */ break; } ``` **Python** ```python from inovio_gateway import Credentials, InovioClient, Money, PaymentMethods, TransactionStatus from inovio_gateway.model import LineItem from inovio_gateway.request import TransactionRequest client = InovioClient(Credentials("api_user", "P@ssw0rd!", site_id="12345"), environment="SANDBOX") result = client.authorize(TransactionRequest( payment_method=PaymentMethods.card("4111111111111111", "122026", "123"), line_items=[ LineItem("1001", 1, Money.of("19.95", "USD")), LineItem("2001", 1, Money.of("10.50", "USD")), ], )) if result.status is TransactionStatus.APPROVED: pass # result.line_item_refs — one LineItemRef per LI_, for later capture_line_item() elif result.status is TransactionStatus.DECLINED: pass # result.outcome.service ``` **Java** ```java InovioClient client = new InovioClient( new InovioClient.Credentials("api_user", "P@ssw0rd!", "12345")); TransactionRequest req = new TransactionRequest( PaymentMethods.card("4111111111111111", "122026", "123"), new LineItem("1001", 1, Money.of("19.95", "USD")), new LineItem("2001", 1, Money.of("10.50", "USD"))); TransactionResult result = client.authorize(req); switch (result.status()) { case APPROVED -> { /* result.lineItemRefs() — one LineItemRef per LI_ */ } case DECLINED -> { /* result.outcome().service() */ } default -> { /* PENDING | RUNNING | FAILED */ } } ``` **Response** ```json { "REQUEST_ACTION": "CCAUTHORIZE", "TRANS_STATUS_NAME": "APPROVED", "CURR_CODE_ALPHA": "USD", "TRANS_VALUE": 30.45, "TRANS_VALUE_SETTLED": 30.45, "CURR_CODE_ALPHA_SETTLED": "USD", "TRANS_EXCH_RATE": "", "TRANS_ID": 3948572123, "CUST_ID": 9827167, "XTL_CUST_ID": "", "PO_ID": 99281742, "XTL_ORDER_ID": "", "BATCH_ID": 883929, "PROC_NAME": "Test Processor", "MERCH_ACCT_ID": 100, "CARD_BRAND_NAME": "Visa", "CARD_DETAIL": "CREDIT", "CARD_TYPE": "VISA BUSINESS", "CARD_CLASS": "CONSUMER CREDIT", "CARD_COUNTRY": "CRI", "CARD_PREPAID": 0, "CARD_BANK": "", "CARD_BALANCE": "", "PMT_L4": "1111", "PMT_ID": "", "PMT_ID_XTL": "", "PROC_UDF01": "", "PROC_UDF02": "", "PROC_AUTH_RESPONSE": "AUTH514", "PROC_RETRIEVAL_NUM": "7169CEF3-64EF-46BB-55A333956994D2D", "PROC_REFERENCE_NUM": "", "REQ_ID": "84729109", "AVS_RESPONSE": "M", "CVV_RESPONSE": "M", "REQUEST_API_VERSION": "4.14", "PO_LI_ID_1": "9928186", "PO_LI_COUNT_1": 1, "PO_LI_AMOUNT_1": 19.95, "PO_LI_PROD_ID_1": "1001", "PO_LI_ID_2": "9928187", "PO_LI_COUNT_2": 1, "PO_LI_AMOUNT_2": 10.5, "PO_LI_PROD_ID_2": "2001", "MBSHP_ID_1": "" } ``` --- # Memberships & Subscriptions Create, update, and cancel subscription-based products through the API. Source: https://developer.inoviopay.com/api/memberships.html Markdown: https://developer.inoviopay.com/api/memberships.md The gateway provides full lifecycle management for subscription-based products. Subscriptions can be defined either in the portal or directly through the API during the initial authorization. ### Direct Creation Parameters | Parameter | Description | |---|---| | `PROD_NAME` | Name for the new subscription product. | | `PROD_TYPE` | 1 = Cancels after period; 2 = Auto-renews. | | `PROD_REBILL_METRIC` | Interval type: M (Month), D (Day), Y (Year). | | `PROD_REBILL_PERIOD` | Numeric interval (e.g., 30 for monthly). | ### Management Actions (Cancel & Update) Use the following actions to manage an existing membership record. | Parameter | Description | |---|---| | `REQUEST_ACTION` (required) | Must be set to `SUB_CANCEL` or `SUB_UPDATE`. | | `REQUEST_REF_MBSHP_ID` (required) | The existing Membership ID you wish to modify. | | `SUB_CANCEL_TYPE` (optional) | Used with `SUB_CANCEL`. Set to `1` to cancel immediately, or `2` to cancel on the next scheduled rebill date. | | `SUB_UPDATE_PROD_ID` (optional) | Used with `SUB_UPDATE` to change the current product ID of the membership record. | | `SUB_UPDATE_PMT_ID` (optional) | Used with `SUB_UPDATE` to change the credit card/payment method tied to the specific membership. | ### Handling the Response When updating a membership, ensure the `SERVICE_RESPONSE` indicates success (e.g., `102` for "Membership Updated") and check the `MBSHP_REBILL_TS_UTC` to confirm the next billing cycle date. | Field Name | Description | |---|---| | `API_RESPONSE / SERVICE_RESPONSE` | Check these values for `0` or `102` to verify the request was successfully processed. | | `MBSHP_ID` | The unique Membership ID that was modified or cancelled. | | `MBSHP_REBILL_TS_UTC` | The UTC timestamp of when the next billing cycle will occur. | | `MBSHP_CANCEL_TS_UTC` | The UTC timestamp of when the cancellation is scheduled to take effect (if applicable). | | `CUST_ID` | The Customer ID tied to the membership. | ### Update membership example > **Not in the SDKs yet** > Membership/subscription management is not implemented in any of the four SDKs. `SUB_UPDATE` and `SUB_CANCEL` exist only as labels in the generated `RequestAction` enum; no client method builds either request, and none of `sale()`/`authorize()`/`capture()` accept an arbitrary `REQUEST_ACTION` override. Use the cURL example directly, or see [the SDKs](https://developer.inoviopay.com/sdks/index.md). ### Cancel membership example > **Not in the SDKs yet** > Same as above: no SDK builds a `SUB_CANCEL` request. See [the SDKs](https://developer.inoviopay.com/sdks/index.md). **cURL** ```bash curl -X POST "https://api.inoviopay.com/payment/pmt_service.cfm" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "request_action=SUB_UPDATE&req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&request_response_format=JSON&request_ref_mbshp_id=83262&sub_update_prod_id=99281" ``` **Response** ```json { "REQUEST_ACTION": "SUB_UPDATE", "API_RESPONSE": "0", "API_ADVICE": "", "SERVICE_RESPONSE": 102, "SERVICE_ADVICE": "Membership Updated", "REF_FIELD": "", "CUST_ID": 8076911, "MBSHP_ID": 83262, "MBSHP_CANCEL_TS_UTC": "", "MBSHP_REBILL_TS_UTC": "March, 25 2026 19:34:39" } ``` **cURL** ```bash curl -X POST "https://api.inoviopay.com/payment/pmt_service.cfm" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "request_action=SUB_CANCEL&req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&request_response_format=JSON&request_ref_mbshp_id=92831&sub_cancel_type=2" ``` **Response** ```json { "REQUEST_ACTION": "SUB_CANCEL", "API_RESPONSE": "0", "API_ADVICE": "", "SERVICE_RESPONSE": 0, "SERVICE_ADVICE": "", "REF_FIELD": "", "CUST_ID": 7283921, "MBSHP_ID": 92831, "MBSHP_CANCEL_TS_UTC": "April, 30 2026 15:30:35", "MBSHP_REBILL_TS_UTC": "" } ``` --- # Fraud Mitigation Control AVS and CVV checks per transaction, overriding the portal-level defaults. Source: https://developer.inoviopay.com/api/fraud-mitigation.html Markdown: https://developer.inoviopay.com/api/fraud-mitigation.md Inovio provides built-in validation for Address Verification Service (AVS) and Card Security Code (CVV). By default, the gateway performs these checks but ignores the results unless specific flags are enabled. This can be maintained in the Inovio Portal settings. If you want to manually control AVS and CVV settings or override the portal settings for specific transactions you can send the following parameters. ### Address Verification Service (AVS) AVS verifies the billing address provided by the customer against the address on file at the issuing bank. It is generally supported in the US and several international markets. | Parameter | Setting | Description | |---|---|---| | `CHKAVS` (optional) | `T` | **Enabled:** Transaction is approved only if the response matches the [AVSMatchSet](https://developer.inoviopay.com/reference/avs-codes.md). | | `CHKAVS` (optional) | `F` | **Disabled:** Do not perform AVS check. | | `CHKAVS` (optional) | `C` | **Conditional:** Check AVS, but ignore results if the CVV check is a match. | | `AVSMATCHSET` (optional) | `[Codes]` | A custom string of AVS codes to accept (e.g., `ADRSTUVWZ`). If the bank returns a code not in this set, the transaction is declined. | ### Card Security Code (CVV) The Card Security Code (CVV2/CVC2) is the 3 or 4-digit number on the card. This must be submitted in the `PMT_KEY` parameter. | Parameter | Setting | Description | |---|---|---| | `CHKCVV` (optional) | `T` | **Enabled:** Transaction is approved only if the response matches the [CVVMATCHSET](https://developer.inoviopay.com/reference/cvv-codes.md). | | `CHKCVV` (optional) | `F` | **Disabled:** Do not send CVV to the bank. | | `CHKCVV` (optional) | `C` | **Conditional:** Check CVV, but ignore results if the AVS check is a match. | | `CVVMATCHSET` (optional) | `[Codes]` | A custom string of CVV codes to accept (e.g., `MPSX`). Default positive response is `M` (Match). | AVS and CVV enforcement is layered onto whatever request action you're already sending (`CCAUTHORIZE`, `CCAUTHCAP`), it does not require a separate SDK call, only additional parameters on the existing sale/authorize request. All four SDKs model these as a `RiskOptions` block (`avs` / `cvv`, values `on` / `off` / `ignore` / `conditional`) on the transaction request, and expose the response as `avs.classification` / `cvv.classification` (`positive` / `partial` / `negative` / `neutral`, derived by the SDK from `AVS_RESPONSE` / `CVV_RESPONSE`), not just the raw letter code. > **The SDKs currently send the wrong CHKAVS / CHKCVV values** > The gateway compares `CHKAVS` and `CHKCVV` against the letter codes in the table above (`T`, `F`, `C`, default `I`); this is verified against the gateway's order processing. All four SDKs instead encode `RiskOptions.avs` / `.cvv` as `1` (on), `0` (off), `2` (ignore) and `3` (conditional), which the gateway does not recognise, so setting `RiskOptions` through an SDK today has no effect and the account's default AVS/CVV policy applies. Until the SDKs are corrected, enforce AVS and CVV by calling the API directly with the letter codes, as in the cURL tab. **cURL** ```bash curl -X POST "https://api.inoviopay.com/payment/pmt_service.cfm" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "request_action=CCAUTHCAP&req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&request_response_format=JSON&pmt_numb=4111111111111111&pmt_expiry=122026&pmt_key=123&request_currency=USD&li_value_1=49.99&xtl_order_id=INV-999&CHKAVS=T&CHKCVV=T" ``` **PHP** ```php use Inovio\Gateway\{Credentials, InovioClient}; use Inovio\Gateway\Model\{LineItem, Money, PaymentMethods, RiskOptions}; use Inovio\Gateway\Request\TransactionRequest; $client = new InovioClient(new Credentials('api_user', 'P@ssw0rd!', '12345'), 'SANDBOX'); $req = (new TransactionRequest( PaymentMethods::card('4111111111111111', '122026', '123'), [new LineItem('111205', 1, Money::of('49.99', 'USD'))] ))->withIdempotency('INV-999'); $req->risk = new RiskOptions(); $req->risk->avs = 'on'; $req->risk->cvv = 'on'; $result = $client->sale($req); if ($result->avs !== null) { echo $result->avs->classification; // positive | partial | negative | neutral } if ($result->cvv !== null) { echo $result->cvv->classification; } ``` **Node** ```ts import { InovioClient, Money, PaymentMethods } from '@inovio/gateway-sdk'; const client = new InovioClient( { reqUsername: 'api_user', reqPassword: 'P@ssw0rd!', siteId: '12345' }, { environment: 'SANDBOX' } ); const result = await client.sale({ paymentMethod: PaymentMethods.card('4111111111111111', '122026', '123'), lineItems: [{ productId: '111205', count: 1, value: Money.of('49.99', 'USD') }], idempotency: { xtlOrderId: 'INV-999' }, risk: { avs: 'on', cvv: 'on' }, }); console.log(result.avs?.classification); // positive | partial | negative | neutral console.log(result.cvv?.classification); ``` **Python** ```python from inovio_gateway import Credentials, InovioClient, Money, PaymentMethods from inovio_gateway.model import Idempotency, LineItem, RiskOptions from inovio_gateway.request import TransactionRequest client = InovioClient(Credentials("api_user", "P@ssw0rd!", site_id="12345"), environment="SANDBOX") result = client.sale(TransactionRequest( payment_method=PaymentMethods.card("4111111111111111", "122026", "123"), line_items=[LineItem("111205", 1, Money.of("49.99", "USD"))], idempotency=Idempotency(xtl_order_id="INV-999"), risk=RiskOptions(avs="on", cvv="on"), )) if result.avs: print(result.avs.classification) # positive | partial | negative | neutral if result.cvv: print(result.cvv.classification) ``` **Java** ```java InovioClient client = new InovioClient( new InovioClient.Credentials("api_user", "P@ssw0rd!", "12345")); TransactionRequest req = new TransactionRequest( PaymentMethods.card("4111111111111111", "122026", "123"), new LineItem("111205", 1, Money.of("49.99", "USD"))) .idempotency("INV-999"); RequestParts.RiskOptions risk = new RequestParts.RiskOptions(); risk.avs = "on"; risk.cvv = "on"; req.risk = risk; TransactionResult result = client.sale(req); if (result.avs() != null) { System.out.println(result.avs().classification()); // positive | partial | negative | neutral } if (result.cvv() != null) { System.out.println(result.cvv().classification()); } ``` **Response** ```json { "REQUEST_ACTION": "CCAUTHCAP", "REQ_ID": "84729110", "TRANS_STATUS_NAME": "APPROVED", "TRANS_VALUE": 3.15, "CURR_CODE_ALPHA": "USD", "TRANS_VALUE_SETTLED": 3.15, "CURR_CODE_ALPHA_SETTLED": "USD", "TRANS_EXCH_RATE": "", "TRANS_ID": 3948572124, "CUST_ID": 9827168, "XTL_CUST_ID": "xT883mP", "PO_ID": 99281743, "XTL_ORDER_ID": "", "BATCH_ID": 883930, "PROC_NAME": "Test Processor", "MERCH_ACCT_ID": 110203, "CARD_BRAND_NAME": "Visa", "CARD_TYPE": "VISA CLASSIC", "CARD_PREPAID": 1, "CARD_BANK": "BOFI FEDERAL BANK", "CARD_BALANCE": "", "PMT_L4": "2921", "PMT_ID": 8839214, "PMT_ID_XTL": "", "PROC_UDF01": "", "PROC_UDF02": "", "PROC_AUTH_RESPONSE": "TEST458", "PROC_RETRIEVAL_NUM": "930C79ED-E6D2-428B-A63EF36A9D769685", "PROC_REFERENCE_NUM": "TEST938316620", "PROC_REDIRECT_URL": "", "AVS_RESPONSE": "M", "CVV_RESPONSE": "M", "REQUEST_API_VERSION": "4.14", "P3DS_RESPONSE": "", "P3DS_VENDOR": "", "PO_LI_ID_1": "9928188", "PO_LI_COUNT_1": 1, "PO_LI_AMOUNT_1": "3.15", "PO_LI_PROD_ID_1": "111205", "MBSHP_ID_1": "" } ``` --- # Order Insight (Verifi) Share transaction details with issuers in real time to resolve disputes and prevent chargebacks before they're filed. Source: https://developer.inoviopay.com/api/order-insight.html Markdown: https://developer.inoviopay.com/api/order-insight.md > **Not in the SDKs yet** > Order Insight is not implemented in any of the four SDKs. `XTL_CUST_ID`, `PROD_DESC_XTL`, `DEVICE_ID_XTL` and `DEVICE_FINGERPRINT_XTL` are not modeled on `Customer` or `Metadata` in any language SDK, so these fields must be sent as raw form parameters alongside an SDK-built request, or over plain HTTP as shown below. See [the SDKs](https://developer.inoviopay.com/sdks/index.md). Order Insight is a real-time platform that allows merchants to share transaction details with issuers to resolve disputes and prevent chargebacks before they are formally filed. By providing granular data, you can prove transaction legitimacy through Compelling Evidence (CE). ### How it Works When a customer initiates a dispute, Inovio automatically shares the transactional data you provided at the time of purchase with the issuer. This process can deflect potential chargebacks by clarifying the purchase for the cardholder and bank. | Field Name | Description | |---|---| | XTL_CUST_ID | Merchant's internal Customer ID (Max 24 chars). | | PROD_DESC_XTL | Detailed description of the merchandise or service (Up to 1,000 chars). | | DEVICE_ID_XTL | The IMEI or MEID of the device used. Data must not be hashed. | | DEVICE_FINGERPRINT_XTL | Unique fingerprint generated by your system to identify the device. | > **Data Retention Requirements** > Standard Order Insight requires a 120-day history of product parameters. To utilize Compelling Evidence (CE), you must maintain a 365-day history of customer and device parameters. ### Order Insight statuses | Status | Meaning | |---|---| | Responded | Inovio successfully provided data to Verifi. | | Deflection | The lookup prevented a chargeback. | | Negation | The lookup resulted in a chargeback. | | Failed | Inovio did not receive the request within SLA. | | Reversal | A previously deflected dispute was reversed. | --- # Timeout Void Cap how long the gateway waits on a processor response and auto-void ghost approvals that arrive after the timeout. Source: https://developer.inoviopay.com/api/timeout-void.html Markdown: https://developer.inoviopay.com/api/timeout-void.md The Timeout Void feature allows you to set a maximum duration for the gateway to wait for a processor's response on `CCAUTHORIZE` or `CCAUTHCAP` requests. If the gateway does not receive an end-state response within your specified window, it will return a decline to your system. ### Automated Resolution Because the transaction may still be in-flight with the processor when the gateway times out, Inovio protects you from "ghost" approvals. If the processor eventually approves the transaction after the timeout period, the gateway will automatically submit a void request to ensure the cardholder is not charged for an abandoned transaction. | Field Name | Description | Value Guideline | |---|---|---| | REQUEST_MAX_WAIT | Enables or disables the timeout functionality. | `1` (Enable) or `0` (Disable). | | REQUEST_MAX_WAIT_TIMER | The number of seconds to wait before abandoning the request. | Required if enabled. Must be between `30` and `600` seconds. | > **Global Portal Setting** > Merchants can also set a global Timeout Void preference at the account level within the Inovio Portal. Any parameters sent within an individual API request will override the global portal settings. **Request** ``` // Set a strict 60-second limit for this sale REQUEST_ACTION=CCAUTHCAP &REQUEST_MAX_WAIT=1 &REQUEST_MAX_WAIT_TIMER=60 &LI_VALUE_1=100.00... ``` **PHP** ```php use Inovio\Gateway\{Credentials, InovioClient}; use Inovio\Gateway\Errors\GatewayTimeoutException; use Inovio\Gateway\Model\{LineItem, Money, PaymentMethods}; use Inovio\Gateway\Refs\Refs; use Inovio\Gateway\Request\TransactionRequest; $client = new InovioClient(new Credentials('api_user', 'P@ssw0rd!', '12345'), 'SANDBOX'); $req = (new TransactionRequest( PaymentMethods::card('4111111111111111', '122026', '123'), [new LineItem('SKU-1', 1, Money::of('100.00', 'USD'))] ))->withIdempotency('ORDER-555'); try { $client->sale($req); } catch (GatewayTimeoutException $e) { // The state is UNKNOWN, not failed — resolve it, don't retry blindly. $actual = $client->status(Refs::xtlOrder($e->xtlOrderId())); echo $actual->transactions === [] ? 'safe to retry' : 'already happened', "\n"; } ``` **Node** ```ts import { InovioClient, Money, PaymentMethods, Refs, TimeoutError } from '@inovio/gateway-sdk'; const client = new InovioClient( { reqUsername: 'api_user', reqPassword: 'P@ssw0rd!', siteId: '12345' }, { environment: 'SANDBOX' } ); try { await client.sale({ paymentMethod: PaymentMethods.card('4111111111111111', '122026', '123'), lineItems: [{ productId: 'SKU-1', count: 1, value: Money.of('100.00', 'USD') }], idempotency: { xtlOrderId: 'ORDER-555' }, }); } catch (e) { if (!(e instanceof TimeoutError)) throw e; // The state is UNKNOWN, not failed — resolve it, don't retry blindly. const actual = await client.status(Refs.xtlOrder(e.xtlOrderId!)); console.log(actual.transactions.length === 0 ? 'safe to retry' : 'already happened'); } ``` **Python** ```python from inovio_gateway import Credentials, InovioClient, InovioTimeoutError, Money, PaymentMethods, Refs from inovio_gateway.model import Idempotency, LineItem from inovio_gateway.request import TransactionRequest client = InovioClient(Credentials("api_user", "P@ssw0rd!", "12345"), environment="SANDBOX") req = TransactionRequest( payment_method=PaymentMethods.card("4111111111111111", "122026", "123"), line_items=[LineItem("SKU-1", 1, Money.of("100.00", "USD"))], idempotency=Idempotency(xtl_order_id="ORDER-555"), ) try: client.sale(req) except InovioTimeoutError as e: # The state is UNKNOWN, not failed — resolve it, don't retry blindly. actual = client.status(Refs.xtl_order(e.xtl_order_id)) print("safe to retry" if not actual.transactions else "already happened") ``` **Java** ```java import com.inoviopay.gateway.errors.GatewayTimeoutException; import com.inoviopay.gateway.refs.Refs; import com.inoviopay.gateway.result.OrderStatus; TransactionRequest req = new TransactionRequest( PaymentMethods.card("4111111111111111", "122026", "123"), new LineItem("SKU-1", 1, Money.of("100.00", "USD"))) .idempotency("ORDER-555"); try { client.sale(req); } catch (GatewayTimeoutException e) { // The state is UNKNOWN, not failed — resolve it, don't retry blindly. OrderStatus actual = client.status(Refs.xtlOrder(e.xtlOrderId())); System.out.println(actual.transactions().isEmpty() ? "safe to retry" : "already happened"); } ``` **Response** ```json { "TRANS_STATUS_NAME": "DECLINED", "SERVICE_RESPONSE": "626", "SERVICE_ADVICE": "Authorization has been voided in accordance to timeout settings." } ``` --- # 3D Secure: Inovio Gateway The 5-step server-and-browser workflow for running 3D Secure 2.0 through the Inovio Gateway's device data collection and challenge flow. Source: https://developer.inoviopay.com/api/3ds-gateway.html Markdown: https://developer.inoviopay.com/api/3ds-gateway.md > **SDK coverage of this flow** > The PHP SDK owns every server leg of this flow: `$client->threeDSecure()->prepare()` (Step 1), the `ThreeDS` block on a normal `sale()`/`authorize()` (Step 3), and `completeSale()`/`completeAuthorize()` (Step 5). Node, Python and Java carry only `BrowserData` on the request (Step 3) and read the challenge outcome from `result.nextAction` (`kind: 'threeDSChallenge'`); none of the three expose a `prepare()`/DDC call or a completion-leg method, so Steps 1 and 5 have no SDK path in those three languages today. If you need the full gateway-managed flow outside PHP, drive the endpoints directly as shown in the cURL examples below, or see [the SDKs](https://developer.inoviopay.com/sdks/index.md). This flow allows the Inovio Gateway to manage the 3D Secure 2.0 process. A successful 3DS implementation requires a strict sequence of events spanning your backend servers and the customer's browser. Below is the complete 5-step workflow. ### Step 1: Request Device Data Collection (DDC) Parameters Before you can authorize the card, you must collect device profile data. Make a server-to-server request to the 3DS endpoint (`https://api.inoviopay.com/payment/3dsrequest.cfm`) to get your session parameters. **Required Parameters:** `REQ_USERNAME`, `REQ_PASSWORD`, `MERCH_ACCT_ID`, and `PMT_BIN` (the first 6 digits of the customer's card), along with your standard billing and transaction details. The response will contain a `JWT`, a `DDC_URL`, and a `DDC_REFERENCEID`. Store the Reference ID on your server; you will need it in Step 3. ### Step 2: Execute DDC on the Client Side Device profiling **must** happen on the customer's device. Using the response from Step 1, create a hidden HTML form on your checkout page containing the `JWT` as an input field, and POST it targeting a hidden iframe to the `DDC_URL`. > **Client-Side Execution Required** > Do not attempt to POST the JWT to the DDC URL from your backend server. The provider must read the browser's user-agent and device footprint directly to satisfy the issuer's risk algorithms. ```html
``` ### Step 3: Initial Enrollment Check Once the DDC iframe has executed (typically wait 1-2 seconds or listen for the provider's javascript `postMessage`), submit your standard transaction request (e.g., `CCAUTHCAP` or `CCAUTHORIZE`) to the primary gateway endpoint. You must include two additional parameters in this authorization call: - `REQUEST_ENROLLMENT=1` (Flags this as a 3DS check) - `DDC_REFERENCEID` (The exact ID returned in Step 1) - `P3DS_RETURN_URL` (Where the customer should be sent after the challenge) If the issuer determines the transaction is low-risk, they may grant a frictionless flow, returning a standard `APPROVED` response. If a challenge is required, the gateway will return a `PENDING` status along with the ACS URL and challenge payload parameters. ### Step 4: Presenting the Challenge (If Required) If you receive a `PENDING` status, the cardholder's bank requires them to complete an authentication challenge (like entering an SMS code). The response will contain an `P3DS_ACS_URL` and a `P3DS_PAYLOAD`. To display this challenge to the user without them leaving your checkout flow: 1. Create a visible `
``` ### Step 5: Final Authorization After the customer completes the challenge and the issuer redirects back to your server, you must submit the final call to Inovio to complete the transaction. Send a standard transaction request (matching the call you made in Step 3) but include the authentication results: - `P3DS_PROCTRANSID`: The Transaction ID returned from the ACS. - `REQUEST_PARES`: The authentication response payload from the ACS. **cURL** ```bash curl -X POST "https://api.inoviopay.com/payment/3dsrequest.cfm" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&merch_acct_id=110203&pmt_bin=411111" ``` **PHP** ```php use Inovio\Gateway\{Credentials, InovioClient, ThreeDSPrepare}; $client = new InovioClient( new Credentials('api_user', 'P@ssw0rd!', '12345', merchAcctId: '110203'), 'SANDBOX' ); $ddc = $client->threeDSecure()->prepare( ThreeDSPrepare::bin('411111', 'USD', 'US') ); // $ddc->jwt, $ddc->ddcUrl — POST jwt to ddcUrl in a hidden iframe (Step 2). // $ddc->ddcReferenceId — store it; you need it for the ThreeDS block in Step 3. ``` **Response** ```json { "JWT": "[opaque JWT value]", "DDC_URL": "https://centinelapistag.cardinalcommerce.com/V1/Cruise/Collect", "DDC_REFERENCEID": "8839201113" } ``` **cURL** ```bash curl -X POST "https://api.inoviopay.com/payment/pmt_service.cfm" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "request_action=CCAUTHCAP&req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&request_response_format=JSON&pmt_numb=4111111111111111&pmt_expiry=122026&pmt_key=123&request_currency=USD&li_value_1=49.99&xtl_order_id=INV-999&request_enrollment=1&ddc_referenceid=[DDC_REFERENCEID_FROM_STEP_1]&p3ds_return_url=https://yourshop.example.com/3ds-return" ``` **PHP** ```php use Inovio\Gateway\Model\{BrowserData, LineItem, Money, PaymentMethods, ThreeDS}; use Inovio\Gateway\Request\TransactionRequest; $req = (new TransactionRequest( PaymentMethods::card('4111111111111111', '122026', '123'), [new LineItem('SKU-992', 1, Money::of('49.99', 'USD'))] ))->withIdempotency('INV-999'); // BrowserData is required — the gateway silently skips 3DS without it. $req->browser = new BrowserData( language: 'en-US', userAgent: $_SERVER['HTTP_USER_AGENT'], header: $_SERVER['HTTP_ACCEPT'] ); $req->threeDS = new ThreeDS($ddc->ddcReferenceId, 'https://yourshop.example.com/3ds-return'); $result = $client->sale($req); match ($result->status) { 'APPROVED', 'DECLINED' => /* frictionless — check $result->threeDS->eci */, 'PENDING' => /* $result->nextAction->jwt / ->redirectUrl / ->procTransId — Step 4 challenge */, default => /* RUNNING | FAILED */, }; ``` **Node** ```ts const result = await client.sale({ paymentMethod: PaymentMethods.card('4111111111111111', '122026', '123'), lineItems: [{ productId: 'SKU-992', count: 1, value: Money.of('49.99', 'USD') }], idempotency: { xtlOrderId: 'INV-999' }, // BrowserData is required — the gateway silently skips 3DS without it. browser: { language: 'en-US', userAgent: req.headers['user-agent']!, header: req.headers['accept']! }, }); if (result.status === 'PENDING' && result.nextAction?.kind === 'threeDSChallenge') { // result.nextAction.jwt / .redirectUrl / .procTransId — Step 4 challenge. // Node has no prepare()/completeSale() — drive Steps 1 and 5 over HTTP directly. } ``` **Python** ```python result = client.sale(TransactionRequest( payment_method=PaymentMethods.card("4111111111111111", "122026", "123"), line_items=[LineItem("SKU-992", 1, Money.of("49.99", "USD"))], idempotency=Idempotency(xtl_order_id="INV-999"), # BrowserData is required — the gateway silently skips 3DS without it. browser=BrowserData(language="en-US", user_agent=user_agent, header=accept_header), )) if result.status is TransactionStatus.PENDING and result.next_action and result.next_action.kind == "threeDSChallenge": pass # result.next_action.jwt / .redirect_url / .proc_trans_id — Step 4 challenge. # Python has no prepare()/complete_sale() — drive Steps 1 and 5 over HTTP directly. ``` **Java** ```java TransactionRequest req = new TransactionRequest( PaymentMethods.card("4111111111111111", "122026", "123"), new LineItem("SKU-992", 1, Money.of("49.99", "USD"))) .idempotency("INV-999"); // BrowserData is required — the gateway silently skips 3DS without it. req.browser = new RequestParts.BrowserData("en-US", userAgent, acceptHeader); TransactionResult result = client.sale(req); if (result.status() == TransactionStatus.PENDING && result.nextAction() != null && "threeDSChallenge".equals(result.nextAction().kind())) { // result.nextAction().jwt / .redirectUrl / .procTransId — Step 4 challenge. // Java has no prepare()/completeSale() — drive Steps 1 and 5 over HTTP directly. } ``` **Response** ```json { "REQUEST_ACTION": "CCAUTHCAP", "REQ_ID": "68192033", "TRANS_STATUS_NAME": "PENDING", "TRANS_VALUE": 49.99, "CURR_CODE_ALPHA": "USD", "TRANS_VALUE_SETTLED": 49.99, "CURR_CODE_ALPHA_SETTLED": "USD", "TRANS_EXCH_RATE": "", "TRANS_ID": 8839201112, "CUST_ID": 9928102, "XTL_CUST_ID": "cUsT992xP", "PO_ID": 77281920, "XTL_ORDER_ID": "INV-999", "BATCH_ID": 882910, "PROC_NAME": "Inovio Primary", "MERCH_ACCT_ID": 110203, "CARD_BRAND_NAME": "Mastercard", "CARD_TYPE": "MASTERCARD BLACK CARD", "CARD_CLASS": "Consumer Credit", "CARD_PREPAID": 0, "CARD_BANK": "CREDOMATIC INTERNATIONAL", "CARD_COUNTRY": "CRI", "CARD_DETAIL": "Credit", "CARD_BALANCE": "", "PMT_L4": "3535", "PMT_ID": 8829102, "PMT_ID_XTL": "", "PMT_AAU_UPDATE_DT": "", "PMT_AAU_UPDATE_DESC": "", "PROC_UDF01": "", "ANI_RESP_DECISION": "", "PROC_UDF02": "", "PROC_AUTH_RESPONSE": "", "PROC_RETRIEVAL_NUM": "", "PROC_REFERENCE_NUM": "", "PROC_REDIRECT_URL": "", "AVS_RESPONSE": "", "CVV_RESPONSE": "", "CARD_BRAND_TRANSID": "", "REQUEST_API_VERSION": "4.14", "P3DS_VENDOR": "", "P3DS_RESPONSE": "", "P3DS_ACS_URL": "https://acs.issuer-example.com/acs/challenge", "P3DS_PAYLOAD": "[opaque challenge payload]", "PO_LI_ID_1": "8829103", "PO_LI_COUNT_1": 1, "PO_LI_AMOUNT_1": "49.99", "PO_LI_PROD_ID_1": "SKU-992", "MBSHP_ID_1": "88291", "TRANS_NTOKEN_USED": 1 } ``` **cURL** ```bash curl -X POST "https://api.inoviopay.com/payment/pmt_service.cfm" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "request_action=CCAUTHCAP&req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&request_response_format=JSON&pmt_numb=4111111111111111&pmt_expiry=122026&pmt_key=123&request_currency=USD&li_value_1=49.99&xtl_order_id=INV-999&p3ds_proctransid=[FROM_ACS]&request_pares=[FROM_ACS]" ``` **PHP** ```php use Inovio\Gateway\Model\ThreeDSChallengeResult; // Reuse the SAME $req that ran the Step 3 enrollment leg. $final = $client->threeDSecure()->completeSale( $req, new ThreeDSChallengeResult($_POST['TransactionId'], $_POST['Response'] ?? '') ); match ($final->status) { 'APPROVED' => /* $final->threeDS->eci — 05/06 means full authentication (liability shift) */, 'DECLINED' => /* $final->outcome->service */, default => /* PENDING | RUNNING | FAILED */, }; ``` **Response** ```json { "REQUEST_ACTION": "CCAUTHCAP", "REQ_ID": "68192033", "TRANS_STATUS_NAME": "APPROVED", "TRANS_VALUE": 49.99, "CURR_CODE_ALPHA": "USD", "TRANS_VALUE_SETTLED": 49.99, "CURR_CODE_ALPHA_SETTLED": "USD", "TRANS_EXCH_RATE": "", "TRANS_ID": 8839201112, "CUST_ID": 9928102, "XTL_CUST_ID": "cUsT992xP", "PO_ID": 77281920, "XTL_ORDER_ID": "INV-999", "BATCH_ID": 882910, "PROC_NAME": "Inovio Primary", "MERCH_ACCT_ID": 110203, "CARD_BRAND_NAME": "Mastercard", "CARD_TYPE": "MASTERCARD BLACK CARD", "CARD_CLASS": "Consumer Credit", "CARD_PREPAID": 0, "CARD_BANK": "CREDOMATIC INTERNATIONAL", "CARD_COUNTRY": "CRI", "CARD_DETAIL": "Credit", "CARD_BALANCE": "", "PMT_L4": "3535", "PMT_ID": 8829102, "PMT_ID_XTL": "", "PMT_AAU_UPDATE_DT": "", "PMT_AAU_UPDATE_DESC": "", "PROC_UDF01": "", "ANI_RESP_DECISION": "", "PROC_UDF02": "", "PROC_AUTH_RESPONSE": "AUTH99", "PROC_RETRIEVAL_NUM": "1029384A-B8C7-D6E5-F4G3-H2I1J0K9L8M7", "PROC_REFERENCE_NUM": "REF10293847", "PROC_REDIRECT_URL": "", "AVS_RESPONSE": "M", "CVV_RESPONSE": "M", "CARD_BRAND_TRANSID": "", "REQUEST_API_VERSION": "4.14", "P3DS_VENDOR": "", "P3DS_RESPONSE": "Y", "PO_LI_ID_1": "8829103", "PO_LI_COUNT_1": 1, "PO_LI_AMOUNT_1": "49.99", "PO_LI_PROD_ID_1": "SKU-992", "MBSHP_ID_1": "88291", "TRANS_NTOKEN_USED": 1 } ``` --- # 3D Secure: External Provider Pass authentication values from a third-party 3D Secure vendor through to the gateway on your authorization call. Source: https://developer.inoviopay.com/api/3ds-external.html Markdown: https://developer.inoviopay.com/api/3ds-external.md If you perform 3D Secure authentication through a third-party vendor before the transaction, you must pass the resulting authentication values to the gateway. | Parameter | Description | |---|---| | `P3DS_CAVV` (required) | Cardholder Authentication Verification Value. | | `P3DS_ECI` (required) | Electronic Commerce Indicator. | | `P3DS_XID` (required) | Transaction ID. | | `P3DS_VERSION` (required) | Reports on 3DS Version used to process Transaction (Required for Mastercard Identity Check transactions in Authorization on 3DS 2). | | `P3DS_TRANSID` (required) | Unique transaction identifier assigned by the Directory Server (DS) - (Required for Mastercard Identity Check transactions in Authorization IF `P3DS_VERSION` is 3DS 2). | | `P3DS_SCREEN_HEIGHT` (optional) | Total height of the cardholder's screen in pixels. | | `P3DS_SCREEN_WIDTH` (optional) | Total width of the cardholder's screen in pixels. | | `P3DS_JAVA_ENABLED` (optional) | A Boolean value (TRUE/FALSE) that represents the ability of the cardholder browser to execute Java. | | `P3DS_JAVASCRIPT_ENABLED` (optional) | A Boolean value (TRUE/FALSE) that represents the ability of the cardholder browser to execute JavaScript. | | `P3DS_BROWSER_HEADER` (optional) | The exact content of the HTTP accept headers sent from the cardholder's browser. Example: `text/html,application/xhtml+xml,application/xml;q=0.9,*/*; q=0.8` | | `P3DS_BROWSER_LANGUAGE` (optional) | Value represents the browser language as defined in IETF BCP47. | | `P3DS_BROWSER_COLOR_DEPTH` (optional) | Value represents the bit depth of the color palette for displaying images, in bits per pixel. Possible Values: 1, 4, 8, 15, 16, 24, 32, 48. | | `P3DS_BROWSER_TIME_ZONE` (optional) | Time difference between UTC time and the cardholder browser local time, in minutes. Note: Regardless of direction value should be positive. | | `P3DS_CHALLENGE_WINDOW` (optional) | An override field that a merchant can pass in to set the challenge window size to display to the end cardholder. Possible values: 01 - 250x400, 02 - 390x400, 03 - 500x600, 04 - 600x400, 05 - Full page. | | `USER_AGENT_XTL` (optional) | Software agent responsible for retrieving and facilitating end-user interaction with Web content. | | `XTL_IP` (optional) | Cardholder's IP Address. | These parameters are add-ons to a standard authorization call (for example `CCAUTHCAP` or `CCAUTHORIZE`), not a `REQUEST_ACTION` of their own. Attach them to the same request as your regular payment fields. > **Only the PHP SDK implements this path** > `ThreeDSResult`, the model for an externally-obtained 3DS authentication attached to a normal one-leg `sale()`/`authorize()`, exists only in the PHP SDK (`Inovio\Gateway\Model\ThreeDSResult`). Node, Python and Java carry `BrowserData` on the request and read the gateway's own challenge outcome from `nextAction`, but none of the three has an equivalent for attaching CAVV/ECI/XID values from a third-party 3DS provider. Use the cURL example directly in those languages, or see [the SDKs](https://developer.inoviopay.com/sdks/index.md). **cURL** ```bash curl -X POST "https://api.inoviopay.com/payment/pmt_service.cfm" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "request_action=CCAUTHCAP&req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&request_response_format=JSON&pmt_numb=4111111111111111&pmt_expiry=122026&pmt_key=123&request_currency=USD&li_value_1=49.99&xtl_order_id=INV-999&p3ds_cavv=[CAVV_FROM_YOUR_3DS_PROVIDER]&p3ds_eci=05&p3ds_xid=[XID_FROM_YOUR_3DS_PROVIDER]&p3ds_version=2.2.0&p3ds_transid=[DS_TRANSID_FROM_YOUR_3DS_PROVIDER]" ``` **PHP** ```php use Inovio\Gateway\{Credentials, InovioClient}; use Inovio\Gateway\Model\{LineItem, Money, PaymentMethods, ThreeDSResult}; use Inovio\Gateway\Request\TransactionRequest; $client = new InovioClient(new Credentials('api_user', 'P@ssw0rd!', '12345'), 'SANDBOX'); $req = (new TransactionRequest( PaymentMethods::card('4111111111111111', '122026', '123'), [new LineItem('SKU-992', 1, Money::of('49.99', 'USD'))] ))->withIdempotency('INV-999'); // One leg, no redirect — the authentication already happened with your provider. $req->threeDSResult = new ThreeDSResult( cavv: '[CAVV_FROM_YOUR_3DS_PROVIDER]', eci: '05', transId: '[DS_TRANSID_FROM_YOUR_3DS_PROVIDER]', version: '2.2.0', xid: '[XID_FROM_YOUR_3DS_PROVIDER]' ); $result = $client->sale($req); match ($result->status) { 'APPROVED' => /* $result->threeDS->eci — 05/06 means full authentication (liability shift) */, 'DECLINED' => /* $result->outcome->service */, default => /* PENDING | RUNNING | FAILED */, }; ``` **Response** ```json { "REQUEST_ACTION": "CCAUTHCAP", "REQ_ID": "68192033", "TRANS_STATUS_NAME": "APPROVED", "TRANS_VALUE": 49.99, "CURR_CODE_ALPHA": "USD", "TRANS_VALUE_SETTLED": 49.99, "CURR_CODE_ALPHA_SETTLED": "USD", "TRANS_EXCH_RATE": "", "TRANS_ID": 8839201112, "CUST_ID": 9928102, "XTL_CUST_ID": "cUsT992xP", "PO_ID": 77281920, "XTL_ORDER_ID": "INV-999", "BATCH_ID": 882910, "PROC_NAME": "Inovio Primary", "MERCH_ACCT_ID": 110203, "CARD_BRAND_NAME": "Mastercard", "CARD_TYPE": "MASTERCARD BLACK CARD", "CARD_CLASS": "Consumer Credit", "CARD_PREPAID": 0, "CARD_BANK": "CREDOMATIC INTERNATIONAL", "CARD_COUNTRY": "CRI", "CARD_DETAIL": "Credit", "CARD_BALANCE": "", "PMT_L4": "3535", "PMT_ID": 8829102, "PMT_ID_XTL": "", "PMT_AAU_UPDATE_DT": "", "PMT_AAU_UPDATE_DESC": "", "PROC_UDF01": "", "ANI_RESP_DECISION": "", "PROC_UDF02": "", "PROC_AUTH_RESPONSE": "AUTH99", "PROC_RETRIEVAL_NUM": "1029384A-B8C7-D6E5-F4G3-H2I1J0K9L8M7", "PROC_REFERENCE_NUM": "REF10293847", "PROC_REDIRECT_URL": "", "AVS_RESPONSE": "M", "CVV_RESPONSE": "M", "CARD_BRAND_TRANSID": "", "REQUEST_API_VERSION": "4.14", "P3DS_VENDOR": "[YOUR_3DS_PROVIDER]", "P3DS_RESPONSE": "Y", "PO_LI_ID_1": "8829103", "PO_LI_COUNT_1": 1, "PO_LI_AMOUNT_1": "49.99", "PO_LI_PROD_ID_1": "SKU-992", "MBSHP_ID_1": "88291", "TRANS_NTOKEN_USED": 1 } ``` --- # Apple Pay Fetch Apple Pay configuration from the gateway, run the Apple Pay JS session on your payment page, and authorize the resulting token with CCAUTHCAP. Source: https://developer.inoviopay.com/api/apple-pay.html Markdown: https://developer.inoviopay.com/api/apple-pay.md ## Apple Pay overview This section covers sending credit card transactions to the gateway using Apple Pay. To accept Apple Pay, the web pages that host your payment forms must have their domains registered with Apple. See the *Apple Pay Registration Process* document for details. The Processor and Merchant Account you use must also support Apple Pay; contact your gateway support representative to confirm this. Customers using Apple devices (iPhone, iPad, MacBook, etc.) with an Apple browser (Safari) are able to use Apple Pay to make purchases. The rest of this page describes how to set up an Apple Pay purchase on your hosted payment page and send the resulting authorization to the gateway API. > **Real cards required for production testing** > Test cards will not work when testing Apple Pay in a production environment. Real cards must be used. ## Integration flow This is the order in which you set up Apple Pay processing: 1. Fetch the Apple Pay configuration data from the gateway. 2. Update the configuration with the specific payment/order details. 3. Present the Apple Pay button. 4. Add the Apple Pay library to the payment page. 5. Create a JS function to validate your domain. 6. Create and start the Apple Pay session. 7. Get the authorized payment information from the customer's browser. 8. Send the transaction to the gateway. Each step is discussed below with example code. The example code is illustrative only and does not represent code that can be used as-is in a production environment. ## Fetching your Apple Pay configuration You need configuration data from the gateway to set up the Apple Pay session when the customer clicks the Apple Pay button. **POST** `https://api.inoviopay.com/payment/applepay.cfm` Requests must be made in JSON format. ### Request parameters | Field Name | Description | |---|---| | `REQ_USERNAME` (required) | API credential username. | | `REQ_PASSWORD` (required) | API credential password. | | `DOMAIN_NAME` (required) | Domain name where the payment page is hosted/served from. | | `CLIENT_ID` (required) | Client ID. | | `REQUEST_ACTION` (required) | `APPLEPAYCONFIG`. Used to instruct the endpoint to provide the Apple Pay configuration. | All parameters are required. ### Response The response is a JSON-formatted string: ```json { "APPLEPAY_CLI_CONF_ID": xx, // internal unique id; do not change "REQ_ID": 11111111111, // trace ID, used for troubleshooting "CLIENT_ID": 1111111, // your gateway api client_id "INITIATIVE": "web", // identifies e-commerce transaction "DISPLAY_NAME": "xxxxxxx", // Short, localized description of the merchant. "PARTNER_MERCH_NAME": "xxxxxx", // name of the merchant "PARTNER_INTERNAL_MERCH_ID": "xxxxxxxxxxxxx", // identifies the merchant to apple "ENCRYPT_TO": "xxxxxxxxxxxxxxxxxxxx", // merchant ID to Apple "DOMAIN_LIST": [ // domain registered in Inovio portal serving this page { "DOMAIN_NAME": "paymentpagedomain.com", "DOMAIN_STATUS": "ACTIVE" } ], "paymentRequest": { "countryCode": "US", "currencyCode": "USD", "merchantCapabilities": [ "supports3DS" ], "supportedNetworks": [ "visa", "masterCard" ], "requiredBillingContactFields": [ "postalAddress", "name", "phoneticName", "phone", "email" ], "total": { "label": "", // required; a short, localized description of the line item "type": "final", // do not change "amount": 0 // must be greater than or equal to zero } }, "fetchUrl": "https://api.inoviopay.com/apple-pay-services/api/session/create" } ``` Provide this response on your payment page as the `merchantConfig` parameter: ```html ``` ## Updating the configuration with order details Once `merchantConfig` is stored on your payment page, update the fields that are specific to the current purchase: ```html ``` ## Adding the Apple Pay library Add the Apple Pay library to your payment page so it can call the Apple Pay API. Source the library script in the page's HTML: ```html ``` This library also validates that the user's browser and device are eligible for Apple Pay. ## Validating your domain The Apple Pay JS API library calls a function you define to validate the domain where your payment page is hosted. This must load directly from your servers on that domain; no proxies or iframes. ```html ``` ## Presenting the Apple Pay button Once the Apple Pay library is added, add an HTML element for the button. The element must be named `apple-pay-button`. ```html ``` ## Creating and starting the Apple Pay session After presenting the button, add an action to it that starts the session, which triggers the browser to display a dialog where the user selects their card and approves the transaction. Add an `onClick` event to the Apple Pay button. `paymentRequest` here refers to the `paymentRequest` object returned by `APPLEPAYCONFIG`. ### Handling the authorized payment After the customer authorizes the payment in the Apple Pay overlay, Safari returns an object (the Apple Pay token) with the data needed to authorize the payment with the gateway API. Create a `session.onpaymentauthorized` method to receive this token object. ### Handling a canceled payment or error Handle the case where the customer does not authorize the payment, or where an error is thrown, with `session.oncancel`. The full example, combining the button click handler, merchant validation, payment authorization, and cancel handling: ```javascript async function onApplePayClick() { // Check for valid ApplePay session if(!ApplePaySession){ return; } // Get the paymentRequest data from the merchantConfig object const paymentRequest = this.merchantConfig.paymentRequest; // Validate the merchant. // Note that this.merchantConfig is defined in Step 3 const session = new ApplePaySession(3, paymentRequest); session.onvalidatemerchant = async (event) => { const merchantSession = await this.validateMerchant(this.merchantConfig).then(res=>res.json()); session.completeMerchantValidation(merchantSession); }; // Handle the authorized payment // Note, this must come inside the onApplePayClick() function, and // before session.begin session.onpaymentauthorized = async (event) => { // Define ApplePayPaymentAuthorizationResult const result = { status: ApplePaySession.STATUS_SUCCESS, }; var applePaymentToken = event.payment; var tokenEncoded = btoa(JSON.stringify(applePaymentToken)); // Now that the payment is authorized in Apple Pay, you can // initiate a service call to the gateway passing the // applePaymentToken into the gateway API parameter: PMT_WALLET_CRYPTOGRAM // // Do that here, through your server // If the payment is declined at the processor, then you should // put an appropriate message in place to the customer // result.status = ApplePaySession.STATUS_FAILURE // Now we can gracefully complete the Apple Pay browser interaction session.completePayment(result); }; // In case the customer cancels or in case of an unexpected error // Note, this must come inside the onApplePayClick() function, and // before session.begin session.oncancel = (event) => { // Payment canceled by WebKit // error handling console.log("Payment canceled by WebKit: "+JSON.stringify(event.error)) } // now the user will interact to approve the purchase // Note, this must come at the END of the onApplePayClick() session.begin(); } ``` ## Sending the authorized payment to the gateway Once Apple Pay returns the authorized payment token, send it to the gateway as a normal `CCAUTHCAP` request, passing the token in `PMT_WALLET_CRYPTOGRAM`: ``` https://api.inoviopay.com/payment/pmt_service.cfm?request_action=CCAUTHCAP&li_count_1=1&li_prod_id_1=111&li_value_1=49.95&req_username=GATEWAY_USER&req_password=GATEWAY_PASS&site_id=11111&request_response_format=JSON&request_api_version=4.14&request_currency=USD&PMT_WALLET=applepay&PMT_WALLET_CRYPTOGRAM=xxxxxxx ``` ### Important requirements for Apple Pay authorization requests You must **not** include any of the following parameters; doing so will cause the gateway to reject the request: - `PMT_NUMB` - `PMT_KEY` - `PMT_EXPIRY` - `TOKEN_GUID` - `PMT_ID` - `PMT_LAST4` - `PMT_ID_XTL` - `PMT_NUMB_COF` - `REQUEST_INITIATOR` Other gateway parameters can be included, but they must match the values used in the Apple Pay session: - `li_value_1` must match the value of `paymentRequest.total` in the `merchantConfig` data. - `request_currency` must match the value of `paymentRequest.currencyCode` in the `merchantConfig` data. > **The PDF gives no further Apple Pay field table beyond this** > Section 21.11 of the source spec lists only the excluded-parameter list and the two must-match rules above; it does not provide a separate table of additional required `PMT_WALLET_*` fields, so none is invented here. ## Rebilling and card on file The gateway response returns a `CUST_ID` and `PMT_ID` on a successful Apple Pay authorization, the same as it does for normal transactions. To authorize against the generated Apple Pay payment record, pass the same `CUST_ID` and `PMT_ID` on all subsequent rebills or unscheduled merchant-initiated card-on-file transaction requests. > **Not in the SDKs yet** > Apple Pay is not implemented in any of the four SDKs. A `WalletToken` variant (`walletType: 'applepay' | 'googlepay'`) is declared in each language's type system, but `PaymentMethods` only constructs `card`, `token` and `savedCard` in v1, there is no constructor that produces a wallet payment method, so `sale()`/`authorize()` cannot be handed a `PMT_WALLET_CRYPTOGRAM` token today. Send the authorization as a raw HTTP request per the example above, or see [the SDKs](https://developer.inoviopay.com/sdks/index.md). Rebilling against the resulting `CUST_ID`/`PMT_ID` does work through the SDK once you have them, using the same `SavedCard` path as [Card on File](https://developer.inoviopay.com/api/card-on-file.md). --- # Google Pay Fetch Google Pay configuration from the gateway, run the Google Pay JS session on your payment page, and authorize the resulting token with CCAUTHCAP. Source: https://developer.inoviopay.com/api/google-pay.html Markdown: https://developer.inoviopay.com/api/google-pay.md ## Google Pay overview This section covers sending credit card transactions to the gateway using Google Pay. To accept Google Pay, the web pages that host your payment forms must have their domains registered. The Processor and Merchant Account you use must also support Google Pay; contact your gateway support representative for more information. For more information on Google Pay itself, see Google's own documentation: an overview of Google Pay, the Google Pay Integration Checklist, and the Google Pay Branding Guidelines, along with Google's list of payment methods that support Google Pay (your `allowedCardNetworks` is determined by your merchant account configuration), Google's list of countries/regions where Google Pay is available (the gateway supports the currencies enabled by your merchant account configuration), and Google's list of supported browsers. > **Real cards required for production testing** > Test cards will not work when testing Google Pay in a production environment. A real PAN must be used. ## Integration flow These are the steps to integrate Google Pay with the gateway: 1. Fetch your Google Pay configuration data from the gateway. 2. Add the Google Pay library to your payment page. 3. Customize the Google Pay button. 4. Handle the Google Pay loaded event. 5. Present the Google Pay button. 6. Handle the Google Pay button click. 7. Handle payment authorization. 8. Submit the authorized payment data. Each step is discussed below with example code. The example code is illustrative only and does not represent code that can be used as-is in a production environment. ## Fetching your Google Pay configuration You need configuration data from the gateway to set up the Google Pay session after the customer clicks the Google Pay button. **POST** `https://api.inoviopay.com/payment/googlepay.cfm` Requests must be made in JSON format. ### Request parameters | Field Name | Description | |---|---| | `REQ_USERNAME` (required) | API credential username. | | `REQ_PASSWORD` (required) | API credential password. | | `DOMAIN_NAME` (required) | Domain name where the payment page is hosted/served from. | | `CLIENT_ID` (required) | Client ID. | | `REQUEST_ACTION` (required) | `GOOGLEPAYCONFIG`. Used to instruct the endpoint to provide the proper Google Pay configuration for your unique information. | All parameters are required. ### Sample body request ```json { "request_login": "googlepayClient@Inovio.com", "request_password": "Test123", "request_action": "GOOGLEPAYCONFIG", "domain_name": "paymentpagedomain.com", "client_id": "100" } ``` ### Sample response ```json { "GOOGLEPAY_CLI_CONF_ID": 1, "REQ_ID": 2744, "CLIENT_ID": 100, "DOMAIN_LIST": [ { "DOMAIN_NAME": "paymentpagedomain.com", "DOMAIN_STATUS": "ACTIVE" } ], "hostConfig": { "merchantId": "BC...JW", "environment": "TEST", "merchantName": "Inovio", "gatewayId": "inoviopay", "gatewayMerchantId": "string-string" } } ``` Provide this response on your payment page as the `merchantConfig` parameter. ## Adding the Google Pay library Import the Google Pay JavaScript library into your payment page, and add an empty `
` titled `gpay-container`. This div is the placeholder where the Google Pay button appears; you can place it anywhere on your page. ```html
``` The `
` is your designated spot for the button. The `pay.js` script tag asynchronously loads the Google Pay library. The `onload="onGooglePayLoaded()"` attribute ensures your `onGooglePayLoaded()` function (which checks whether Google Pay is ready) runs as soon as the library finishes loading. ## Customizing the Google Pay button The Google Pay button should fit your website's design and user experience; consult Google's documentation for how to achieve this. After the Google Pay API is loaded and ready, your site can display the button. It is dynamically generated by the Google Pay library and placed inside the `gpay-container` div. ```javascript const GPAY_BUTTON_CONTAINER_ID = 'gpay-container'; function renderGooglePayButton() { const button = getGooglePaymentsClient().createButton({ buttonColor: 'default', buttonType: 'buy', buttonRadius: 4, buttonLocale: 'en', onClick: onGooglePaymentButtonClicked, allowedPaymentMethods: baseGooglePayRequest.allowedPaymentMethods, }); document.getElementById(GPAY_BUTTON_CONTAINER_ID).appendChild(button); } ``` The `createButton()` library method takes a `ButtonOptions` configuration argument that defines how the button looks and behaves: - `GPAY_BUTTON_CONTAINER_ID`: constant holding the ID (`gpay-container`) of the HTML element where the button should appear. - `renderGooglePayButton()`: function responsible for creating and adding the button. - `getGooglePaymentsClient().createButton({...})`: uses the Google Pay client to generate the button. The `ButtonOptions` object allows customization: - `buttonColor`: choose the appearance (`default`, `black`, `white`). - `buttonType`: defines the text on the button (for example "Buy/Checkout with Google Pay"). - `buttonRadius`: adjusts the roundness of the button's corners. - `buttonLocale`: sets the language for the button's text. - `onClick: onGooglePaymentButtonClicked`: calls your `onGooglePaymentButtonClicked` function (which initiates the payment process) whenever a customer clicks the button. - `allowedPaymentMethods`: ensures the button only appears if there are payment methods available that match what you've configured in `baseGooglePayRequest`. - `document.getElementById(...).appendChild(button)`: inserts the button Google Pay created directly into the div on your webpage, making it visible to the user. ## Handling the Google Pay loaded event The `onGooglePayLoaded()` function is called when the Google Pay API script has finished loading. It is the handshake with the Google Pay service, ensuring everything is in order before you present the payment option to your customers. ```javascript function onGooglePayLoaded() { const req = deepCopy(baseGooglePayRequest); getGooglePaymentsClient() .isReadyToPay(req) .then(function (res) { if (res.result) { renderGooglePayButton(); } else { console.log('Google Pay is not ready for this user.'); } }) .catch(console.error); } ``` ## Configuring Google Pay The `baseGooglePayRequest` object defines the fundamental configuration for all Google Pay requests. ```javascript const baseGooglePayRequest = { apiVersion: 2, apiVersionMinor: 0, allowedPaymentMethods: [ { type: 'CARD', parameters: { allowedAuthMethods: ['PAN_ONLY', 'CRYPTOGRAM_3DS'], allowedCardNetworks: ['AMEX', 'DISCOVER', 'MASTERCARD', 'VISA'], }, tokenizationSpecification: { type: "PAYMENT_GATEWAY", parameters: { "gateway": "inoviopay", "gatewayMerchantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" } }, }, ], merchantInfo, }; Object.freeze(baseGooglePayRequest); let paymentsClient = null; function getGooglePaymentsClient() { if (paymentsClient === null) { paymentsClient = new google.payments.api.PaymentsClient({ environment: 'TEST', // Environment configuration merchantInfo, paymentDataCallbacks: { onPaymentAuthorized: onPaymentAuthorized, onPaymentDataChanged: onPaymentDataChanged, }, }); } return paymentsClient; } ``` Key properties that shape the Google Pay experience: - `apiVersion` and `apiVersionMinor`: tell Google Pay which version of the Google Pay API you are using. - `allowedPaymentMethods`: array declaring what types of payments you accept. - `parameters` (within the `CARD` type): - `allowedAuthMethods`: defines the authentication methods your integration supports: - `PAN_ONLY`: used for cards saved directly to a user's Google account. Google Pay returns the actual card number. To use 3D Secure with `PAN_ONLY`, submit your third party 3-D Secure details with the authorization; the gateway will not prevent a transaction from being sent to the processor without 3DS on the transaction request. - `CRYPTOGRAM_3DS`: used for cards tokenized via the Google Wallet app. Instead of the real card number, a device-specific token is used. A 3-D Secure cryptogram is generated on the user's device, providing stronger security and often shifting liability for fraud to the card issuer. Inovio recommends using this `allowedAuthMethods` value only. - `allowedCardNetworks`: lists all the major card networks your Merchant Account is set up to accept (American Express, Discover, Mastercard, Visa, and/or more). See Google's official Google Pay documentation for details. - `tokenizationSpecification`: configuration for how payment credentials are secured and sent to your payment processor. - `type: "PAYMENT_GATEWAY"`: indicates that the payment gateway handles tokenization on your behalf. - `parameters`: gateway-specific details essential for secure processing, provided to you when you request your configuration data from the gateway: - `gateway`: the name of your payment gateway (for example `inoviopay`). - `gatewayMerchantId`: your unique ID provided by the gateway. - `Object.freeze(baseGooglePayRequest)`: prevents accidental changes to your base Google Pay configuration after it is set. - `paymentsClient` starts as `null`; `getGooglePaymentsClient()` uses lazy initialization, creating the `PaymentsClient` instance only when needed. - `environment`: `'TEST'` is for development and debugging; change to `'PRODUCTION'` when you're ready to go live. - `paymentDataCallbacks`: functions to handle events during the Google Pay flow: - `onPaymentAuthorized`: called after the user has successfully authorized a payment. This is where you send the payment data to the payment service provider for processing. - `onPaymentDataChanged`: fires if the user changes their payment data (such as selecting a different shipping address or payment method) within the Google Pay sheet, letting you dynamically update the order total or shipping options. ## Handling the Google Pay button click When a user clicks your Google Pay button, `onGooglePaymentButtonClicked()` gathers the specifics of the current transaction and launches the Google Pay payment window. ```javascript // @namespace googlePayHandler // @description A self-contained handler for a professional and secure Google Pay // integration. This script manages fetching configuration from a secure backend, // setting up the Google Pay client, and handling the entire payment lifecycle. const googlePayHandler = { // Default settings can be overridden by the customer during initialization. config: { gpayButtonContainerId: 'gpay-container', currencyCode: 'USD', countryCode: 'US', environment: 'TEST', // Should be 'PRODUCTION' for live transactions }, // --- Internal State --- paymentsClient: null, merchantConfig: null, // CRITICAL: Fetches the merchant configuration from the CUSTOMER'S backend. // This is a vital security measure. API credentials should NEVER be exposed in // client-side code. // Your customer must implement a server endpoint that securely communicates with // your API. // @returns {Promise} A promise that resolves with the merchant // configuration or null on failure. async fetchMerchantConfigFromServer() { // --- CUSTOMER ACTION REQUIRED --- // This URL must point to an endpoint on YOUR CUSTOMER'S server. // Their server is responsible for making the secure, server-to-server call to the // Inovio API. const customerBackendUrl = 'https://api.customer-website.com/get-payment-config'; try { const response = await fetch(customerBackendUrl); if (!response.ok) { throw new Error(`Network response was not ok: ${response.statusText}`); } const configData = await response.json(); console.log("Successfully fetched merchant configuration."); return configData; } catch (error) { console.error("Fatal Error: Could not retrieve merchant configuration from the server.", error); // Optional: Display a user-friendly error message in the UI. // e.g., document.getElementById(this.config.gpayButtonContainerId).innerText = // "Payment system unavailable."; return null; } }, // Initializes the Google Pay client if it doesn't already exist. // @returns {google.payments.api.PaymentsClient} getGooglePaymentsClient() { if (this.paymentsClient === null) { if (!this.merchantConfig) { throw new Error("Cannot initialize Google Payments client without merchant configuration."); } this.paymentsClient = new google.payments.api.PaymentsClient({ environment: this.config.environment, merchantInfo: { merchantId: this.merchantConfig.HOST_CONFIG.merchantId, // Customer will set their own display name merchantName: this.merchantConfig.HOST_CONFIG.merchantName || 'Sample Merchant', }, paymentDataCallbacks: { onPaymentAuthorized: this.onPaymentAuthorized, onPaymentDataChanged: this.onPaymentDataChanged, }, }); } return this.paymentsClient; }, // Main entry point. Initializes the Google Pay flow. // @param {Object} [userConfig={}] - Customer-specific configuration to override the // defaults. async initialize(userConfig = {}) { // 1. Merges customer's configuration with defaults. this.config = { ...this.config, ...userConfig }; // 2. Securely fetches the configuration from the customer's backend. this.merchantConfig = await this.fetchMerchantConfigFromServer(); if (!this.merchantConfig) { console.error("Google Pay initialization failed: Merchant configuration is missing or could not be fetched."); return; } const isReadyToPayRequest = this.createBaseRequest(); this.getGooglePaymentsClient() .isReadyToPay(isReadyToPayRequest) .then((isReadyToPayRequest) => { if (isReadyToPayRequest.result) { this.renderGooglePayButton(); } else { console.log('Google Pay is not ready for this user.'); } }) .catch(console.error); }, // Creates and appends the Google Pay button to the configured container. renderGooglePayButton() { const container = document.getElementById(this.config.gpayButtonContainerId); if (!container) { console.error(`Google Pay button container with ID "${this.config.gpayButtonContainerId}" was not found in the DOM.`); return; } // Clears any previous content (e.g. error messages) container.innerHTML = ''; const button = this.getGooglePaymentsClient().createButton({ onClick: this.onGooglePaymentButtonClicked.bind(this), allowedPaymentMethods: this.createBaseRequest().allowedPaymentMethods, }); container.appendChild(button); }, // Creates the base payment request object required by the Google Pay API. // @returns {Object} The Google Pay base request object. createBaseRequest() { return { apiVersion: 2, apiVersionMinor: 0, allowedPaymentMethods: [{ type: 'CARD', parameters: { allowedAuthMethods: ['PAN_ONLY', 'CRYPTOGRAM_3DS'], allowedCardNetworks: ['AMEX', 'DISCOVER', 'INTERAC', 'JCB', 'MASTERCARD', 'VISA'], }, tokenizationSpecification: { type: 'PAYMENT_GATEWAY', parameters: { gateway: this.merchantConfig.HOST_CONFIG.gatewayId, gatewayMerchantId: this.merchantConfig.MERCH_IDENTIFIER, // These values should come from your secure config fetch }, }, }], }; }, // Handles the click event from the Google Pay button. onGooglePaymentButtonClicked() { const paymentDataRequest = { ...this.createBaseRequest(), transactionInfo: { countryCode: this.config.countryCode, currencyCode: this.config.currencyCode, totalPriceStatus: 'FINAL', totalPrice: '10.00', }, merchantInfo: { merchantId: this.merchantConfig.MERCH_IDENTIFIER, merchantName: this.merchantConfig.MERCHANT_NAME || 'Sample Merchant' }, // The callbackIntents must match the callbacks provided in // getGooglePaymentsClient(). We provided onPaymentAuthorized and // onPaymentDataChanged. To handle dynamic shipping, you would add // 'SHIPPING_ADDRESS' and 'SHIPPING_OPTION' and then build out the // logic in the onPaymentDataChanged callback. callbackIntents: ['PAYMENT_AUTHORIZATION'], }; console.log('Requesting payment data...', paymentDataRequest); this.getGooglePaymentsClient() .loadPaymentData(paymentDataRequest) .catch(err => console.error("loadPaymentData error:", err)); // Catches user cancellation or other errors. }, // Callback for when payment is successfully authorized by the user. // @param {Object} paymentData - The authorized payment data from Google, including // the token. // @returns {Promise} A promise resolving with the final transaction // result. onPaymentAuthorized(paymentData) { return new Promise((resolve) => { console.log('Payment authorized. Response from Google:', paymentData); const paymentToken = paymentData.paymentMethodData.tokenizationData.token; // --- CUSTOMER ACTION REQUIRED --- // 1. Send this `paymentToken` to your server. Never do this from the client // side. // 2. On your server, use this token to make the final "charge" or // "auth/capture" call to the Payment Gateway API. NEVER do this from the client side. // 3. Based on the response from the Payment Gateway, resolve with the final // state. console.log("Action required: Send this payment token to your server for processing:", paymentToken); // Fetch your server here. resolve({ transactionState: 'SUCCESS' }); // Example of resolving with an error if the Payment Gateway declines the // transaction: /* resolve({ transactionState: 'ERROR', error: { intent: 'PAYMENT_AUTHORIZATION', message: 'Your payment was declined by the issuer. Please try another card.', reason: 'PAYMENT_DATA_INVALID', }, }); */ }); }, } // Optional callback for when payment data changes (e.g., shipping address or // options). // @param {Object} intermediatePaymentData // @returns {Promise} function onPaymentDataChanged(intermediatePaymentData) { return new Promise((resolve) => { console.log('Intermediate payment data changed:', intermediatePaymentData); // This is where you would implement dynamic updates, such as recalculating // shipping costs or taxes based on the user's selected address. // For this example, we do nothing and resolve an empty object. resolve({}); }); } ``` The `googlePayHandler` is designed to make Google Pay work smoothly and securely: - **Default settings**: currency (`USD`), country (`US`), and `TEST` mode (use `PRODUCTION` for live transactions). - **Getting merchant information**: the system gets setup information from your store's backend server. Never put sensitive payment details directly on the website itself; the server talks securely to the gateway. If it can't get this information, Google Pay can't start. - **Setting up Google Pay**: once `googlePayHandler` has the store's information, it sets up the connection to Google Pay. - **Checking readiness**: the system checks whether the customer's device and browser are ready to use Google Pay, showing the button if so and hiding it otherwise. - **Showing the button**: if everything is ready, `googlePayHandler` places the Google Pay button on the page where you told it to appear. - **Preparing the payment request**: clicking the Google Pay button creates a detailed request for the payment, per [Configuring Google Pay](#configuring-google-pay). - **Opening the Google Pay window**: this request is sent to Google Pay, which opens the payment window. - **Getting a secure token**: on successful payment, the system receives a `paymentToken`, a highly secure encrypted representation of the payment. - **Security step**: this `paymentToken` must never be handled directly on the browser or device beyond receiving it; it is sent immediately to the store's secure backend server, which is the only place that should process this Google Pay token to complete the payment with Inovio. - **Finalizing the payment**: the store's server uses `paymentToken` to make the final "charge" or "authorize" transaction with the gateway. - **Handling changes (optional)**: if the customer changes their shipping address within Google Pay, the system can automatically update shipping costs or taxes. ## Payment authorization `onPaymentAuthorized()` is called automatically after the user has successfully gone through the Google Pay sheet and approved the payment. This is where you take the secure payment information and send it to your payment processor. ```javascript function onPaymentAuthorized(paymentData) { return new Promise(function (resolve, reject) { // Write the data to console for debugging console.log('onPaymentAuthorized', paymentData); // --- CUSTOMER ACTION REQUIRED --- // 1. Send this `paymentToken` to your server. // 2. On your server, use this token to make the final "charge" or // "auth/capture" call to the Inovio gateway API. NEVER do this from the client side. // 3. Based on the response from the Inovio gateway, resolve with the final // state. const paymentAuthorizationResult = { transactionState: 'SUCCESS' }; // Example of resolving with an error if your gateway declines the payment: /** const paymentAuthorizationResult ={ transactionState: 'ERROR', error: { intent: 'PAYMENT_AUTHORIZATION', message: 'Insufficient funds', reason: 'PAYMENT_DATA_INVALID', }, }; */ resolve(paymentAuthorizationResult); }); } ``` When this function is called, Google Pay hands you the `paymentData` object. Inside it is the secure Google payment token, at `paymentData.paymentMethodData.tokenizationData.token`. This token represents the customer's payment method without exposing their actual card details. The critical backend step, though commented out in the example above, is sending this Google payment token to your own secure server. Your server is then responsible for sending the Google Pay token to the gateway to authorize and capture the funds, and receiving the processing result from the gateway. `onPaymentAuthorized(paymentData)` returns a Promise, letting you perform asynchronous operations and then resolve with the final `paymentAuthorizationResult` when you have it. In essence, `onPaymentAuthorized()` is the bridge between the customer approving the payment in Google Pay and your system charging their card through your payment processor. ## Submitting authorized payment data Once Google Pay returns the authorized payment token, send it to the gateway as a normal `CCAUTHCAP` request, passing the token in `PMT_WALLET_CRYPTOGRAM`: ``` https://api.inoviopay.com/payment/pmt_service.cfm?request_action=CCAUTHCAP&li_count_1=1&li_prod_id_1=111&li_value_1=49.95&req_username=GATEWAY_USER&req_password=GATEWAY_PASS&site_id=11111&request_response_format=JSON&request_api_version=4.14&request_currency=USD&PMT_WALLET=googlepay&PMT_WALLET_CRYPTOGRAM=xxxxxxx ``` ### Important requirements for Google Pay authorization requests You must **not** include any of the following parameters; doing so will cause the gateway to reject the request: - `PMT_NUMB` - `PMT_KEY` - `PMT_EXPIRY` - `TOKEN_GUID` - `PMT_ID` - `PMT_LAST4` - `PMT_ID_XTL` - `BILL_ADDR_STATE` - `BILL_ADDR_ZIP` - `BILL_ADDR_COUNTRY` - `BILL_ADDR_CITY` - `BILL_ADDR` Other gateway parameters can be included, but they must match the values used in the Google Pay session: - `li_value_1` must match the value of `paymentRequest.total` in the `merchantConfig` data. - `request_currency` must match the value of `paymentRequest.currencyCode` in the `merchantConfig` data. > **The PDF gives no further Google Pay field table beyond this** > Section 22.10.1 of the source spec lists only the excluded-parameter list and the two must-match rules above; it does not provide a separate table of additional required `PMT_WALLET_*` fields, so none is invented here. ## Rebilling and card on file The gateway response returns a `CUST_ID` and `PMT_ID` on a successful Google Pay authorization, the same as it does for normal transactions. To authorize against the generated Google Pay payment record, pass the same `CUST_ID` and `PMT_ID` on all subsequent rebills or unscheduled merchant-initiated card-on-file transaction requests. > **Not in the SDKs yet** > Google Pay is not implemented in any of the four SDKs. A `WalletToken` variant (`walletType: 'applepay' | 'googlepay'`) is declared in each language's type system, but `PaymentMethods` only constructs `card`, `token` and `savedCard` in v1, there is no constructor that produces a wallet payment method, so `sale()`/`authorize()` cannot be handed a `PMT_WALLET_CRYPTOGRAM` token today. Send the authorization as a raw HTTP request per the example above, or see [the SDKs](https://developer.inoviopay.com/sdks/index.md). Rebilling against the resulting `CUST_ID`/`PMT_ID` does work through the SDK once you have them, using the same `SavedCard` path as [Card on File](https://developer.inoviopay.com/api/card-on-file.md). --- # Postback Service v2.13 Real-time server-to-server notifications for purchases, rebills, subscription changes, reversals, chargebacks, and account updater events. Source: https://developer.inoviopay.com/api/postback.html Markdown: https://developer.inoviopay.com/api/postback.md > **Not in the SDKs yet** > Postback parsing and signature verification are not implemented in any of the four SDKs. There is no `Postback` model, no HMAC-verification helper, and no event-type enum for `PURCHASE`/`RENEWAL`/`CHARGEBACK`/etc. in any language's source tree. The gateway pushes postbacks to your endpoint independently of any outbound SDK call, so parse and verify them with your own HTTP framework and HMAC library, as shown below. See [the SDKs](https://developer.inoviopay.com/sdks/index.md). Postback is our way of communicating important payment processing and customer related information to your system in real-time. A Postback is triggered by events such as new purchase requests, rebills, subscription cancellations, transaction reversal, chargebacks, and others. Merchants must provide us with a valid webpage URL that will accept Postback data. ### Successful Response & Retries In order to tell the service that the postback request was received successfully, merchants must simply reply with the string, `100` inside the `` of the response. Note that anything other than "100" will be considered as a failed response. This does not reference the HTTPS response header. In the event of a failed postback response, the service will attempt the postback three additional times: - **2nd attempt:** 5 minutes after first failed attempt - **3rd attempt:** 1 hour after the second failed attempt - **4th attempt:** 1 day after the third failed attempt > **DATA REPOST WARNING** > Merchants must be aware that some data may be reposted for any reason. Your system should verify if a postback with the exact same data (e.g., Order ID / PO_ID) has already been recorded and prevent duplicate ledger entries. ### Postback Event Types The `TYPE` element describes the type of postback, corresponding to specific system or user actions: | Event Type | Action Description | |---|---| | PURCHASE | Product purchase, Reactivate subscription, Upgrade subscription, Capture transaction. Also used for 3D Secure approved transactions. | | RENEWAL | Subscription Renewal/Rebill. | | SUBSCRIPTION TERMINATION REQUEST | User requests subscription cancellation. | | SUBSCRIPTION TERMINATION | System cancels subscription. | | CREDIT | Credit transaction. Also triggered when TC40, RDR, Ethoca, or Verifi alerts result in automated refunds. | | REVERSAL | Void transaction. | | CHARGEBACK | Chargeback transaction. | | BLACKLIST PAYMENT NUMBER | Add credit card to black list. | | PAYMENT AAU UPDATE | Automatic Account Updater (e.g., Closed Account, Expiration Date Change, New Account Number). | | TRANSACTION STATUS UPDATE | Status changes from DECLINE to APPROVED. Also used for 3DS declined or failed transactions. | | TRANSACTION ARN UPDATE | ARN (acquirer reference number) value updated on a previously authorized transaction. | | VERIFY | Postback URL verification. | | TC40 / RDR / RDR STANDALONE | Chargeback Mitigation Alert Events (TC40 Alerts, RDR Disputes). | ### Postback Top-Level Elements | Element Name | Description | |---|---| | TYPE | Postback Event Type Name (Example: "PURCHASE"). | | CUSTOMER | Contains customer record information (CUST_ID, CUST_EMAIL, CUST_BRCPFCNPJ, etc.). | | MEMBERSHIP | Contains membership (subscription) record information. Includes SITE and ORIGINAL_TRANSACTION_DATA sub-elements. | | PURCHASE_ORDER | Contains purchase order (invoice record) information (PO_ID, PO_VALUE, CURRENCY, XTL_UDFxx, etc.). | | LINE_ITEM | Contains line item parameters (LI_AMOUNT, LI_TYPE, PROD_ID, etc.). | | BILL_ADDRESS / SHIP_ADDRESS | Contains billing and shipping address parameters. | | PAYMENT | Contains billing account information (PAYMENT_TYPE, PMT_BIN, PAYMENT_HASH, MERCH_ACCT_ID, etc.). | | TRANSACTION | Contains important transaction information (TRANS_ID, TRANS_STATUS_NAME, PROCESSOR_RESPONSE, etc.). | | SOURCE / MESSAGE | Contains the originating source of the event (used in Alerts: "TC40", "RDR", "ETHOCA", "VERIFI") and a short description. | ### Acknowledging a postback Reply to every postback with the literal body text `100` to acknowledge receipt. Anything else is treated as a failure and retried per the schedule above. ```text 100 ``` ### Verifying the postback signature Postback signature validation is available on v2.9+ to enhance security. The gateway computes an HMAC-SHA256 over the raw JSON body using your postback secret; recompute the same HMAC on your end and compare it to the signature the gateway sends so you can reject forged postback traffic. ```php ``` ### Example postback payloads Postbacks are pushed by the gateway to your merchant URL, so they aren't modeled as outbound `REQUEST_ACTION` calls the way the payment API is. Below is one representative `PURCHASE` event body. `VERIFY` postbacks (used to confirm your endpoint is reachable), `CHARGEBACK` postbacks (populated `TRANSACTION` fields describing the dispute), and `PAYMENT AAU UPDATE` postbacks (populated `PAYMENT` fields describing the card change) follow the same top-level shape with different `TYPE` values and populated sub-elements. ```json { "TYPE": "PURCHASE", "CUSTOMER": { "CUST_ID": 9928102, "CUST_EMAIL": "customer@example.com", "CUST_BRCPFCNPJ": "" }, "PURCHASE_ORDER": { "PO_ID": 77281920, "PO_VALUE": 49.99, "CURRENCY": "USD", "XTL_UDF01": "INV-999" }, "LINE_ITEM": { "LI_AMOUNT": 49.99, "LI_TYPE": "PRODUCT", "PROD_ID": "SKU-992" }, "BILL_ADDRESS": { "CUST_ADDRESS1": "123 Main St", "CUST_CITY": "Austin", "CUST_STATE": "TX", "CUST_ZIP": "78701", "CUST_COUNTRY": "US" }, "PAYMENT": { "PAYMENT_TYPE": "CREDITCARD", "PMT_BIN": "411111", "PAYMENT_HASH": "a1b2c3d4e5f6", "MERCH_ACCT_ID": 110203 }, "TRANSACTION": { "TRANS_ID": 8839201112, "TRANS_STATUS_NAME": "APPROVED", "PROCESSOR_RESPONSE": "AUTH99" } } ``` --- # Testing Test bank usage, a full reference table of gateway test scenarios, and the test credit card numbers that trigger them. Source: https://developer.inoviopay.com/api/testing.html Markdown: https://developer.inoviopay.com/api/testing.md ### Using the Test Bank Merchants may run test authorizations and other gateway actions using the test bank MID already configured in the portal. Please note that transactions that have been blocked by the gateway due to invalid format or missing data will not be recorded and will not show up in merchant-facing reports on the Portal. ## Basic Gateway Test Scenarios Use the table below as a reference for basic gateway testing scenarios and for testing your gateway response parser. | Case | Description / Action | Expected Result | |---|---|---| | INVALID DATA | Send "1234" in `PMT_EXPIRY` | API_ADVICE: Invalid Data | | REQUIRED FIELD | Send null in `SITE_ID` | API_ADVICE: Required field \| REF_FIELD: SITE_ID | | INVALID MERCH_ACCT_ID | Send "9999" in `MERCH_ACCT_FIELD` | SERVICE_ADVICE: No merchant account configured | | APPROVED AUTHORIZATION | Send `CCAUTHORIZE` with valid data | TRANS_STATUS_NAME: APPROVED | | CAPTURE AUTHORIZATION | Send `CCCAPTURE` with required fields | TRANS_STATUS_NAME: APPROVED | | SALE (AUTH + CAPTURE) | Send `CCAUTHCAP` with valid data | TRANS_STATUS_NAME: APPROVED | | EXPIRED CARD | Send "5.02" in `LI_VALUE_1` | SERVICE_ADVICE: Expired Card | | FAILED CVV | Send "5.03" in `LI_VALUE_1` | SERVICE_ADVICE: Failed CVV | | FAILED AVS | Send "5.04" in `LI_VALUE_1` | SERVICE_ADVICE: Failed AVS | | BANK DECLINED | Send "5.05" in `LI_VALUE_1` | SERVICE_ADVICE: Declined | | FRAUD | Send "5.06" in `LI_VALUE_1` | SERVICE_ADVICE: Fraud | | OVER LIMIT | Send "5.07" in `LI_VALUE_1` | PROCESSOR_ADVICE: Overlimit | | AVS NO MATCH | Send "5.08" in `LI_VALUE_1` | AVS_RESPONSE: N | | MISSING REQUIRED FIELD | Send "5.18" in `LI_VALUE_1` | PROCESSOR_ADVICE: Missing Required Field | | PROCESSOR UNAVAILABLE | Send "5.30" in `LI_VALUE_1` | PROCESSOR_ADVICE: Downstream Processor Unavail | | GENERAL DECLINE | Send "6.00" in `LI_VALUE_1` | PROCESSOR_ADVICE: General Decline | | STOLEN CARD | Send "6.05" in `LI_VALUE_1` | PROCESSOR_ADVICE: Stolen Card | | PICKUP CARD | Send "6.10" in `LI_VALUE_1` | PROCESSOR_ADVICE: Pickup Card | | INVALID CVV | Send "6.20" in `LI_VALUE_1` | PROCESSOR_ADVICE: Invalid CVV | | EXPIRED CARD (ALT) | Send "6.24" in `LI_VALUE_1` | PROCESSOR_ADVICE: Expired Card | | TIMEOUT VOID | Send "6.26" in `LI_VALUE_1` | SERVICE_ADVICE: Voided per Timeout Void settings | | INSUFFICIENT FUNDS | Send "6.35" in `LI_VALUE_1` | PROCESSOR_ADVICE: Insufficient Funds | | PARTIAL AUTH TEST | Send `PARTIAL_AUTH=1`, `PARTIAL_AUTH_MIN=5.00`, `LI_VALUE_1=6.60` | TRANS_STATUS_NAME: APPROVED \| TRANS_VALUE: 5.5 | | CONVERT CURRENCY | Send `REQUEST_CURRENCY=EUR` and `LI_VALUE_1=7.00` | TRANS_VALUE: 7 \| TRANS_VALUE_SETTLED: 5.686415 | | CVV NO MATCH | Send "7.25" in `LI_VALUE_1` | CVV_RESPONSE: N | | R1 REVOCATION (INDUSTRY) | Send "8.81" in `LI_VALUE_1` | INDUSTRY_RESPONSE: R1 | | R3 REVOCATION (INDUSTRY) | Send "8.83" in `LI_VALUE_1` | INDUSTRY_RESPONSE: R3 | | R1 REVOCATION (PROCESSOR) | Send "9.91" in `LI_VALUE_1` | PROCESSOR_RESPONSE: R1 | | R3 REVOCATION (PROCESSOR) | Send "9.93" in `LI_VALUE_1` | PROCESSOR_RESPONSE: R3 | ## Triggering a Decline in Code The `LI_VALUE_1` amounts in the table above are live triggers, not documentation placeholders: send `5.05` as the line item value against a test card and the gateway declines it every time, with `SERVICE_ADVICE: Declined`. Use it to exercise your decline-handling path without waiting on a real bank response. **cURL** ```bash curl -X POST "https://api.inoviopay.com/payment/pmt_service.cfm" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "request_action=CCAUTHCAP&req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&request_response_format=JSON&pmt_numb=4111111111111111&pmt_expiry=122026&pmt_key=123&request_currency=USD&li_value_1=5.05&xtl_order_id=TEST-DECLINE-001" ``` **PHP** ```php use Inovio\Gateway\{Credentials, InovioClient}; use Inovio\Gateway\Model\{LineItem, Money, PaymentMethods}; use Inovio\Gateway\Request\TransactionRequest; $client = new InovioClient(new Credentials('api_user', 'P@ssw0rd!', '12345'), 'SANDBOX'); // 5.05 is a live decline trigger in the test bank, not a placeholder amount. $req = (new TransactionRequest( PaymentMethods::card('4111111111111111', '122026', '123'), [new LineItem('SKU-TEST', 1, Money::of('5.05', 'USD'))] ))->withIdempotency('TEST-DECLINE-001'); $result = $client->sale($req); echo $result->status, "\n"; // DECLINED — this is not an exception if ($result->status === 'DECLINED') { echo $result->outcome->service->code, ' ', $result->outcome->service->advice, "\n"; } ``` **Node** ```ts import { InovioClient, Money, PaymentMethods } from '@inovio/gateway-sdk'; const client = new InovioClient( { reqUsername: 'api_user', reqPassword: 'P@ssw0rd!', siteId: '12345' }, { environment: 'SANDBOX' } ); // 5.05 is a live decline trigger in the test bank, not a placeholder amount. const result = await client.sale({ paymentMethod: PaymentMethods.card('4111111111111111', '122026', '123'), lineItems: [{ productId: 'SKU-TEST', count: 1, value: Money.of('5.05', 'USD') }], idempotency: { xtlOrderId: 'TEST-DECLINE-001' }, }); console.log(result.status); // DECLINED — this is not a thrown error if (result.status === 'DECLINED') { console.log(result.outcome.service.code, result.outcome.service.advice); } ``` **Python** ```python from inovio_gateway import Credentials, InovioClient, Money, PaymentMethods, TransactionStatus from inovio_gateway.model import Idempotency, LineItem from inovio_gateway.request import TransactionRequest client = InovioClient(Credentials("api_user", "P@ssw0rd!", "12345"), environment="SANDBOX") # 5.05 is a live decline trigger in the test bank, not a placeholder amount. req = TransactionRequest( payment_method=PaymentMethods.card("4111111111111111", "122026", "123"), line_items=[LineItem("SKU-TEST", 1, Money.of("5.05", "USD"))], idempotency=Idempotency(xtl_order_id="TEST-DECLINE-001"), ) result = client.sale(req) print(result.status.value) # DECLINED — this is not a raised exception if result.status is TransactionStatus.DECLINED: print(result.outcome.service.code, result.outcome.service.advice) ``` **Java** ```java // 5.05 is a live decline trigger in the test bank, not a placeholder amount. TransactionRequest req = new TransactionRequest( PaymentMethods.card("4111111111111111", "122026", "123"), new LineItem("SKU-TEST", 1, Money.of("5.05", "USD"))) .idempotency("TEST-DECLINE-001"); TransactionResult result = client.sale(req); System.out.println(result.status()); // DECLINED — this is not a thrown exception if (result.status() == TransactionStatus.DECLINED) { System.out.println(result.outcome().service().code() + " " + result.outcome().service().advice()); } ``` **Response** ```json { "REQUEST_ACTION": "CCAUTHCAP", "TRANS_STATUS_NAME": "DECLINED", "TRANS_VALUE": 5.05, "CURR_CODE_ALPHA": "USD", "PO_ID": 18103700, "XTL_ORDER_ID": "TEST-DECLINE-001", "TRANS_ID": 3948572200, "API_RESPONSE": "0", "SERVICE_RESPONSE": 515, "SERVICE_ADVICE": "Declined", "PROCESSOR_ADVICE": "General Decline", "CARD_BRAND_NAME": "Visa", "PMT_L4": "1111", "REQUEST_API_VERSION": "4.14" } ``` ## Test Credit Cards > **Note** > Use the credit cards below with any address, expiration date, and CVV2 data. | Network | Credit Card Number | |---|---| | MASTERCARD | `5105105105105100` | | MASTERCARD | `5555555555554444` | | MASTERCARD | `5546989999990033` | | VISA | `4111111111111111` | | VISA | `4907639999990022` | | AMERICAN EXPRESS | `378282246310005` | | DINERS CLUB | `38520000023237` | | DINERS CLUB | `30569309025904` | | DISCOVER | `6011111111111117` | | DISCOVER | `6011000990139424` | | JCB | `3530111333300000` | | JCB | `3566002020360505` | --- # Order Detail API ReST-style reporting endpoint for retrieving granular transaction, settlement, and chargeback data by date range or transaction ID. Source: https://developer.inoviopay.com/api/order-detail.html Markdown: https://developer.inoviopay.com/api/order-detail.md The Order Detail API is a ReST-style service used to retrieve granular transaction data, settlement details, and chargeback information. Unlike the real-time Payment API, this endpoint is optimized for data reconciliation and reporting. **GET** `https://api.inoviopay.com/payment/prtl_service.cfm` ### Query Filtering The `REQUEST_FILTER` parameter is the core of this API. It requires a specific internal format: `OBJECT_NAME:KEY+VALUE|KEY2+VALUE2`. **All filters must be URL-encoded.** You can refer to the list of filters [here](https://developer.inoviopay.com/reference/reporting-filters.md#reporting-filter-parameters). | Filter Key | Description | |---|---| | FROM_TIMESTAMP | Start date (YYYY-MM-DD). **Required.** | | TO_TIMESTAMP | End date (YYYY-MM-DD). **Required.** | | REF_DATE | Search by `AUTH` (default), `SETTLE`, or `UPDATE` date. | | SITE_ID | Filter by a specific website ID. | | TRANS_ID | Retrieve a specific transaction by its ID. | > **30-Day Limit** > Standard queries are limited to a 30-day date range. For high-volume merchants needing up to 100,000 records, utilize **Fast Filter Mode** by adding `FAST_FILTER+1` to your request filter. ### Important Response Fields - `ARN`: The Acquirer Reference Number for tracking card movement. - `CB_AMOUNT / CB_REASON`: Populated only for chargeback events. - `TRANS_STATUS`: Current state (APPROVED, DECLINED, PENDING, SETTLED). - `XTL_UDF01-20`: Your custom pass-through fields. ### Standard report request > **Not in the SDKs yet** > The Order Detail (reporting) API is not implemented in any of the four SDKs. This is a separate endpoint (`prtl_service.cfm`) from the one the SDKs speak (`pmt_service.cfm`), `REQUEST_FILTER` and `FAST_FILTER` appear nowhere in any language's source tree. The closest SDK equivalent is `status(orderRef)` (see [Order Status](https://developer.inoviopay.com/api/status.md)), which returns the net position for one order from `pmt_service.cfm`'s `CCSTATUS` action, not a date-ranged, multi-order report. Use the cURL example directly, or see [the SDKs](https://developer.inoviopay.com/sdks/index.md). ### Fast Filter mode > **Not in the SDKs yet** > Same as above: `FAST_FILTER` is not modeled in any SDK. See [the SDKs](https://developer.inoviopay.com/sdks/index.md). **cURL** ```bash curl -G "https://api.inoviopay.com/payment/prtl_service.cfm" \ -d "req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&request_response_format=JSON&request_filter=FROM_TIMESTAMP%3A2026-08-01%7CTO_TIMESTAMP%3A2026-08-31" ``` **Response** ```json [ { "TRANS_ID": 8839201112, "PO_ID": 77281920, "TRANS_STATUS": "SETTLED", "TRANS_VALUE": 49.99, "ARN": "74008824192010293847561", "XTL_UDF01": "INV-999" }, { "TRANS_ID": 8839201118, "PO_ID": 77281926, "TRANS_STATUS": "APPROVED", "TRANS_VALUE": 19.99, "ARN": "74008824192010293847588", "XTL_UDF01": "INV-1004" } ] ``` **cURL** ```bash curl -G "https://api.inoviopay.com/payment/prtl_service.cfm" \ -d "req_username=api_user&req_password=P%40ssw0rd%21&site_id=12345&request_api_version=4.14&request_response_format=JSON&request_filter=FROM_TIMESTAMP%3A2026-08-01%7CTO_TIMESTAMP%3A2026-08-31%7CFAST_FILTER%2B1" ``` **Response** ```json [ { "TRANS_ID": 8839201112, "PO_ID": 77281920, "TRANS_STATUS": "SETTLED", "TRANS_VALUE": 49.99, "ARN": "74008824192010293847561", "XTL_UDF01": "INV-999" } ] ``` --- # Request Types Valid values for request_action parameter Source: https://developer.inoviopay.com/reference/request-types.html Markdown: https://developer.inoviopay.com/reference/request-types.md ## Service Request Types The following table defines all valid values for the `request_action` parameter. | Request Action | Description | |---|---| | `ACHAUTHCAP` | record. Request a transaction for Electronic Funds Transfer | | `ACHAUTHORIZE` | Authorize/Validate Check without funds transfer | | `ACHREVERSE` | Used for Authorization Capture Reversal | | `ACHCREDIT` | Used for transaction credit requests. | | `APPLEPAYCONFIG` | Instructs the endpoint to provide Apple Pay Configuration | | `CCAUTHORIZE` | Used for sending transaction authorization-only requests. | | `CCCAPTURE` | Used for sending transaction capture previous authorization | | `CCAUTHCAP` | requests. Used for sending transaction “authorization and capture” | | `CCREVERSE` | requests. Used for sending transaction reversal or void requests. Sending this will reverse the original authorization. | | `CCREVERSECAP` | For reversing CCCAPTURE transactions, merchants should use as the request action. | | `CCCREDIT` | “CCCAPTURE” transaction. Used for issuing transaction returns or credits. | | `CCRDR` | Used for RDR Dispute Processing | | `CCRDRDELETE` | Used for RDR Dispute Processing to show a case that has been | | `CCTC40` | removed by the customer/issuer Used for TC40 Alerts Processing | | `CCSTATUS` | Used for checking the status of a previous transaction or order. | | `CCTRANSUPDATE` | Used to add receipts on transaction that was previously run | | `DBTAUTHORIZE` | (approved or declined) Used for preparing Mandate without charging | | `DBTCAPTURE` | Used to charge the Mandate for the submitted amount | | `DBTCREDIT` | Used for SEPA Direct Debit Refund/Credit request | | `DBTDEBIT` | Used for SEPA Direct Debit Pay Immediately 'Pay Now' | | `DBTREVERSE` | Used for canceling existing mandate and end subscription | | `GOOGLEPAYCONFIG` | Instructs the endpoint to provide Google Pay Configuration | | `TESTGW` | Used for testing gateway availability. | | `TESTAUTH` | Used for testing basic authentication. | | `SUB_CANCEL` | Used for requesting cancelation of an active membership record. | | `SUB_UPDATE` | Used for updating the Product ID of an existing membership | | `BOLETOAUTHCAP` | Used for Brazilian Boleto Payment type | | `PIXSALE` | Used for Brazilian Pix Payment type | | `PAGSALE` | Used for Peru’s PagoEfectivo Payment type | --- # Customer Parameters Customer data parameters for transaction requests Source: https://developer.inoviopay.com/reference/customer-parameters.html Markdown: https://developer.inoviopay.com/reference/customer-parameters.md | Field Name | Description | | --- | --- | | BILL_ADDR | Cardholder Billing Street Address | | BILL_ADDR_CITY | Cardholder Billing City | | BILL_ADDR_COUNTRY | Cardholder Billing Country | | BILL_ADDR_STATE | Cardholder Billing State | | BILL_ADDR_ZIP | Cardholder Billing Postal/ZIP code | | CUST_BIRTHDAY | Cardholder’s date of birth.FORMAT: MM-DD-YYYY | | CUST_DLN | Cardholder Driver’s License ID | | CUST_DLN_STATE | Cardholder Driver’s License State (US Customers Only) | | CUST_EMAIL | Cardholder Email AddressNote: Required by some banks. | | CUST_FNAME | Cardholder’s First NameNote: Required by some banks. | | CUST_LNAME | Cardholder’s Last NameNote: Required by some banks. | | CUST_LOGIN | Cardholder’s Login or User Name | | CUST_PASSWORD | Cardholder’s PasswordPassword must be at least 10 characters with 1 number, lower case and upper case letter. | | CUST_PHONE | Cardholder’s Phone Numberexample +1 (123) 456–7890Note: Required by some banks. | | CUST_SSN_L4 | Cardholder’s Last 4 digits of Social Security Number (US Customers Only) | | SHIP_ADDR | Cardholder’s Shipping Street Address | | SHIP_ADDR_CITY | Cardholder’s Shipping City | | SHIP_ADDR_COUNTRY | Cardholder’s Shipping Country | | SHIP_ADDR_STATE | Cardholder’s Shipping State | | SHIP_ADDR_ZIP | Cardholder’s Shipping Postal/ZIP Code | | SITE_ID | Merchant’s Website ID | | XTL_IP | Cardholder’s IP Address | | MBSHP_ID_XTL | External Membership ID | | USER_AGENT_XTL | Software agent responsible for retrieving and facilitating end-user interaction with Web content | --- # Payment and Bank Information Parameters Payment and bank data parameters for transaction requests Source: https://developer.inoviopay.com/reference/payment-parameters.html Markdown: https://developer.inoviopay.com/reference/payment-parameters.md | Field Name | Description | | --- | --- | | PMT_DESCRIPTOR | Bank Dynamic Descriptor | | PMT_DESCRIPTOR_PHONE | Bank Dynamic Customer Support Phone Number(aka City Field) | | PMT_DESCRIPTOR_CITY | Bank Dynamic Customer Support CityApplicable to MasterCard only | | PMT_EXPIRY | Credit Card Expiration Date | | PMT_KEY | Credit Card CVV2 or CVC2 Code | | PMT_NUMB | Credit Card Number | | TOKEN_GUID | Token ID used in place of pmt_numb. See Tokenization. | | PMT_L4 | Last 4 digits of the account or credit card number | | PMT_ID | Payment Unique Identifier | | PMT_ID_XTL | External Payment ID | | REQUEST_CURRENCY | Currency 3-letter CodeSee Currency section for more information. | | MERCH_ACCT_ID | Merchant Account ID | | DEBIT_TYPE | Debit Type used for the transaction. (SEPA, iDEAL, and EPS | --- # Adjustment Parameters Adjustment parameters for transaction requests Source: https://developer.inoviopay.com/reference/adjustment-parameters.html Markdown: https://developer.inoviopay.com/reference/adjustment-parameters.md | Field Name | Description | | --- | --- | | REQUEST_REF_PO_ID | Reference Order ID is used when sending adjustment request against authorizations (i.e. Delayed Capture, Reversal and Credit requests). | | REQUEST_REF_PO_LI_ID | Reference Line Item ID. Use this if multiple line items processing is needed. | | CUST_ID | Customer ID created by the system after sending a successful authorization request. | | REQUEST_REF_PO_ID_XTL | Request Reference XTL_ORDER_ID (Merchant’s Order ID). Used for sending CCSTATUS requests. | | CREDIT_ON_FAIL | Used with reversal (void) requests. If set to 1, the system will automatically attempt a credit when the reversal request failed. | | FORCE_CREDIT | Used for sending Force Credit transactions. Force Credit is a type of transaction where the credit request is sent directly to the settlement file. Merchants do not need to send the REQUEST_REF_PO_ID parameter on this type of request. However, the FORCE_CREDIT parameter is required, together with the full credit card information. *Force Credits are only available to certain Merchant Accounts. | --- # Membership Adjustment Parameters Membership adjustment parameters for transaction requests Source: https://developer.inoviopay.com/reference/membership-parameters.html Markdown: https://developer.inoviopay.com/reference/membership-parameters.md | Field Name | Description | | --- | --- | | REQUEST_REF_MBSHP_ID | Referring Membership ID | | SUB_UPDATE_PROD_ID | New Subscription Product ID:Used for updating the current product ID of a membership record. | | SUB_CANCEL_TYPE | Subscription Cancel Request Type | | SUB_UPDATE_PMT_ID | Used to Update Card used for Membership (tied to specific Membership) | | SUB_UPDATE | Request_Action for any membership update | --- # Merchant Parameters Merchant parameters for transaction requests Source: https://developer.inoviopay.com/reference/merchant-parameters.html Markdown: https://developer.inoviopay.com/reference/merchant-parameters.md | Field Name | Description | | --- | --- | | CHKAVS | Address Verification Service Flag (this is for enabling, ignoring or disabling the AVS option).AVS Check is enabled by default. | | AVSMATCHSET | Used for setting the response codes to check when approving transactions based on the AVS response code.See AVSMatchSet explanation for more details. | | CHKCVV | CVV Check flag (this is for enabling, ignoring, or disabling the Credit Card CVV2 or CVC2 check).CVV check is enabled by default. Some processors may automatically decline transactions that return a negative CVV response code. | | CVVMATCHSET | Used for setting the response codes to check when approving transactions based on the CVV response code.See CVVMATCHSET explanation for more details. | | CUST_BRCPFCNPJ | Individual CPF/Business CPNJ Number – Specific to Brazil | | CONVENIENCE_FEE | Reference Field used to indicate the surcharge fee amount which is included in the amount of the transaction | | REQUEST_AFF_ID | External Affiliate ID | | REQUEST_API_VERSION | Payment Service API Version (4.12) | | REQUEST_LANGUAGE | Language 3-letter Code | | REQUEST_RESPONSE_FORMAT | Service Response Format | | XTL_UDFXX | Merchant’s User Defined Field, with xx having a value from 01 to 20, i.e. xtl_udf01, xtl_udf02, xtl_udf03 | | XTL_ORDER_ID | Merchant’s Order ID | | XTL_CUST_ID | Merchant’s Customer ID | | LI_XTL_PROD_ID_X | Merchant’s Product ID Name. “x” indicates a dynamic number depending on the line items sent in the request. For more information see the Multiple Line Items section. | | REQUEST_INITIATOR | Used in Card on File (COF) transactions. Set this parameter to 'C' for Customer Initiated transactions (CIT). Set this parameter to 'M' for Merchant Initiated transactions (MIT). | | REQUEST_INSTALLMENT | Default: “0”Set this parameter to ‘1” if the transaction is an installment payment. | | PMT_NUMB_COF | Default: “0”Set this parameter to “1” to specify that a stored payment number has been used by the merchant's own system for a COF transaction. | | REQUEST_REBILL | Default: "0"Set this parameter to “1” to specify that the transaction is a Rebill in a subscriptionSet this parameter to “2” to specify that the transaction is the FIRST transaction in a subscription | | REQUEST_XSALE | Request Cross-sale transaction. | | PROC_GUID | Processor GUID/GUWID | | P3DS_RETURN_URL | Deprecated3-D Secure Return URL after verification. | | P3DS_PARAMS | Deprecated3-D Secure Verification Parameters (Payspace accounts with 3DS only)Note: Data returned in this field is URL-encoded. | | P3DS_VERIFICATION_URL | Deprecated3-DS Verification URL where customers should be redirected to for external verification. | | REQUEST_ENROLLMENT | Transforms a request into a 3-D Secure enrollment check request | | P3DS_TRANSID | Transaction ID specific to a 3-D Secure vendor | | P3DS_VERSION | 3DS Version used on transaction (2) | | P3DS_ECI | 3-D Secure ECI value | | P3DS_SCREEN_HEIGHT | Total height of the cardholder's screen in pixels | | P3DS_SCREEN_WIDTH | Total width of the cardholder's screen in pixels | | P3DS_JAVA_ENABLED | A Boolean value (TRUE/FALSE) that represents the ability of the cardholder browser to execute Java | | P3DS_JAVASCRIPT_ENABLED | A Boolean value (TRUE/FALSE) that represents the ability of the cardholder browser to execute JavaScript | | P3DS_BROWSER_HEADER | The exact content of the HTTP accept headers sent from the cardholder's browser. Example:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 | | P3DS_BROWSER_LANGUAGE | Value represents the browser language as defined in IETF BCP47 | | P3DS_BROWSER_COLOR_DEPTH | Value represents the bit depth of the color palette for displaying images, in bits per pixel Possible Values: 1, 4, 8, 15, 16, 24, 32, 48 | | P3DS_BROWSER_TIME_ZONE | Time difference between UTC time and the cardholder browser local time, in minutesNote: Regardless of direction value should be positive. | | P3DS_CHALLENGE_WINDOW | An override field that a merchant can pass in to set the challenge window size to display to the end cardholder. The ACS will reply with content that is formatted appropriately to this window size to allow for the best user experience. The sizes are width x height in pixels of the window displayed in the cardholder browser window. Possible values: 01 - 250x400, 02 - 390x400, 03 - 500x600, 04 - 600x400, 05 - Full page | | REQUEST_PARES | Submitted in a 3-D Secure transaction, a value obtained from an enrollment request | | REQUEST_AFF_ID_SUB | External Sub-affiliate ID | | REQ_LOW_VALUE_SCAEXEMPTION | Used by Merchants to bypass 3DS on low value transactions by requesting Low-Value SCA Exemption | | UNIQUE_XTL_ORDER_ID | Enables External Order ID uniqueness. | | PMT_ID_XTL | External Payment /Credit Card Unique ID | | PROC_UDF01 | Processor Defined Field 1 | | PROC_UDF02 | Processor Defined Field 2 | | PROC_SUCCESS_URL | Redirect URL after successful 3-D Secure authentication process. | | PROC_ERROR_URL | Redirect URL after unsuccessful 3-D Secure authentication process. | | PROC_REDIRECT_URL | 3DS Authentication URL where customers should be redirected to for authentication. | | PROD_NAME | Product Name | | PROD_TYPE | 1- Membership Cancels, 2-Membership Renews | | PROD_REBILL_METRIC | M - MonthD - DayY - Year | | PROD_REBILL_PERIOD | 73090 etc | | TAX_AMT | Used to capture the tax amount charged on a transaction. | | TAX_EXEMPT | A flag to indicate if a business is exempted from paying taxes or not. | | TRANS_TRIAL_REBILL_CUSTOMER_CONSENT | Reports on Customer Consent for rebill | | TRANS_CUSTOMER_RECEIPT | Stores Customer transaction receipt | | MBSHP_ID_XTL | For capturing customer membership id for memberships managed by the merchant. | | TRANS_REBILL_TYPE | Reports if rebill is of either of these types NONE, TRIAL, INITIAL, REBILL | | TPPE_ID | Third-Party Processing Entity; this can be an e-Commerce platform, a CRM, another gateway or any other entity which transmits cardholder data on a merchant’s behalf. | | PMT_WALLET | This field shall be used to hold all the different types of wallets including e-wallet and m-wallet when available | | PMT_WALLET_CRYPTOGRAM | Base64 URL encoded field that’ll hold the payload of the authorized transaction which comes from the Apple Pay or Google Pay session on the payment form hosted by the merchant | | ORIG_CARD_BRAND_TRANSID | This is the incoming card scheme transaction id from the original transaction for the card and the merchant account. | --- # Currency Codes Valid currency response codes Source: https://developer.inoviopay.com/reference/currency-fields.html Markdown: https://developer.inoviopay.com/reference/currency-fields.md | Field Name | Description | | --- | --- | | TRANS_VALUE | Requested transaction amount | | CURR_CODE_ALPHA | Requested Currency (3-letter code) | | TRANS_VALUE_SETTLED | Settled amount after currency conversion | | CURR_CODE_ALPHA_SETTLED | Settled Currency (3-letter code) | | TRANS_EXCH_RATE | Currency Conversion Rate (for more information, contact your gateway support representative) | --- # Reporting Filter Parameters Reporting filter parameters Source: https://developer.inoviopay.com/reference/reporting-filters.html Markdown: https://developer.inoviopay.com/reference/reporting-filters.md | Filter Parameter | Description | | --- | --- | | ADDR_ADDRESS | Filter by customer billing street address. | | ADDR_ZIP | Filter by customer billing postal or zip code. | | APPROVED | Set to 1 to explicitly include approved orders. | | AUTH_AMT | Filter by a specific authorization amount. | | BATCH_ID | Filter by a specific gateway settlement batch number. | | CLIENT_ID_CHILD | Filter results for a specific child client under a master account. | | CUST_EMAIL | Filter by the customer's email address. | | CUST_NAME | Filter by the customer's full name. | | DECLINED | Set to 1 to include declined transactions in the results. | | DESCRIPTOR | Filter by the transaction descriptor sent to the bank. | | FROM_TIMESTAMP | The start date for the report range (YYYY-MM-DD). | | IP | Filter by the customer's originating IP address. | | MBSHP_ID | Filter by a specific Membership or Subscription ID. | | MERCH_ACCT_ID | Filter results for a specific Merchant Account ID (MID). | | OMNI | A universal search key. Matches against any ID field (Transaction, Order, Customer, etc.) or email addresses. Supports partial matches on non-numeric strings. | | ORDER_ID | Filter by the Gateway Purchase Order ID (PO_ID). | | PENDING | Set to 1 to include orders currently in a pending state (common for ACH and eCheck). | | PMT_BIN | Filter by the first 6 digits of the payment card number. | | PMT_ID | Filter by the unique internal Payment ID or Token. | | PMT_LAST4 | Filter by the last four digits of the card or account number. | | PMT_NUMB | Filter by the full credit card number. | | SITE_ID | Filter results for a specific Website ID. | | TO_TIMESTAMP | The end date for the report range (YYYY-MM-DD). | | TRANS_ID | Filter by the unique Gateway Transaction ID. | | USERNAME | Filter by the customer's website access or subscription username. | --- # AVS Response Codes Address Verification Service response codes Source: https://developer.inoviopay.com/reference/avs-codes.html Markdown: https://developer.inoviopay.com/reference/avs-codes.md | Code | Description | Default Behavior | | --- | --- | --- | | A | Street address matches, but 5-digit and 9-digit postal code do not match. | Approve | | B | Street address matches, but postal code not verified. | Approve | | D | Street address and postal code match. Code "M" is equivalent. | Approve | | E | AVS data is invalid or AVS is not allowed for this card type. | Approve | | F | Card member's name does not match, but billing postal code matches. | Approve | | G | Non-U.S. issuing bank does not support AVS. | Approve | | H | Card member's name does not match. Street address and postal code match. | Approve | | I | Address not verified. | Approve | | J | Card member's name, billing address, and postal code match. | Approve | | L | Card member's name and billing postal code match, but billing address does not match. | Approve | | M | Street address and postal code match. Code "D" is equivalent. | Approve | | O | Card member's name and billing address match, but billing postal code does not match. | Approve | | P | Postal code matches, but street address not verified. | Approve | | Q | Card member's name, billing address, and postal code match. | Approve | | R | System unavailable. | Approve | | S | Bank does not support AVS. | Approve | | T | Card member's name does not match, but street address matches. | Approve | | U | Address information unavailable. Returned if the U.S. bank does not support non-U.S. AVS or if the AVS in a U.S. bank is not functioning properly. | Approve | | V | Card member's name, billing address, and billing postal code match. | Approve | | W | Street address does not match, but 9-digit postal code matches. | Approve | | X | Street address and 9-digit postal code match. | Approve | | Y | Street address and 5-digit postal code match. | Approve | | Z | Street address does not match, but 5-digit postal code matches. | Approve | | C | Street address and postal code do not match. | Decline | | K | Card member's name matches but billing address and billing postal code do not match. | Decline | | N | Street address and postal code do not match. | Decline | --- # CVV Response Codes Card Verification Value response codes Source: https://developer.inoviopay.com/reference/cvv-codes.html Markdown: https://developer.inoviopay.com/reference/cvv-codes.md | Code | Description | Default Behavior | | --- | --- | --- | | M | Match | Approve | | P | Not Processed | Approve | | S | Not Supported | Approve | | U | Service Not Available | Approve | | X | No CVC/CVV/CVV2/CID Response Data Available | Approve | | (empty) | No CVC/CVV/CVV2/CID Response Data Available | Approve | | N | No match | Decline | --- # API Response Codes API response codes Source: https://developer.inoviopay.com/reference/api-codes.html Markdown: https://developer.inoviopay.com/reference/api-codes.md | Code | Description | Recommendation | | --- | --- | --- | | `100` | Invalid login information (throttle) | Check your login credentials and try again. If you continue to receive this response, contact Client Support | | `101` | Invalid login information | Check your login credentials and try again. If you continue to receive this response, contact Client Support | | `102` | User not active | These credentials have been disabled. If you think this is an error, contact Client Support | | `103` | Invalid site | The value of SITE_ID does not exist, or it does not match the authentication credentials provided. | | `104` | Invalid service | Check the value of request_action to confirm it is correct. | | `105` | Invalid service action | Check the value of request_action to confirm it is correct. | | `106` | Invalid service object | Check the value of request_object to confirm it is correct. | | `110` | Required field | A required key/value pair has not been included in the request. In the response, check the value of REF_FIELD to see what is missing | | `111` | Invalid length | The length of a value is too short or long. Check the returned value of REF_FIELD to see which field may need editing | | `112` | Not numeric | Numeric data is expected. Confirm the amount sent for LI_VALUE_x, which should only contain numerals and one decimal Something in the request was not | | `113` | Invalid Data | expected. Check the values that were submitted for unusual characters, spaces, or null values where there perhaps should not be | | `115` | Customer not found | If CUST_ID or CUST_ID_XTL was submitted, check these values and try again. If this response has come from a request without these parameters, contact Client Support | | `116` | User MUST change password | User passwords expire every 90 days. This does not apply to API credentials. | | `118` | New password must not match the previous 5 passwords | Try a different password. | | `119` | request_ref_po_id and request_po_li_id mismatch | The order ID and the line item ID do not relate to one another. Check the order information. | | `120` | System Error | Contact Client Support | | `125` | Duplicate Login | This email address, a unique identifier, already exists. | | `130` | Same Product ID found on different line items. | Check the values of LI_PROD_ID_x. Each one should have a unique ID. If the intent is to submit a purchase for multiples of the same product use LI_COUNT_x to indicate the quantity. | | `135` | Duplicate Company Name | This company name is already in the system. If you are certain it doesn't already exist in the system, it could be a company with the same name, but doing business in a different region. Contact Client Support for assistance. | | `136` | Duplicate Site Name | This site name already exists in our system. | | `150` | Product Not Found | The product ID is not valid. It may not exist, or it might be associated with another site. Check | | `152` | Product Type Not Found | The value for PROD_TYPE is not valid. | | `153` | Duplicate XTL product id | This value is already in the system. To confirm and review, the ID can be searched for in our | | `155` | Selected currency not configured | Check the merchant account configuration in the portal. | | `160` | Invalid product amount | Check the value of LI_VALUE_x to confirm it is the intended amount. | | `165` | Currency not supported | Check the merchant account configuration in the portal. The MID's allowed currencies can be configured there. Additionally, check the value of PROCESSOR_RESPONSE in the | | `170` | Duplicate product amount and currency | A product with matching properties already exists within the site. | | `176` | Duplicate product description and language | A product with matching properties already exists within this Site | | `180` | Invalid transaction limit type | The limit type was not recognized. Try using the portal to adjust velocity settings. | | `181` | Invalid limit type | The limit type was not recognized. Try using the portal to adjust velocity settings. | | `183` | Payment Type is required | Confirm that PMT_TYPE has been submitted, and has not been included multiple times. | | `205` | No Permissions on requested object | You may not be able to check and confirm your own user permissions, so it may be necessary for an administrator to check them for you. If | | `210` | Merchant Account not found | you feel this is an error, contact your administrator or Client Support. Verify the value of MERCH_ACCT_ID | | `211` | Currency not found | The expected format is three-character currency code. | | `215` | Invalid Card Brand | Check the card brand submitted. If you are certain it’s correct, contact Client Support | | `410` | Field not supported with wallet payment | Check the value of REF_FIELD in the response to see what incompatible element was | | `411` | REQUEST_CURRENCY mismatch with Cryptogram | The currency in the gateway request needs to match the currency that was packed into the ApplePay cryptogram | | `414` | GooglePay token has expired | | --- # Service Response Codes Service response codes Source: https://developer.inoviopay.com/reference/service-codes.html Markdown: https://developer.inoviopay.com/reference/service-codes.md | Code | Description | Retryable | Terminal | Stop Recurring | | --- | --- | --- | --- | --- | | `100` | User Authorized | No | No | No | | `101` | Service Available | No | No | No | | `102` | Membership Updated | No | No | No | | `150` | Product Not Found | No | No | No | | `152` | Product Type Not Found | No | No | No | | `155` | Selected currency not configured | No | No | No | | `157` | MID has RDR Status OFF | No | No | No | | `190` | Invalid Product Configuration | No | No | No | | `192` | Product Not Active | No | No | No | | `200` | CVV required by processor | No | No | No | | `201` | Country required by processor | No | No | No | | `202` | DOB required by processor | No | No | No | | `203` | SSN required by processor | No | No | No | | `204` | Address required by processor | No | No | No | | `205` | City required by processor | No | No | No | | `206` | State required by processor | No | No | No | | `207` | Postal Code required by processor | No | No | No | | `208` | Phone required by processor | No | No | No | | `209` | IP required by processor | No | No | No | | `210` | CPF required by processor | No | No | No | | `211` | Email required by processor | No | No | No | | `212` | FName required by processor | No | No | No | | `213` | LName required by processor | No | No | No | | `215` | Activity limit exceeded | No | No | No | | `216` | Invalid amount | No | No | No | | `217` | No such issuer | No | No | No | | `218` | Wrong PIN entered | No | No | No | | `219` | R0: Stop recurring payments | No | No | Yes | | `220` | R1: Stop recurring payments | No | No | Yes | | `221` | System malfunction | No | No | No | | `500` | No merchant account configured | No | No | No | | `501` | Customer not found | No | No | No | | `502` | Transaction error | No | No | No | | `503` | Service Unavailable | No | No | No | | `505` | Order adjusted to zero | No | No | No | | `506` | Capture amount exceeds order value | No | No | No | | `507` | Order fully captured | No | No | No | | `510` | Order already reversed | No | No | No | | `511` | Order already charged back | No | No | No | | `512` | Order not found | No | No | No | | `515` | Order fully credited | No | No | No | | `516` | Credit amount exceeds order value | No | No | No | | `518` | Missing required field | No | No | No | | `520` | Unsupported Currency | No | No | No | | `522` | Unsupported card brand | No | No | No | | `525` | Batch Closed: Please credit | No | No | No | | `526` | ApplePay is not supported on this merch_acct_id | No | No | No | | `527` | No ApplePay merch_acct_id configured | No | No | No | | `528` | ApplePay MCC Restricted | No | No | No | | `530` | Downstream Processor Unavailable | No | No | No | | `536` | Order not settled: Please reverse | No | No | No | | `540` | Maximum Auth Limit Exceeded | No | No | No | | `546` | GooglePay MCC Restricted | No | No | No | | `547` | No GooglePay merch_acct_id configured | No | No | No | | `548` | GooglePay is not supported on this merch_acct_id | No | No | No | | `555` | Call Center | No | No | No | | `560` | Invalid Service Action | No | No | No | | `564` | Invalid Terminal | No | No | No | | `565` | Invalid Amount | No | No | No | | `570` | Invalid Card Type | No | No | No | | `580` | Unsupported Request | No | No | No | | `600` | Declined | No | No | No | | `601` | Scrub Decline | No | No | No | | `603` | Fraud | No | No | No | | `605` | Stolen Card | No | No | No | | `610` | Pickup Card | No | No | No | | `615` | Lost Card | No | No | No | | `620` | Invalid CVV | No | No | No | | `621` | Failed CVV | No | No | No | | `622` | Invalid AVS | No | No | No | | `623` | Failed AVS | No | No | No | | `624` | Expired Card | No | No | No | | `625` | Excessive Use | No | No | No | | `630` | Invalid Card Number | No | No | No | | `635` | Insufficient Funds | No | No | No | | `640` | Retry | No | No | No | | `650` | Do Not Honor | No | No | No | | `660` | Partial Approval | No | No | No | | `670` | Additional Authentication Required | No | No | No | | `675` | Invalid Card Number, failed Mod 10 validation | No | No | No | | `680` | Duplicate Transaction Detected | No | No | No | | `685` | Duplicate Order Detected | No | No | No | | `690` | Active Membership Exists | No | No | No | | `692` | Invalid Rebill Product | No | No | No | | `695` | Site Username Unavailable | No | No | No | | `697` | Membership Not Active | No | No | No | | `698` | Membership Not Found | No | No | No | | `699` | Membership Not Set for Rebill | No | No | No | | `700` | Scrub Decline | No | No | No | | `706` | Failed Age Validation | No | No | No | | `707` | Invalid CPF | No | No | No | --- # Transaction Response Fields Fields returned in payment transaction responses Source: https://developer.inoviopay.com/reference/transaction-fields.html Markdown: https://developer.inoviopay.com/reference/transaction-fields.md ## Transaction Response The following table defines all fields that may be returned in a transaction response. | Field Name | Description | |----------|-----------| | REQUEST_ACTION | This will return the Service Request Action the merchant sent in the transaction request. | | TRANS_STATUS_NAME | Transaction Status | | TRANS_VALUE | Total requested transaction amount for all line items. | | CURR_CODE_ALPHA | Requested Currency 3-letter Code | | TRANS_VALUE_SETTLED | Transaction Settled Amount (after conversion to settled currency). | | CURR_CODE_ALPHA_SETTLED | Settled Currency 3-letter Code | | TRANS_EXCH_RATE | Currency Exchange Rate | | TRANS_ID | Transaction ID | | CUST_ID | Customer ID | | XTL_CUST_ID | Merchant’s Customer ID | | PO_ID | Purchase order ID | | XTL_PO_ID | Merchant’s Order ID | | BATCH_ID | Settlement Batch ID | | PROC_NAME | Merchant Processor Name (Example: “EPX”) | | MERCH_ACCT_ID | Merchant Bank’s Account ID | | CARD_BRAND_NAME | Credit Card Network/Brand Name | | CARD_DETAIL | Credit or Debit Card | | CARD_TYPE | Credit Card Type | | CARD_CLASS | Categorizes the BIN as a Business, Corporate, Purchase, or Consumer card | | CARD_COUNTRY | Issuer Bank country for the BIN | | CARD_PREPAID | Indicates that the credit card is a prepaid card if value returned is “1”. | | CARD_BANK | Credit Card Issuing Bank Name | | CARD_BALANCE | Prepaid card balance (this is a processor-specific feature). This field will return the card’s available balance. | | PMT_L4 | Payment account or credit card’s last 4 digits. | | PMT_ID | Payment Unique Identifier | | PMT_ID_XTL | External Unique Identifier | | PROC_UDF01 | Processor User Defined Field 1 | | PROC_UDF02 | Processor User Defined Field 2 | | PROC_AUTH_RESPONSE | Processor Authorization Response Code | | PROC_RETRIEVAL_NUM | Processor Retrieval Number or GUID | | PROC_REFERENCE_NUM | Processor Reference Number | | PROC_REDIRECT_URL | URL where customers are redirected to for external verification (i.e. 3D Secure page) | | AVS_RESPONSE | Address Verification Service Response Code | | CVV_RESPONSE | Card Verification Value Response Code | | REQUEST_API_VERSION | Payment Service API Version | | PO_LI_ID_X | Purchase Order Line Item ID | | PO_LI_COUNT_X | Purchase Order Line Item Count | | PO_LI_AMOUNT_X | Purchase Order Line Item Total Amount | | PO_LI_PROD_ID_X | Purchase Order Line Item Product ID | | MBSHP_ID | Membership ID (returned on membership transactions) | | TRANS_NTOKEN_USED | Used to indicate on whether a Network Token was used or a PAN was used. (It will be set to “1” if a scheme token was used or set to “0” otherwise) | | CARD_BRAND_TRANSID | This is the card scheme transaction id for the current transaction | --- # SDKs Server-side SDKs for the Inovio gateway in PHP, Node, Python and Java, covering the v1 card surface against API v4.14. Source: https://developer.inoviopay.com/sdks/index.html Markdown: https://developer.inoviopay.com/sdks/index.md Four server-side libraries wrap the Inovio Gateway Payments Service so you call `client.sale()` instead of assembling `REQUEST_ACTION=CCAUTHCAP` form fields by hand. They are language-idiomatic projections of one object model: a partner reading the Node docs recognises the PHP shape one for one. All four target **API version 4.14** and cover the **v1 card surface**: sale, authorize, capture, line-item capture, reverse, reverse capture, refund, force credit, status, order update, tokenize, and the two health checks. Payment methods are `Card`, `Token` and `SavedCard`. ACH, EU direct debit, LatAm vouchers, wallets, subscriptions and disputes are modelled in the type system but not implemented, so adding them later fills existing seams rather than breaking your integration. > **The SDKs are alpha** > Version 0.1.0-alpha, and **not published to any package registry**. Install > from the public GitHub repository, as shown below. Pin a commit until a > tagged release lands. ## What the SDKs are for Every one of them is a **server-side** library. They hold your gateway credentials, sign token-service requests with your site key, and speak the gateway's form-encoded protocol. None of them runs in a browser. What you get over raw HTTP: - **Actions become methods.** `client.refund(orderRef, amount)`, not a `REQUEST_ACTION` string plus six correlated parameters. - **The five-state result.** `APPROVED`, `DECLINED`, `PENDING`, `RUNNING`, `FAILED`, with no `approved` boolean to make `PENDING` look like a failure. - **Decimal money.** Amounts never pass through a binary float in any of the four languages. - **Typed references.** `capture()` takes an `OrderRef`, so it cannot be handed a customer id by mistake. - **Idempotency and timeout recovery.** Setting an order id makes a retry return the original result instead of charging twice, and the timeout exception carries the key you need to reconcile. - **Wire quirks normalised once.** The `REQUEST_INITATOR` misspelling, the `XTL_ORDER_ID` and `XTL_PO_ID` duality, `PMT_L4` versus `PMT_LAST4`, and the case-inconsistent response keys never reach you. See [How the SDKs think](https://developer.inoviopay.com/sdks/concepts.md) for the object model these share. ## Choosing a language | | PHP | Node / TypeScript | Python | Java | |---|---|---|---|---| | Minimum runtime | PHP 8.0 | Node 18 | Python 3.8 | Java 11 | | Extra dependencies | none (Composer) | none | none | none | | Required extensions | `ext-json`, `ext-bcmath`, `ext-curl` | n/a | n/a | n/a | | HTTP client | cURL, injectable | `fetch`, injectable | `urllib`, injectable | `java.net.http`, injectable | | Amount type | bcmath decimal string | decimal string | `decimal.Decimal` | `BigDecimal` | | Concurrency | synchronous | promise-based | synchronous | synchronous | | Package name | `inovio/gateway-sdk` | `@inovio/gateway-sdk` | `inovio-gateway-sdk` | `com.inoviopay:inovio-gateway-sdk` | | Version | 0.1.0-alpha | 0.1.0-alpha | 0.1.0-alpha | 0.1.0-alpha | | Registry status | not on Packagist | not on npm | not on PyPI | not on Maven Central | | 3D Secure | full server legs | `BrowserData` only | `BrowserData` only | `BrowserData` only | | Repository | [inovio-gateway-sdk-php](https://github.com/Inoviopay/inovio-gateway-sdk-php) | [inovio-gateway-sdk-node](https://github.com/Inoviopay/inovio-gateway-sdk-node) | [inovio-gateway-sdk-python](https://github.com/Inoviopay/inovio-gateway-sdk-python) | [inovio-gateway-sdk-java](https://github.com/Inoviopay/inovio-gateway-sdk-java) | | Page | [PHP](https://developer.inoviopay.com/sdks/php.md) | [Node](https://developer.inoviopay.com/sdks/node.md) | [Python](https://developer.inoviopay.com/sdks/python.md) | [Java](https://developer.inoviopay.com/sdks/java.md) | The 3D Secure row is the one real capability difference. PHP ships the full server-leg client (`prepare()`, the enrollment leg, `completeSale()`); the other three carry the `BrowserData` block and read the challenge `nextAction` from a result, but do not yet expose a `threeDSecure()` sub-client. If you need gateway 3DS today, use PHP or drive [the 3DS endpoints](https://developer.inoviopay.com/api/3ds-gateway.md) directly. Node is the reference implementation. It defined the canonical method surface and the conformance fixtures the other three run against, so where the four disagree, Node is the intended shape. ## Install and first transaction Each SDK installs from its public GitHub repository. Below is the install command followed by a sale, in each language. The gateway endpoint defaults to the sandbox (`https://api-uap.inoviopay.com/payment/pmt_service.cfm`). Pass the `PRODUCTION` environment to switch to `https://api.inoviopay.com/payment/pmt_service.cfm`, or override the endpoint outright to point at a local stack or a proxy. **PHP** ```php # composer.json: add the repository, then require the package composer config repositories.inovio vcs https://github.com/Inoviopay/inovio-gateway-sdk-php composer require inovio/gateway-sdk:dev-main use Inovio\Gateway\{Credentials, InovioClient}; use Inovio\Gateway\Model\{LineItem, Money, PaymentMethods}; use Inovio\Gateway\Request\TransactionRequest; $client = new InovioClient(new Credentials($user, $password, '123'), 'SANDBOX'); $req = (new TransactionRequest( PaymentMethods::card('4111111111111111', '122030', '123'), [new LineItem('SKU-1', 1, Money::of('10.00', 'USD'))] ))->withIdempotency('ORDER-555'); // retry-safe by default $result = $client->sale($req); match ($result->status) { 'APPROVED' => /* fulfil */, 'DECLINED' => /* $result->outcome->service, $result->serviceClassification */, 'PENDING' => /* $result->nextAction — 3DS challenge, redirect, voucher */, default => /* RUNNING | FAILED */, }; ``` **Node** ```ts npm install github:Inoviopay/inovio-gateway-sdk-node import { InovioClient, Money, PaymentMethods, Refs } from '@inovio/gateway-sdk'; const client = new InovioClient( { reqUsername: process.env.INOVIO_USER!, reqPassword: process.env.INOVIO_PASS!, siteId: '123' }, { environment: 'SANDBOX' } ); const result = await client.sale({ paymentMethod: PaymentMethods.card('4111111111111111', '122030', '123'), lineItems: [{ productId: 'SKU-1', count: 1, value: Money.of('10.00', 'USD') }], idempotency: { xtlOrderId: 'ORDER-555' }, // retry-safe by default }); switch (result.status) { case 'APPROVED': /* fulfil */ break; case 'DECLINED': /* result.outcome.service, result.serviceClassification */ break; case 'PENDING': /* result.nextAction — 3DS challenge, redirect, voucher */ break; case 'RUNNING': case 'FAILED': break; } ``` **Python** ```python pip install "git+https://github.com/Inoviopay/inovio-gateway-sdk-python.git@main" from inovio_gateway import ( Credentials, InovioClient, Money, PaymentMethods, TransactionStatus, ) from inovio_gateway.model import Idempotency, LineItem from inovio_gateway.request import TransactionRequest client = InovioClient(Credentials(user, password, site_id="123"), environment="SANDBOX") result = client.sale(TransactionRequest( payment_method=PaymentMethods.card("4111111111111111", "122030", "123"), line_items=[LineItem("SKU-1", 1, Money.of("10.00", "USD"))], idempotency=Idempotency(xtl_order_id="ORDER-555"), # retry-safe by default )) if result.status is TransactionStatus.APPROVED: ... elif result.status is TransactionStatus.PENDING: result.next_action # 3DS challenge, redirect, voucher ``` **Java** ```java git clone https://github.com/Inoviopay/inovio-gateway-sdk-java.git cd inovio-gateway-sdk-java && mvn install # then depend on com.inoviopay:inovio-gateway-sdk:0.1.0-alpha.0 InovioClient client = new InovioClient( new InovioClient.Credentials(user, password, "123")); TransactionRequest req = new TransactionRequest( PaymentMethods.card("4111111111111111", "122030", "123"), new LineItem("SKU-1", 1, Money.of("10.00", "USD"))) .idempotency("ORDER-555"); // retry-safe by default TransactionResult r = client.sale(req); switch (r.status()) { case APPROVED: /* fulfil */ break; case DECLINED: /* r.outcome().service(), r.serviceClassification() */ break; case PENDING: /* r.nextAction() — 3DS challenge, redirect, voucher */ break; case RUNNING: case FAILED: break; } ``` ## Where the card number goes Every SDK's `tokenize()` is a **server-side** call. It POSTs the PAN to `token_service.cfm` from your process, which means the card number transits your infrastructure and your server sits inside your own cardholder data flow. That is a deliberate property of this surface, not an oversight. The lower-scope alternative is a browser client that tokenizes the PAN without it ever reaching your server. **That Hosted Fields client is not yet available.** Until it ships, your options are: - **Server-side `tokenize()`**, accepting that the PAN passes through your server. See [Tokenization](https://developer.inoviopay.com/api/tokenization.md) for the endpoint contract. - **Browser direct-post to `token_service.cfm`.** The token service sends `Access-Control-Allow-Origin: *`, so a browser can post the PAN to it directly once your server has supplied an HMAC signature. This is what the [cart plugins](https://developer.inoviopay.com/carts/index.md) do: the shopper's browser exchanges the PAN for a `TOKEN_GUID`, and only the token reaches the store's PHP. All four SDKs export the signing helper that a merchant-hosted signature endpoint needs. The site key that signs a token request is a per-site HMAC secret issued by Inovio support. It is not your gateway password, and it must never be shipped to a browser. ## What is next - [How the SDKs think](https://developer.inoviopay.com/sdks/concepts.md) walks the shared object model: the status lifecycle, decimal money, idempotency, outcome tiers, order-level reconciliation, tokenization and 3D Secure. - The per-language pages give the full method surface, the language-specific ergonomics, and the runnable examples in each repository. - [Shopping carts](https://developer.inoviopay.com/carts/index.md) covers the pre-built store plugins, which are a different integration path from calling an SDK yourself. --- # How the SDKs think The object model shared by all four Inovio SDKs, covering the status lifecycle, decimal money, idempotency, outcome tiers, reconciliation, tokenization and 3D Secure. Source: https://developer.inoviopay.com/sdks/concepts.html Markdown: https://developer.inoviopay.com/sdks/concepts.md The four SDKs are projections of one object model. The names change to match each language, but the shapes, the invariants and the surprises are identical. Learn them once and the other three read as translations. > **The SDKs are alpha** > Version 0.1.0-alpha, not published to a package registry. Install from the > public GitHub repositories described on the [SDKs overview](https://developer.inoviopay.com/sdks/index.md). ## Actions are methods The gateway protocol is a single form-encoded endpoint discriminated by `REQUEST_ACTION`. The SDKs never ask you to write one. Each action is a method whose parameters are the ones that action actually consumes. | Method | `REQUEST_ACTION` | Purpose | |---|---|---| | `sale` | `CCAUTHCAP` | Authorize and capture in one step. See [Sale](https://developer.inoviopay.com/api/sale.md). | | `authorize` | `CCAUTHORIZE` | Hold funds; capture later. See [Authorize](https://developer.inoviopay.com/api/authorize.md). | | `capture` | `CCCAPTURE` | Capture an authorization, in full or in part. See [Capture](https://developer.inoviopay.com/api/capture.md). | | `captureLineItem` | `CCCAPTURE` | Capture one line item. Needs the parent order, the item, and an amount. | | `reverse` | `CCREVERSE` | Void the original authorization. See [Reversal](https://developer.inoviopay.com/api/reversal.md). | | `reverseCapture` | `CCREVERSECAP` | Void a capture rather than the original auth. | | `refund` | `CCCREDIT` | Refund against an existing order, in full or in part. See [Credit](https://developer.inoviopay.com/api/credit.md). | | `forceCredit` | `CCCREDIT` + `FORCE_CREDIT` | Credit with no referenced original. Needs MID provisioning. | | `status` | `CCSTATUS` | Order-level net position and unknown-state recovery. See [Status](https://developer.inoviopay.com/api/status.md). | | `updateOrder` | `CCTRANSUPDATE` | Attach receipts to an existing order. | | `tokenize` | token service | Exchange a PAN for a single-use token. See [Tokenization](https://developer.inoviopay.com/api/tokenization.md). | | `testAuth` | `TESTAUTH` | Verify credentials without transacting. | | `testAvailability` | `TESTGW` | Verify gateway availability. Safe to poll. | Method names are camelCase in PHP, Node and Java, and snake_case in Python: `capture_line_item`, `reverse_capture`, `force_credit`, `update_order`, `test_auth`, `test_availability`. The follow-up methods take **typed references**, never bare strings. `capture()` accepts an `OrderRef`, `captureLineItem()` accepts an `OrderRef` plus a `LineItemRef`, and `status()` accepts either an `OrderRef` or an `XtlOrderId`. Every result exposes the references it produced, so chaining authorize into capture is type-safe and a customer id cannot be passed where an order id belongs. ## The five-state result Every transaction method returns a result whose `status` is one of five values, taken from Appendix B of the v4.14 specification: | Status | Meaning | |---|---| | `APPROVED` | The transaction succeeded. | | `DECLINED` | The transaction was refused. You got an answer. | | `PENDING` | A 3DS challenge is outstanding, or an async rail is awaiting settlement. | | `RUNNING` | Gateway processing is incomplete. | | `FAILED` | An EU direct-debit payment did not complete. | Two consequences shape everything else. **A decline is a return value, not an exception.** `sale()` returns normally with `status` set to `DECLINED`, carrying the full outcome tiers, AVS and CVV detail. Exceptions are reserved for cases where you never got a payment answer at all: transport failure, authentication, validation and configuration. Wrap your calls in try/catch for the exceptional cases, and branch on `status` for the payment answer. **There are no `approved` or `declined` booleans.** They were deliberately left out. A boolean invites `if (approved) { ... } else { ... }`, which silently treats `PENDING` as a failure, and `PENDING` is a real, non-failed state. The enum forces you to decide what your integration does about it. `settling` survives as a convenience because it is a genuine grouping (`PENDING` or `RUNNING`), not a one-to-one alias for a status value. Two related fields catch people out: - **`settled` is almost always false at response time.** It is written 0 at authorization and flipped later by batch settlement, except on settle-on-auth processors. It is not a failure signal. - **`conversion` is populated only on real FX.** On a domestic transaction the wire's settled-amount fields are just the auth amount echoed back, so a block that was always present would tell you nothing. The SDKs gate it on a non-null exchange rate and name it for what it reports. ## Money is a decimal Amounts never pass through a binary float. Each SDK uses its language's decimal representation and rejects float input at the boundary rather than silently corrupting an amount, because 0.1 plus 0.2 is not 0.3 in binary floating point and the wire format is a decimal string like `"1.25"`. | Language | Internal type | Accepts | Rejects | |---|---|---|---| | PHP | bcmath decimal string | `string`, `int` | `float` | | Node | decimal `string` | `string` | `number` | | Python | `decimal.Decimal` | `Decimal`, `str`, `int` | `float` | | Java | `BigDecimal` | `String`, `BigDecimal` | `double` | The rounding decision is yours and must be explicit. Java goes furthest: it declares a `double` overload whose only job is to reject floating point with a clear message rather than let the call bind to something implicit. Currency is an ISO-4217 alpha-3 code, validated at construction. Equality compares numerically, so `"1.5"` equals `"1.50"`. **PHP** ```php use Inovio\Gateway\Model\Money; $ok = Money::of('10.00', 'USD'); Money::of(1.25, 'USD'); // InvalidArgumentException: pass "1.25", not 1.25 ``` **Node** ```ts import { Money } from '@inovio/gateway-sdk'; const ok = Money.of('10.00', 'USD'); Money.of(1.25, 'USD'); // TypeError: pass '1.25', not 1.25 ``` **Python** ```python from decimal import Decimal from inovio_gateway import Money ok = Money.of("10.00", "USD") also_ok = Money.of(Decimal("10.00"), "USD") Money.of(1.25, "USD") # TypeError: pass "1.25", not 1.25 ``` **Java** ```java import com.inoviopay.gateway.model.Money; Money ok = Money.of("10.00", "USD"); Money.of(1.25, "USD"); // throws — the double overload exists to reject floats ``` ## Idempotency and timeouts A timeout does not mean the transaction failed. It means the state is **unknown**: the gateway may have approved the charge and lost the response. Retrying blindly can charge the customer twice. Two mechanisms work together to make that safe. **Idempotency.** Set your own order id on the request and the SDK sends it as `XTL_ORDER_ID` with the mode defaulted to `RETURN_ORIGINAL`. A repeat of the same request then returns the original result instead of creating a second charge. The mode is settable to `OFF` or `DECLINE_DUP` if you want the gateway to refuse duplicates outright rather than replay the first answer. **The timeout exception carries the key.** When the transport times out, the SDK raises an exception that carries your order id and a recovery hint. That lets you resolve what actually happened rather than guess. If no order id was set, the exception says so, because without a key there is nothing to look the transaction up by. Note the exception name differs by language, and in two of the four it deliberately avoids shadowing a builtin: | Language | Exception | Key accessor | |---|---|---| | PHP | `GatewayTimeoutException` | `xtlOrderId()` | | Node | `TimeoutError` | `xtlOrderId` | | Python | `InovioTimeoutError` | `xtl_order_id` | | Java | `GatewayTimeoutException` | `xtlOrderId()` | Python's is not called `TimeoutError` because callers routinely catch the builtin of that name, and shadowing it would hide exactly the unknown-state case that needs recovery. Java's is not called `TimeoutException` so it is never confused with `java.util.concurrent.TimeoutException`. The default timeout is 120 seconds in all four SDKs, matching the gateway's own window. **PHP** ```php use Inovio\Gateway\Errors\GatewayTimeoutException; use Inovio\Gateway\Refs\Refs; try { $client->sale($req->withIdempotency('ORDER-555')); } catch (GatewayTimeoutException $e) { error_log($e->recoveryHint()); $actual = $client->status(Refs::xtlOrder($e->xtlOrderId())); // a blind retry here could double-charge } ``` **Node** ```ts try { await client.sale({ ...req, idempotency: { xtlOrderId: 'ORDER-555' } }); } catch (e) { if (e instanceof TimeoutError) { console.warn(e.recoveryHint); const actual = await client.status(Refs.xtlOrder('ORDER-555')); // a blind retry here could double-charge } } ``` **Python** ```python from inovio_gateway import InovioTimeoutError, Refs try: client.sale(req) except InovioTimeoutError as e: print(e.recovery_hint) actual = client.status(Refs.xtl_order(e.xtl_order_id)) # a blind retry here could double-charge ``` **Java** ```java try { client.sale(req.idempotency("ORDER-555")); } catch (GatewayTimeoutException e) { log.warn(e.recoveryHint()); OrderStatus actual = client.status(Refs.xtlOrder(e.xtlOrderId())); // a blind retry here could double-charge } ``` ## Outcome tiers Every response carries four independent response-and-advice tiers, from outermost to innermost. They answer different questions, and collapsing them loses information. | Tier | Wire fields | What it reports | |---|---|---| | API | `API_RESPONSE` / `API_ADVICE`, plus `REF_FIELD` | Gateway request validation: credentials, field format, configuration. Fires before the processor is reached. See [API codes](https://developer.inoviopay.com/reference/api-codes.md). | | Service | `SERVICE_RESPONSE` / `SERVICE_ADVICE` | Gateway transaction outcome and the decline taxonomy. See [Service codes](https://developer.inoviopay.com/reference/service-codes.md). | | Processor | `PROCESSOR_RESPONSE` / `PROCESSOR_ADVICE` | Acquirer or bank level. | | Industry | `INDUSTRY_RESPONSE` / `INDUSTRY_ADVICE` | Issuing-bank level. | Alongside those sit the risk results: `avs` and `cvv`, from Appendices E and F. See [AVS codes](https://developer.inoviopay.com/reference/avs-codes.md) and [CVV codes](https://developer.inoviopay.com/reference/cvv-codes.md). The API tier is the one that becomes an exception. The SDKs map known API error codes onto the exception hierarchy: authentication, validation (carrying the offending `REF_FIELD`), configuration and rate limit. The service tier and below always come back as a result. > **The gateway sends codes. The SDK adds labels.** > A result carries three layers. `outcome` holds the response codes exactly as > the gateway returned them, one entry per tier. `raw` holds every wire field > untouched. Between them sit a few **labels the SDK derives by looking the codes > up in its own tables**. You will branch real business logic on those labels, > so know which fields they are: > > - `serviceClassification.retryable`, `.terminal` and `.stopRecurring` > (`service_classification.stop_recurring` in Python). Derived from the > service response code. Retryable means a later attempt might succeed, for > instance insufficient funds. Terminal means it never will, for instance a > closed account. Stop recurring means the issuer is telling you to end the > subscription. Dunning logic branches on these. > - `avs.classification`, one of `positive`, `partial`, `negative` or > `neutral`. Derived from the AVS code. `partial` means street or postal code > matched but not both. > > Reading a label means trusting the SDK's reading of the code tables. Reading > `outcome` or `raw` means applying your own. **Whether a `partial` AVS result > is acceptable is your risk decision**: the SDK labels it and deliberately > does not approve or reject on your behalf. ## OrderStatus is the reconciliation primitive `status()` is not just the timeout recovery path. It is the only correct source of net figures for any order with more than one leg. In the gateway, a partial capture, a refund and a void are **not** modifications of the original transaction. They are separate transaction rows sharing a `PO_ID`, each with its own transaction id. Net position is therefore an order-level question, and a single `TransactionResult` cannot answer "what did this order actually settle for". `OrderStatus` can: | Field | Derivation | |---|---| | `transactions` | Every leg against the order, in order: auth, captures, refunds, voids. | | `authorized` | The original authorization. | | `captured` | Sum of the capture legs. | | `refunded` | Sum of the refund and void legs. | | `net` | `captured` minus `refunded`. | | `outstanding` | `authorized` minus `captured`, the uncaptured balance. | | `settled` | True once every settle-eligible leg has settled. | The arithmetic mirrors the way the gateway's own settlement batch derives these figures, summing siblings keyed on `PO_ID`. The SDK does that summing so you do not have to. One protocol quirk worth knowing, because it is not described in the v4.14 response-fields section: `CCSTATUS` does not answer with flat fields like every other action. It returns a tabular payload with `COLUMNS` and `DATA` arrays, one `DATA` row per leg. All four SDKs parse that shape internally and hand you typed legs. This was verified against the live gateway. ## Tokenization `tokenize()` exchanges a PAN for a single-use `TOKEN_GUID` that replaces `PMT_NUMB` on a later sale or authorize. It hits a **different endpoint** (`token_service.cfm`) with **different authentication**: HMAC headers rather than username and password. You need a **site key**, a per-site HMAC secret issued by Inovio support. It is not your gateway password. Without it the token service answers error 121. Two things the SDKs handle that the specification will mislead you on. **The signed message excludes the PAN.** The v4.14 PDF's section 4.8.1.2 note says the HMAC covers `card_pan`, and its worked example agrees. The gateway does not. Verified against the live token service, the gateway validates: ``` hmac_sha256(timestamp || unique_id || site_id, site_key) ``` Signing with the card number included fails with error 121. The SDKs sign the way the gateway actually behaves, not the way the document describes. **A token replaces the PAN only.** The transaction still needs the expiry, and the CVV where the processor asks for it. `tokenize()` carries both forward onto the returned token for you. Sending a bare `TOKEN_GUID` yields API 110 `Required field` on `REF_FIELD=pmt_expiry`. BIN metadata on the result (`brand`, `bank`, `country` and friends) is best-effort. The service returns those keys empty when the BIN is not in its lookup table, and the SDKs normalise blanks to null so you can test for presence rather than for an empty string. All four SDKs also export the signing helper on its own, so a merchant-hosted signature endpoint can sign a request that the browser then posts directly to the token service. The helper is `Tokenize::signRequest()` in PHP, `signTokenRequest()` in Node, `sign_token_request()` in Python and `Tokenize.signRequest()` in Java. > **tokenize() is a server-side call** > The card number passes through your server, which puts your server inside > your cardholder data flow. The browser Hosted Fields client that would keep > the PAN in the cardholder's browser is not yet available. See > [Where the card number goes](https://developer.inoviopay.com/sdks/index.md#where-the-card-number-goes). ## 3D Secure Gateway-managed 3DS is a multi-leg flow. The SDK owns every server leg; your page owns two iframes, a hidden one for device-data collection and a visible one for the challenge. 1. **Prepare.** Call `prepare()` against `3dsrequest.cfm`. It returns a JWT, the 3DS provider's collection URL, and a DDC reference id. Your page POSTs the JWT (field name `JWT`) to that URL in a hidden iframe. 2. **Enrollment leg.** A normal `sale()` or `authorize()` carrying a `ThreeDS` block with the DDC reference id and your return URL. **`BrowserData` is required**: without it the gateway silently skips 3DS entirely. An `APPROVED` or `DECLINED` result here means frictionless authentication and you are done. `PENDING` means a challenge is required. 3. **Challenge.** On `PENDING`, `nextAction` carries a redirect URL and a JWT. Your page POSTs that JWT to that URL in a visible iframe. The ACS then POSTs `TRANSACTIONID`, `RESPONSE` and `MD` back to your return URL. 4. **Completion leg.** Pass the challenge outcome to `completeSale()` or `completeAuthorize()`, reusing the same request object that ran the enrollment leg. The `RESPONSE` value **may legitimately be empty**; that is not an error, and it must still be passed through as an empty string. On the completed result, `threeDS.eci` values 05 and 06 mean fully authenticated, which is where liability shift applies. > **Only the PHP SDK implements the 3DS server legs** > `prepare()`, `completeSale()` and `completeAuthorize()` exist only in > `Inovio\Gateway\ThreeDSecureClient`. The Node, Python and Java SDKs carry the > `BrowserData` block on a request and surface a `threeDSChallenge` > `nextAction` when a result is `PENDING`, but they do not yet expose a > `threeDSecure()` sub-client. In those three languages, drive the prepare and > completion legs against [the 3DS endpoints](https://developer.inoviopay.com/api/3ds-gateway.md) directly. Here is the PHP flow end to end: ```php use Inovio\Gateway\ThreeDSPrepare; use Inovio\Gateway\Model\{BrowserData, ThreeDS, ThreeDSChallengeResult}; // 1. Start the session — returns what the DDC iframe needs. $ddc = $client->threeDSecure()->prepare( ThreeDSPrepare::card($card, 'USD', 'US') ); // Browser: POST $ddc->jwt (field name JWT) to $ddc->ddcUrl in a hidden iframe. // 2. Enrollment leg — a normal sale/authorize carrying the ThreeDS block. // BrowserData is REQUIRED: without it the gateway silently skips 3DS. $req->browser = new BrowserData($lang, $userAgent, $acceptHeader, javaEnabled: false, colorDepth: 24, screenHeight: 1080, screenWidth: 1920, timeZoneOffsetMinutes: 480); $req->threeDS = new ThreeDS($ddc->ddcReferenceId, 'https://you.example/3ds-return'); $r = $client->sale($req); match ($r->status) { 'APPROVED', 'DECLINED' => /* frictionless — done, check $r->threeDS->eci */, 'PENDING' => /* challenge: POST $r->nextAction->jwt to $r->nextAction->redirectUrl in a visible iframe */, }; // 3. The ACS POSTs TRANSACTIONID / RESPONSE / MD to your return URL. // RESPONSE may be EMPTY — that is not an error; pass it through as ''. $final = $client->threeDSecure()->completeSale( $req, new ThreeDSChallengeResult($_POST['TransactionId'], $_POST['Response'] ?? '') ); // $final->threeDS?->eci — 05/06 means fully authenticated (liability shift). ``` `prepare()` identifies the card three ways. Use `ThreeDSPrepare::card()` when you hold the PAN, `::savedCard()` for a vaulted card (the gateway looks the BIN up, and requires both `pmtId` and `custId`), and `::bin()` when the browser tokenized the card but you captured the BIN client-side. **Partners running their own 3DS provider skip all of the above.** Attach a `ThreeDSResult` carrying your CAVV, ECI and transaction id to a normal one-leg `sale()`. See [External 3DS](https://developer.inoviopay.com/api/3ds-external.md). This external path is also PHP-only in the current alpha. ## Reverse versus credit A reversal voids a transaction that has not settled. Once a transaction has settled, it cannot be reversed and the money has to come back as a credit instead. Rather than make you detect that yourself, the gateway will do the re-routing for you. Sending `CREDIT_ON_FAIL=1` alongside `CCREVERSE` or `CCREVERSECAP` tells the gateway: if this transaction is already settled and cannot be reversed, re-route the request to `CCCREDIT`. The response then comes back carrying `REQUEST_ACTION=CCCREDIT` instead of the reversal action you sent, which is how you know which path it took. This behaviour is verified against the gateway. > **Only the PHP SDK exposes this flag** > `reverse()` and `reverseCapture()` take `bool $creditOnFail = false` in PHP > only. The Node, Python and Java signatures take just the order reference and > never send `CREDIT_ON_FAIL`. Until they do, call > [the reversal endpoint](https://developer.inoviopay.com/api/reversal.md) directly with the flag set. Do not > substitute a `status()` settlement check followed by a client-side choice > between `reverse()` and `refund()`: the settled flag flips in batch and the > gateway's own routing is the only reliable answer. ```php // PHP: let the gateway decide between void and credit. $r = $client->reverse($order, creditOnFail: true); if ($r->action === 'CCCREDIT') { // it was already settled; the gateway credited instead of voiding } ``` Do not write your own settled-versus-unsettled pre-check and fall back between the two calls. The gateway owns that routing decision and has information you do not. ## What the SDK hides Wire quirks are normalised once, internally, and never reach you: | Wire | SDK | |---|---| | `REQUEST_ACTION=CCAUTHCAP` | `client.sale()` | | `REQUEST_INITATOR` (misspelled in the protocol) | `recurring.initiator` | | `XTL_ORDER_ID` and `XTL_PO_ID` (the same thing) | `xtlOrderRef` | | `PMT_L4` and `PMT_LAST4` (the same thing) | `card.last4` | | `PMT_NUMB` meaning PAN, bank account or IBAN | `PaymentMethod` variants | | `LI_VALUE_1`, `LI_COUNT_1`, and the rest of the indexed set | a list of `LineItem` | | Case-inconsistent response keys | an upper-cased map | | `CCSTATUS` returning `COLUMNS`/`DATA` instead of flat fields | typed `OrderStatus` legs | Every result also carries `raw`, the complete unmodified field map, as an escape hatch for anything the model does not surface. ## Errors All four SDKs share one hierarchy. Only the naming suffix differs: PHP and Java use `Exception`, Node and Python use `Error`. | Exception | Raised for | |---|---| | `InovioException` / `InovioError` | Base type for everything below. | | `AuthenticationException` / `AuthenticationError` | Bad credentials, inactive account, bad site or service. API tier 100 to 106. | | `ValidationException` / `ValidationError` | Missing or invalid input, caught locally before send or reported by the gateway. Carries the offending `refField`. | | `ConfigurationException` / `ConfigurationError` | Currency, product or merchant account not configured. | | `TransportException` / `TransportError` | Network failure. | | `GatewayTimeoutException` / `TimeoutError` / `InovioTimeoutError` | The timeout case. Subclass of the transport error. Carries the idempotency key. | | `RateLimitException` / `RateLimitError` | Throttled. | In Java every one of these is unchecked, so nothing forces a `throws` clause onto your call sites. Remember what is **not** in this list: a decline. That is a `TransactionResult` with `status` set to `DECLINED`, and the decline taxonomy lives on its service tier. One case that surprises people: `forceCredit()` fails with API 104 "Invalid service action" unless the merchant account has `FORCE_CREDIT` enabled. That arrives as an authentication-tier exception, not a decline, because the gateway rejected the request before it ever reached a processor. --- # PHP SDK The Inovio gateway SDK for PHP 8, with no Composer dependencies, an injectable HTTP client, and the full 3D Secure server legs. Source: https://developer.inoviopay.com/sdks/php.html Markdown: https://developer.inoviopay.com/sdks/php.md Repository: https://github.com/Inoviopay/inovio-gateway-sdk-php The Inovio payment gateway for PHP 8. Card transactions covering authorize, capture, refund and tokenize, designed to drop into a WooCommerce, Magento or custom cart. Bring your own HTTP client; there are no Composer dependencies. This is the only one of the four SDKs that ships the complete 3D Secure server legs and the `creditOnFail` reversal flag. > **The SDK is alpha** > Version 0.1.0-alpha and **not published to Packagist**. Install from the > public GitHub repository as shown below, and pin a commit until a tagged > release lands. ## Status and install Add the repository to your `composer.json`, then require the package from the `main` branch: ```bash composer config repositories.inovio vcs https://github.com/Inoviopay/inovio-gateway-sdk-php composer require inovio/gateway-sdk:dev-main ``` Or write it into `composer.json` directly: ```json { "repositories": [ { "type": "vcs", "url": "https://github.com/Inoviopay/inovio-gateway-sdk-php" } ], "require": { "inovio/gateway-sdk": "dev-main" } } ``` The package autoloads via a classmap over `src/`, so it also works if you clone the repository and require the classmap yourself, without Composer at all. ## Requirements PHP **8.0 or newer**, with `ext-json`, `ext-bcmath` and `ext-curl`. No Composer dependencies. `bcmath` is required, not optional. `Money` does its decimal arithmetic through it so that amounts never touch a binary float. ## Quick start ```php use Inovio\Gateway\{Credentials, InovioClient}; use Inovio\Gateway\Model\{LineItem, Money, PaymentMethods}; use Inovio\Gateway\Request\TransactionRequest; $client = new InovioClient(new Credentials($user, $password, '123'), 'SANDBOX'); $req = (new TransactionRequest( PaymentMethods::card('4111111111111111', '122030', '123'), [new LineItem('SKU-1', 1, Money::of('10.00', 'USD'))] ))->withIdempotency('ORDER-555'); // retry-safe by default $result = $client->sale($req); match ($result->status) { 'APPROVED' => /* fulfil */, 'DECLINED' => /* $result->outcome->service, $result->serviceClassification */, 'PENDING' => /* $result->nextAction — 3DS challenge, redirect, voucher */, default => /* RUNNING | FAILED */, }; ``` The constructor takes credentials and an environment, then a long tail of optional arguments. Use named arguments for anything past the environment: ```php $client = new InovioClient( credentials: new Credentials($user, $password, '123'), environment: 'PRODUCTION', httpClient: new MyPsr18Adapter(), timeoutMs: 30000, siteKey: $siteKey, // required only for tokenize() ); ``` ## PHP-specific notes **Injectable HTTP client.** Host platforms usually want their own transport: WordPress `wp_remote_post`, Magento's PSR-18 client, or an instrumented client of your own. Implement `Inovio\Gateway\Transport\HttpClient` and pass it in. The SDK never assumes it owns the socket. ```php $client = new InovioClient($creds, 'PRODUCTION', null, new MyPsr18Adapter()); ``` Throw `Inovio\Gateway\Transport\TimeoutSignal` from your adapter on timeout so the SDK can convert it into a `GatewayTimeoutException` with the idempotency key attached. Without that signal the SDK cannot tell a timeout apart from any other transport failure, and you lose the recovery path. **bcmath money.** `Money` holds the amount as a string and does arithmetic through bcmath at scale 8. `Money::of()` accepts a decimal string or an int, and throws `InvalidArgumentException` on a float. `Money::of(1.25, 'USD')` throws; pass `'1.25'`. **Named arguments over builders.** The model objects use public promoted properties and named arguments rather than fluent builders, so a request is assembled by assignment: ```php $req->customer = new Customer(); $req->customer->email = 'ada@example.invalid'; $req->billingAddress = new Address(); $req->billingAddress->country = 'US'; ``` **Statuses are strings.** `$result->status` is a plain string, one of `APPROVED`, `DECLINED`, `PENDING`, `RUNNING`, `FAILED`, so it works directly in a `match` expression. ## Operations Every public method on `InovioClient`, with the `REQUEST_ACTION` it maps to: | Method | Action | Notes | |---|---|---| | `sale(TransactionRequest $req): TransactionResult` | `CCAUTHCAP` | Authorize and capture in one step. See [Sale](https://developer.inoviopay.com/api/sale.md). | | `authorize(TransactionRequest $req): TransactionResult` | `CCAUTHORIZE` | Hold funds; capture later. See [Authorize](https://developer.inoviopay.com/api/authorize.md). | | `capture(OrderRef $order, ?Money $amount = null): TransactionResult` | `CCCAPTURE` | Omit the amount to capture in full. See [Capture](https://developer.inoviopay.com/api/capture.md). | | `captureLineItem(OrderRef $order, LineItemRef $item, Money $amount): TransactionResult` | `CCCAPTURE` | All three arguments are required. | | `reverse(OrderRef $order, bool $creditOnFail = false): TransactionResult` | `CCREVERSE` | Void the original authorization. See [Reversal](https://developer.inoviopay.com/api/reversal.md). | | `reverseCapture(OrderRef $order, bool $creditOnFail = false): TransactionResult` | `CCREVERSECAP` | Void a capture rather than the original auth. | | `refund(OrderRef $order, ?Money $amount = null): TransactionResult` | `CCCREDIT` | Omit the amount to refund in full. See [Credit](https://developer.inoviopay.com/api/credit.md). | | `forceCredit(TransactionRequest $req): TransactionResult` | `CCCREDIT` + `FORCE_CREDIT` | Credit with no referenced original. Needs MID provisioning. | | `status(OrderRef\|XtlOrderId $ref): OrderStatus` | `CCSTATUS` | Order-level net position. See [Status](https://developer.inoviopay.com/api/status.md). | | `updateOrder(OrderRef $order, OrderUpdate $update): TransactionResult` | `CCTRANSUPDATE` | Attach a receipt and UDF metadata. | | `tokenize(Card $card, ?string $uniqueId = null): TokenizeResult` | token service | Needs `siteKey`. See [Tokenization](https://developer.inoviopay.com/api/tokenization.md). | | `threeDSecure(): ThreeDSecureClient` | 3DS legs | Sub-client, described below. | | `testAuth(): HealthResult` | `TESTAUTH` | Verify credentials without transacting. | | `testAvailability(): HealthResult` | `TESTGW` | Verify gateway availability. Safe to poll. | `captureLineItem()` requires the parent order as well as the line item. The gateway rejects `REQUEST_REF_PO_LI_ID` on its own with API 113 "Invalid Data", and `LineItemRef` does not carry its order, so both must be passed. This was verified against the live gateway. `reverse()` and `reverseCapture()` accept `$creditOnFail`. With it set the SDK sends `CREDIT_ON_FAIL=1`, and if the transaction is already settled and cannot be reversed, the gateway itself re-routes the request to `CCCREDIT`. The response then carries `REQUEST_ACTION=CCCREDIT` instead of the action you sent. **This parameter exists only in the PHP SDK**; see [Reverse versus credit](https://developer.inoviopay.com/sdks/concepts.md#reverse-versus-credit). ## Handling results `$result->status` carries the answer. A decline is a return value, not an exception, so branch on the status rather than wrapping in try/catch for the payment outcome: ```php $r = $client->sale($req); show('status', $r->status); show('order', $r->orderRef?->poId() ?? '-'); show('amount', $r->amount ? $r->amount->toWire() . ' ' . $r->amount->currency() : '-'); show('card', sprintf('%s ****%s', $r->card->brand ?? '?', $r->card->last4 ?? '?')); switch ($r->status) { case 'APPROVED': show('next', 'fulfil the order'); break; case 'DECLINED': // The service tier carries the decline taxonomy your dunning logic needs. show('next', $r->serviceClassification?->retryable ? 'retry later' : 'do not retry'); break; case 'PENDING': show('next', 'complete ' . ($r->nextAction->kind ?? '?')); break; default: show('next', 'inspect $r->outcome'); } ``` Reference keys sit flat on the result, not inside a nested bag, because they are the fields you reach for most: `$client->capture($r->orderRef, $amount)`. The available refs are `orderRef`, `xtlOrderRef`, `transactionId`, `requestId`, `batchId`, `customerRef`, `savedCardRef`, `membershipRef` and `lineItemRefs`. The gateway sends codes; the SDK adds labels. `$r->serviceClassification->retryable`, `->terminal` and `->stopRecurring`, plus `$r->avs->classification`, are labels the SDK derives from the response codes, not values the gateway sent. The codes themselves are on `$r->outcome` and the untouched wire fields on `$r->raw`. See [Outcome tiers](https://developer.inoviopay.com/sdks/concepts.md#outcome-tiers). Exceptions extend `Inovio\Gateway\Errors\InovioException`: `AuthenticationException`, `ValidationException` (carrying the offending `refField`), `ConfigurationException`, `TransportException`, `GatewayTimeoutException` (a subclass of `TransportException`) and `RateLimitException`. ## Tokenization `tokenize()` exchanges a PAN for a single-use `TOKEN_GUID` that replaces `PMT_NUMB` on a later sale or authorize. It hits `token_service.cfm` with HMAC header authentication rather than username and password, so it needs the per-site `siteKey` on the client. Without it the call throws a `ValidationException` before any network traffic, and the service itself would answer error 121. ```php // Tokenize on the site that holds the HMAC key. $t = $tokenClient->tokenize(PaymentMethods::card($pan, $expiry, $cvv)); echo $t->token->guid(); echo $t->tokenReqId; // quote this to support // BIN metadata is best-effort — null when the BIN is not in the lookup table. echo $t->card->brand; // 'Visa' echo $t->card->bank; // The token replaces the PAN only: expiry and CVV still travel with it, which // tokenize() carries forward for you. $req = new TransactionRequest($t->token, [new LineItem('SKU-1', 1, Money::of('10.00', 'USD'))]); $sale = $client->sale($req->withIdempotency('ORDER-556')); ``` The signed message **excludes the PAN**, contrary to what the v4.14 PDF says. The gateway validates `hmac_sha256(timestamp || unique_id || site_id, site_key)`, verified against the live token service. The SDK signs the way the gateway behaves. For a merchant-hosted signature endpoint that lets a browser post the PAN directly, use the signing helpers on their own: ```php use Inovio\Gateway\Tokenize; $timestamp = Tokenize::timestamp(); // YYYYMMDDHHMMSS UTC $signature = Tokenize::signRequest($siteKey, $timestamp, $uniqueId, $siteId); // Return {$signature, $timestamp, $siteId} to the browser. Never the site key. ``` > **tokenize() runs on your server** > The card number passes through your infrastructure. The browser Hosted Fields > client that would keep it in the cardholder's browser is not yet available. > See [Where the card number goes](https://developer.inoviopay.com/sdks/index.md#where-the-card-number-goes). ## 3D Secure The SDK owns every server leg; your page owns two iframes, a hidden one for device-data collection and a visible one for the challenge. ```php use Inovio\Gateway\ThreeDSPrepare; use Inovio\Gateway\Model\{BrowserData, ThreeDS, ThreeDSChallengeResult}; // 1. Start the session — returns what the DDC iframe needs. $ddc = $client->threeDSecure()->prepare( ThreeDSPrepare::card($card, 'USD', 'US') ); // Browser: POST $ddc->jwt (field name JWT) to $ddc->ddcUrl in a hidden iframe. // 2. Enrollment leg — a normal sale/authorize carrying the ThreeDS block. // BrowserData is REQUIRED: without it the gateway silently skips 3DS. $req->browser = new BrowserData($lang, $userAgent, $acceptHeader, javaEnabled: false, colorDepth: 24, screenHeight: 1080, screenWidth: 1920, timeZoneOffsetMinutes: 480); $req->threeDS = new ThreeDS($ddc->ddcReferenceId, 'https://you.example/3ds-return'); $r = $client->sale($req); match ($r->status) { 'APPROVED', 'DECLINED' => /* frictionless — done, check $r->threeDS->eci */, 'PENDING' => /* challenge: POST $r->nextAction->jwt to $r->nextAction->redirectUrl in a visible iframe */, }; // 3. The ACS POSTs TRANSACTIONID / RESPONSE / MD to your return URL. // RESPONSE may be EMPTY — that is not an error; pass it through as ''. $final = $client->threeDSecure()->completeSale( $req, new ThreeDSChallengeResult($_POST['TransactionId'], $_POST['Response'] ?? '') ); // $final->threeDS?->eci — 05/06 means fully authenticated (liability shift). ``` `prepare()` sends no `REQUEST_ACTION`; the 3DS request service derives the action from the username and password auth path. That is verified against the deployed endpoint. Three ways to identify the card on `prepare()`: - `ThreeDSPrepare::card($card, $currency, $country)` when you hold the PAN. - `ThreeDSPrepare::savedCard($saved, $currency, $country)` for a vaulted card. The gateway looks the BIN up, and both `pmtId` and `custId` are required. - `ThreeDSPrepare::bin($bin, $currency, $country)` when the browser tokenized the card and you captured at least the first six digits client-side. Use `completeAuthorize()` instead of `completeSale()` when the enrollment leg was an `authorize()`. Both reuse the same request object that ran the enrollment leg, swapping its `ThreeDS` block for the challenge result. Partners running their own 3DS provider skip all of this and attach `$req->threeDSResult = new ThreeDSResult($cavv, $eci, $transId)` to a normal one-leg `sale()`. See [External 3DS](https://developer.inoviopay.com/api/3ds-external.md). Setting more than one of `threeDS`, `threeDSChallenge` and `threeDSResult` on a request is a validation error, because they are three distinct legs. See [3D Secure](https://developer.inoviopay.com/sdks/concepts.md#3d-secure) for the flow described language-agnostically, and [Gateway 3DS](https://developer.inoviopay.com/api/3ds-gateway.md) for the endpoint contract. ## Timeouts and reconciliation A timeout means the state is **unknown**, not failed. The gateway may have approved the charge and lost the response. ```php use Inovio\Gateway\Errors\GatewayTimeoutException; use Inovio\Gateway\Refs\Refs; try { $client->sale($req->withIdempotency('ORDER-555')); } catch (GatewayTimeoutException $e) { error_log($e->recoveryHint()); $actual = $client->status(Refs::xtlOrder($e->xtlOrderId())); // Do NOT retry blindly — that risks a double charge. } ``` `withIdempotency()` defaults the mode to `RETURN_ORIGINAL`, so a retry of the same request returns the original result rather than charging twice. `status()` is also the reconciliation primitive. Partial captures, refunds and voids are separate transactions sharing one order, so net position is an order-level question: ```php $s = $client->status($order->orderRef); echo count($s->transactions); // every leg: auth, captures, refunds, voids echo $s->authorized?->toWire(); echo $s->captured?->toWire(); echo $s->refunded?->toWire(); echo $s->net?->toWire(); // captured - refunded echo $s->outstanding?->toWire(); // authorized - captured ``` You can also look an order up by your own id with `Refs::xtlOrder('ORDER-555')`. ## Running the conformance tests ```bash php tests/conformance.php # 67 assertions, no Composer needed python3 scripts/generate_enums.py # regenerate enums from spec/spec-enums.json ``` The suite is plain PHP rather than PHPUnit so it runs on a bare interpreter. It replays the shared cross-language fixtures in `spec/conformance-fixtures.json`, the same corpus the other three SDKs run, which is what keeps the four honest about producing identically shaped results. The enums in `src/Enums/Generated.php` are generated from `spec/spec-enums.json`, extracted from the v4.14 specification appendices. Do not hand-edit them; regenerate. ## Examples in the repo Fourteen runnable files in `examples/`, one per operation. They are real, executed code rather than markdown snippets, so they cannot silently drift from the API. ```bash php examples/run_all.php # all 14, against a mock transport php examples/03_sale.php # just one ``` | File | Operation | |---|---| | `01_test_availability.php` | `testAvailability()`, the `TESTGW` health check | | `02_test_auth.php` | `testAuth()`, credential verification with no transaction | | `03_sale.php` | `sale()`, authorize and capture in one step | | `04_authorize.php` | `authorize()`, holding funds and keeping the order ref | | `05_capture.php` | `capture()`, full or partial | | `06_capture_line_item.php` | `captureLineItem()`, which needs order, item and amount | | `07_reverse.php` | `reverse()`, voiding an authorization | | `08_reverse_capture.php` | `reverseCapture()`, voiding a capture pre-settlement | | `09_refund.php` | `refund()`, returning captured funds | | `10_force_credit.php` | `forceCredit()`, which needs MID provisioning | | `11_status.php` | `status()`, reconciliation and net position | | `12_update_order.php` | `updateOrder()`, attaching receipts for compliance | | `13_tokenize.php` | `tokenize()`, PAN to single-use token | | `14_timeout_recovery.php` | The pattern that prevents double charges | By default they run against a mock transport: no credentials, no network, no money moves, safe in CI. Set `INOVIO_LIVE=1` plus credentials to run the same code against a real gateway: ```bash INOVIO_LIVE=1 \ INOVIO_USER=... INOVIO_PASS=... INOVIO_SITE_ID=... INOVIO_MERCH_ACCT_ID=... \ INOVIO_SITE_KEY=... # token service HMAC key INOVIO_TOKEN_SITE_ID=... # only if tokenizing on a different site php examples/run_all.php ``` Live mode creates **real transactions**, so point it at a test environment. Running live is worth doing before you trust an integration: mocks only replay responses you already believed in, so they cannot catch request-side errors. Two examples need provisioning that a fresh account will not have. `forceCredit` fails with API 104 "Invalid service action" unless the MID has `FORCE_CREDIT` enabled, which is an authentication-tier error rather than a decline. `tokenize` needs the per-site HMAC key from Inovio support, which may live on a different site than your gateway credentials. --- # Node SDK The Inovio gateway SDK for Node 18 and TypeScript, with a typed promise-based API and no runtime dependencies. Source: https://developer.inoviopay.com/sdks/node.html Markdown: https://developer.inoviopay.com/sdks/node.md Repository: https://github.com/Inoviopay/inovio-gateway-sdk-node The Inovio payment gateway for Node. Card transactions covering authorize, capture, refund and tokenize, with a typed, promise-based API and no runtime dependencies. This is the **reference implementation**. It defined the canonical method surface, the naming and the conformance fixtures that the PHP, Python and Java SDKs were ported against, so where the four disagree, this is the intended shape. > **The SDK is alpha** > Version 0.1.0-alpha and **not published to npm**. Install from the public > GitHub repository as shown below, and pin a commit until a tagged release > lands. ## Status and install npm installs directly from the GitHub repository: ```bash npm install github:Inoviopay/inovio-gateway-sdk-node ``` Pin a commit rather than tracking `main` while the package is alpha: ```bash npm install github:Inoviopay/inovio-gateway-sdk-node# ``` The `package.json` sets `"private": true`, which blocks an accidental `npm publish` before the package is ready. It does not affect installing from a git URL. > **A git install does not build the package** > The repository ships TypeScript sources and `main` points at `dist/`, but > there is no `prepare` script, so `npm install github:...` leaves you without a > compiled `dist/`. Until a build step is wired in, clone and build: > > ```bash > git clone https://github.com/Inoviopay/inovio-gateway-sdk-node.git > cd inovio-gateway-sdk-node > npm install && npm run build > ``` > > Then depend on it with `npm install /path/to/inovio-gateway-sdk-node`, or run > `npm link` in the clone followed by `npm link @inovio/gateway-sdk` in your > project. ## Requirements Node **18 or newer**. Zero runtime dependencies: HTTP goes through the built-in `fetch`, and the flat key-value response is parsed without a JSON library beyond the platform's own. The package is **ESM** (`"type": "module"`), with `.d.ts` declarations shipped alongside. Import it with `import`, not `require`. From a CommonJS file, use a dynamic import: ```js const { InovioClient } = await import('@inovio/gateway-sdk'); ``` ## Quick start ```ts import { InovioClient, Money, PaymentMethods, Refs } from '@inovio/gateway-sdk'; const client = new InovioClient( { reqUsername: process.env.INOVIO_USER!, reqPassword: process.env.INOVIO_PASS!, siteId: '123' }, { environment: 'SANDBOX' } ); const result = await client.sale({ paymentMethod: PaymentMethods.card('4111111111111111', '122030', '123'), lineItems: [{ productId: 'SKU-1', count: 1, value: Money.of('10.00', 'USD') }], idempotency: { xtlOrderId: 'ORDER-555' }, // retry-safe by default }); switch (result.status) { case 'APPROVED': /* fulfil */ break; case 'DECLINED': /* result.outcome.service, result.serviceClassification */ break; case 'PENDING': /* result.nextAction — 3DS challenge, redirect, voucher */ break; case 'RUNNING': case 'FAILED': break; } ``` The second constructor argument is a `ClientOptions` object. Everything on it is optional: ```ts const client = new InovioClient(creds, { environment: 'PRODUCTION', endpoint: 'http://localhost:8080/payment/pmt_service.cfm', // overrides environment apiVersion: '4.14', timeoutMs: 30_000, httpClient: myInstrumentedClient, siteKey: process.env.INOVIO_SITE_KEY, // required only for tokenize() }); ``` ## Node-specific notes **Requests are plain object literals.** There is no request builder class. `sale()` and `authorize()` take a `SaleRequest` / `AuthorizeRequest` interface, so you write the object inline and TypeScript checks it. That is why the quick start passes `lineItems` as an array of literals rather than constructing a `LineItem`. **`Money` refuses JavaScript numbers.** `Money.of(1.25, 'USD')` throws a `TypeError`; pass `'1.25'`. Binary floats cannot represent decimal amounts exactly, and the wire format is a decimal string, so rounding has to be your explicit decision. **The timeout error is `TimeoutError`,** a subclass of `TransportError`. It carries `xtlOrderId` and a `recoveryHint` getter. **Payment methods are a discriminated union.** `PaymentMethods.card()`, `.token()` and `.savedCard()` return objects tagged with a `kind` field, so a `switch` over a payment method narrows correctly. The variants declared but not implemented in v1 are rejected at request-build time with a clear message rather than sent to the gateway. **References are branded types.** `OrderRef` and friends are structurally `{ poId: string }` but branded, so a plain object literal will not type-check where a ref is expected. Build them with the `Refs` factory: `Refs.order('18800001')`, `Refs.xtlOrder('ORDER-555')`, `Refs.lineItem('9000001')`. ## Operations Every public method on `InovioClient`, with the `REQUEST_ACTION` it maps to. All of them return a promise. | Method | Action | Notes | |---|---|---| | `sale(req: SaleRequest): Promise` | `CCAUTHCAP` | Authorize and capture in one step. See [Sale](https://developer.inoviopay.com/api/sale.md). | | `authorize(req: AuthorizeRequest): Promise` | `CCAUTHORIZE` | Hold funds; capture later. See [Authorize](https://developer.inoviopay.com/api/authorize.md). | | `capture(order: OrderRef, amount?: Money): Promise` | `CCCAPTURE` | Omit the amount to capture in full. See [Capture](https://developer.inoviopay.com/api/capture.md). | | `captureLineItem(order: OrderRef, item: LineItemRef, amount: Money): Promise` | `CCCAPTURE` | All three arguments are required. | | `reverse(order: OrderRef): Promise` | `CCREVERSE` | Void the original authorization. See [Reversal](https://developer.inoviopay.com/api/reversal.md). | | `reverseCapture(order: OrderRef): Promise` | `CCREVERSECAP` | Void a capture rather than the original auth. | | `refund(order: OrderRef, amount?: Money): Promise` | `CCCREDIT` | Omit the amount to refund in full. See [Credit](https://developer.inoviopay.com/api/credit.md). | | `forceCredit(req: CreditRequest): Promise` | `CCCREDIT` + `FORCE_CREDIT` | Credit with no referenced original. Needs MID provisioning. | | `status(ref: OrderRef \| XtlOrderId): Promise` | `CCSTATUS` | Order-level net position. See [Status](https://developer.inoviopay.com/api/status.md). | | `updateOrder(order: OrderRef, update: OrderUpdate): Promise` | `CCTRANSUPDATE` | Attach a receipt and UDF metadata. | | `tokenize(card: Card, options?: { uniqueId?: string }): Promise` | token service | Needs `siteKey`. See [Tokenization](https://developer.inoviopay.com/api/tokenization.md). | | `testAuth(): Promise` | `TESTAUTH` | Verify credentials without transacting. | | `testAvailability(): Promise` | `TESTGW` | Verify gateway availability. Safe to poll. | `captureLineItem()` requires the parent order as well as the line item. The gateway rejects `REQUEST_REF_PO_LI_ID` on its own with API 113 "Invalid Data", and `LineItemRef` does not carry its order, so both must be passed. Omitting the amount throws a `ValidationError` locally rather than letting the gateway reject it. Both behaviours were verified against the live gateway. > **Two capabilities are PHP-only in this alpha** > `reverse()` and `reverseCapture()` take only the order reference here and > never send `CREDIT_ON_FAIL`, and there is no `threeDSecure()` sub-client. See > [Reverse versus credit](https://developer.inoviopay.com/sdks/concepts.md#reverse-versus-credit) and > [3D Secure](https://developer.inoviopay.com/sdks/concepts.md#3d-secure) for what to do instead. ## Handling results `result.status` carries the answer. A decline resolves normally with `status: 'DECLINED'`; rejections are reserved for cases where you never got a payment answer at all. ```ts const result = await client.sale({ paymentMethod: PaymentMethods.card(pan, expiry, cvv), lineItems: [{ productId: 'SKU-1', count: 1, value: Money.of('10.00', 'USD') }], customer, billingAddress, // Setting an order id makes the call retry-safe: a repeat returns the // original result instead of charging twice. idempotency: { xtlOrderId: 'ORDER-555' }, }); switch (result.status) { case 'APPROVED': // fulfil the order break; case 'DECLINED': // The service tier carries the decline taxonomy your dunning logic needs. if (result.serviceClassification?.retryable) { /* retry later */ } break; case 'PENDING': // complete result.nextAction.kind break; default: // inspect result.outcome } ``` Reference keys sit flat on the result, not inside a nested bag, because they are the fields you reach for most: `client.capture(result.orderRef, amount)`. The available refs are `orderRef`, `xtlOrderRef`, `transactionId`, `requestId`, `batchId`, `customerRef`, `savedCardRef`, `membershipRef` and `lineItemRefs`. The gateway sends codes; the SDK adds labels. `result.serviceClassification.retryable`, `.terminal` and `.stopRecurring`, plus `result.avs.classification`, are labels the SDK derives from the response codes, not values the gateway sent. The codes themselves are on `result.outcome` and the untouched wire fields on `result.raw`. See [Outcome tiers](https://developer.inoviopay.com/sdks/concepts.md#outcome-tiers). Errors extend `InovioError`: `AuthenticationError`, `ValidationError` (carrying the offending `refField`), `ConfigurationError`, `TransportError`, `TimeoutError` (a subclass of `TransportError`) and `RateLimitError`. ## Tokenization `tokenize()` exchanges a PAN for a single-use `TOKEN_GUID` that replaces `PMT_NUMB` on a later sale or authorize. It hits `token_service.cfm` with HMAC header authentication rather than username and password, so it needs `siteKey` in `ClientOptions`. Without it the call throws a `ValidationError` before any network traffic, and the service itself would answer error 121. ```ts // Tokenize on the site that holds the HMAC key... const t = await tokenClient().tokenize(PaymentMethods.card(pan, expiry, cvv)); t.token.guid; t.tokenReqId; // quote this to support // BIN metadata is best-effort — undefined when the BIN is not in the table. [t.card.brand, t.card.type, t.card.bank].filter(Boolean).join(' / '); // The token replaces the PAN ONLY: expiry (and CVV) still travel with it, which // tokenize() carries forward for you. // ...then transact on the gateway site. const sale = await client().sale({ paymentMethod: t.token, lineItems: [{ productId: 'SKU-1', count: 1, value: Money.of('10.00', 'USD') }], customer, billingAddress, idempotency: { xtlOrderId: 'ORDER-556' }, }); ``` The signed message **excludes the PAN**, contrary to what the v4.14 PDF says. The gateway validates `hmac_sha256(timestamp || unique_id || site_id, site_key)`, verified against the live token service. The SDK signs the way the gateway behaves. For a merchant-hosted signature endpoint that lets a browser post the PAN directly, use the exported helpers on their own: ```ts import { signTokenRequest, tokenTimestamp, verifyTokenResponse } from '@inovio/gateway-sdk'; const timestamp = tokenTimestamp(); // YYYYMMDDHHMMSS UTC const signature = signTokenRequest(siteKey, timestamp, uniqueId, siteId); // Return { signature, timestamp, siteId } to the browser. Never the site key. ``` > **tokenize() runs on your server** > The card number passes through your infrastructure. The browser Hosted Fields > client that would keep it in the cardholder's browser is not yet available. > See [Where the card number goes](https://developer.inoviopay.com/sdks/index.md#where-the-card-number-goes). ## 3D Secure This SDK carries the request-side and result-side 3DS surface, but not the server-leg driver. What is here: - **`BrowserData`** on a request, which the request builder maps to the `P3DS_BROWSER_LANGUAGE`, `USER_AGENT_XTL` and `P3DS_BROWSER_HEADER` fields plus the optional EMVCo device fields. **These are required for gateway 3DS**: without them the gateway silently skips authentication entirely. - **The challenge `nextAction`.** When a result is `PENDING` because a challenge is required, `result.nextAction` narrows to `{ kind: 'threeDSChallenge'; redirectUrl?; jwt?; procTransId?; pareq? }`, which is everything your page needs to post into the visible iframe. ```ts if (result.status === 'PENDING' && result.nextAction?.kind === 'threeDSChallenge') { const { redirectUrl, jwt } = result.nextAction; // Browser: POST jwt to redirectUrl in a visible iframe. } ``` > **The 3DS server legs are PHP-only in this alpha** > There is no `client.threeDSecure()` here. The prepare leg against > `3dsrequest.cfm` and the completion leg after the ACS challenge have to be > driven against [the 3DS endpoints](https://developer.inoviopay.com/api/3ds-gateway.md) directly, or through > the [PHP SDK](https://developer.inoviopay.com/sdks/php.md#3d-secure), which implements both. ## Timeouts and reconciliation A timeout means the state is **unknown**, not failed. The gateway may have approved the charge and lost the response. ```ts try { await client.sale({ ...req, idempotency: { xtlOrderId: 'ORDER-555' } }); } catch (e) { if (e instanceof TimeoutError) { console.warn(e.recoveryHint); const actual = await client.status(Refs.xtlOrder('ORDER-555')); // a blind retry here could double-charge } } ``` Idempotency defaults to `RETURN_ORIGINAL` when `xtlOrderId` is set, so a retry returns the original result rather than charging twice. `status()` is also the reconciliation primitive. Partial captures, refunds and voids are separate transactions sharing one order, so net position is an order-level question: ```ts // Build a multi-leg order: authorize 100, capture 60, refund 10. const order = await seedOrder(c, 'STATUS', { amount: '100.00' }); await c.capture(order.orderRef, Money.of('60.00', 'USD')); await c.refund(order.orderRef, Money.of('10.00', 'USD')); const s = await c.status(order.orderRef); s.transactions.length; // every leg: auth, captures, refunds, voids s.authorized?.amount; s.captured?.amount; s.refunded?.amount; s.net?.amount; // captured - refunded s.outstanding?.amount; // authorized - captured // You can also look an order up by YOUR id: // await c.status(Refs.xtlOrder('ORDER-555')); ``` ## Running the conformance tests ```bash npm install npm run generate # regenerate enums from spec/spec-enums.json npm run build npm test ``` `npm test` runs both suites through the Node test runner. `npm run conformance` runs only the cross-language corpus in `spec/conformance-fixtures.json`, the same fixtures the other three SDKs replay, which is what keeps the four honest about producing identically shaped results. `npm run typecheck` type-checks without emitting. The enums in `src/enums/generated.ts` are generated from `spec/spec-enums.json`, extracted from the v4.14 specification appendices. Do not hand-edit them; regenerate. ## Examples in the repo Fourteen runnable files in `examples/`, one per operation. They are real, executed code rather than markdown snippets, so they cannot silently drift from the API. ```bash npm run examples # all 14, against a mock transport node examples/03-sale.mjs # just one ``` | File | Operation | |---|---| | `01-test-availability.mjs` | `testAvailability()`, the `TESTGW` health check | | `02-test-auth.mjs` | `testAuth()`, credential verification with no transaction | | `03-sale.mjs` | `sale()`, authorize and capture in one step | | `04-authorize.mjs` | `authorize()`, holding funds and keeping the `orderRef` | | `05-capture.mjs` | `capture()`, full or partial | | `06-capture-line-item.mjs` | `captureLineItem()`, which needs order, item and amount | | `07-reverse.mjs` | `reverse()`, voiding an authorization | | `08-reverse-capture.mjs` | `reverseCapture()`, voiding a capture pre-settlement | | `09-refund.mjs` | `refund()`, returning captured funds | | `10-force-credit.mjs` | `forceCredit()`, which needs MID provisioning | | `11-status.mjs` | `status()`, reconciliation and net position | | `12-update-order.mjs` | `updateOrder()`, attaching receipts for compliance | | `13-tokenize.mjs` | `tokenize()`, PAN to single-use token | | `14-timeout-recovery.mjs` | The pattern that prevents double charges | By default they run against a mock transport: no credentials, no network, no money moves, safe in CI. Set `INOVIO_LIVE=1` plus credentials to run the same code against a real gateway: ```bash INOVIO_LIVE=1 \ INOVIO_USER=... INOVIO_PASS=... INOVIO_SITE_ID=... INOVIO_MERCH_ACCT_ID=... \ INOVIO_SITE_KEY=... \ npm run examples ``` Live mode creates **real transactions**, so point it at a test environment. Running live is worth doing before you trust an integration: mocks only replay responses you already believed in, so they cannot catch request-side errors. Every example that operates on an existing order builds its own first, rather than hardcoding an id that resolves only against a mock. Two examples need provisioning that a fresh account will not have. `forceCredit` fails with API 104 "Invalid service action" unless the MID has `FORCE_CREDIT` enabled, which arrives as an `AuthenticationError` rather than a decline. `tokenize` needs the per-site HMAC key from Inovio support. --- # Python SDK The Inovio gateway SDK for Python 3.8, synchronous, typed, and built on the standard library alone. Source: https://developer.inoviopay.com/sdks/python.html Markdown: https://developer.inoviopay.com/sdks/python.md Repository: https://github.com/Inoviopay/inovio-gateway-sdk-python The Inovio payment gateway for Python. Card transactions covering authorize, capture, refund and tokenize, with a typed, synchronous API and no third-party dependencies. > **The SDK is alpha** > Version 0.1.0-alpha and **not published to PyPI**. Install from the public > GitHub repository as shown below, and pin a commit until a tagged release > lands. ## Status and install pip installs directly from the GitHub repository: ```bash pip install "git+https://github.com/Inoviopay/inovio-gateway-sdk-python.git@main" ``` Pin a commit rather than tracking `main` while the package is alpha: ```bash pip install "git+https://github.com/Inoviopay/inovio-gateway-sdk-python.git@" ``` In a `requirements.txt`: ``` inovio-gateway-sdk @ git+https://github.com/Inoviopay/inovio-gateway-sdk-python.git@main ``` Or as a `pyproject.toml` dependency: ```toml [project] dependencies = [ "inovio-gateway-sdk @ git+https://github.com/Inoviopay/inovio-gateway-sdk-python.git@main", ] ``` The distribution is a standard setuptools build from `src/`, so a git install needs no extra flags. ## Requirements Python **3.8 or newer**, standard library only. HTTP goes through `urllib`. The client is **synchronous**. An async client was deliberately deferred until a partner asks for one, so every call blocks. If you need it inside an async application, run it in a thread executor. `py.typed` ships in the package, so type checkers see the annotations rather than falling back to `Any`. ## Quick start ```python from inovio_gateway import ( Credentials, InovioClient, Money, PaymentMethods, Refs, TransactionStatus, ) from inovio_gateway.model import Idempotency, LineItem from inovio_gateway.request import TransactionRequest client = InovioClient(Credentials(user, password, site_id="123"), environment="SANDBOX") result = client.sale(TransactionRequest( payment_method=PaymentMethods.card("4111111111111111", "122030", "123"), line_items=[LineItem("SKU-1", 1, Money.of("10.00", "USD"))], idempotency=Idempotency(xtl_order_id="ORDER-555"), # retry-safe by default )) if result.status is TransactionStatus.APPROVED: ... elif result.status is TransactionStatus.PENDING: result.next_action # 3DS challenge, redirect, voucher ``` Everything after the credentials is a keyword argument: ```python client = InovioClient( Credentials(user, password, site_id="123"), environment="PRODUCTION", endpoint="http://localhost:8080/payment/pmt_service.cfm", # overrides environment timeout_ms=30_000, http_client=my_instrumented_client, site_key=os.environ["INOVIO_SITE_KEY"], # required only for tokenize() ) ``` ## Python-specific notes **Method names are snake_case.** The shared object model's `captureLineItem` becomes `capture_line_item`, `reverseCapture` becomes `reverse_capture`, `forceCredit` becomes `force_credit`, `updateOrder` becomes `update_order`, and `testAuth` / `testAvailability` become `test_auth` / `test_availability`. Result fields follow the same rule: `order_ref`, `xtl_order_ref`, `next_action`, `service_classification`, `line_item_refs`. **Amounts are `decimal.Decimal` internally.** `Money.of` accepts a `Decimal`, a `str` or an `int`, and **rejects `float`**. `Money.of(1.25, "USD")` raises a `TypeError`; pass `"1.25"` or `Decimal("1.25")`. **The timeout exception is `InovioTimeoutError`,** deliberately not named `TimeoutError`. Callers routinely catch the builtin of that name, and shadowing it would hide exactly the unknown-state case that needs `status()` recovery. It subclasses `TransportError` and carries `xtl_order_id` and `recovery_hint`. **Status is an enum, not a string.** `result.status` is a `TransactionStatus` member, so compare with `is` rather than `==`, and read `result.status.value` when you need the wire string for logging. **Requests and model objects are dataclasses.** `TransactionRequest` takes keyword arguments at construction and its optional blocks are assigned afterwards: ```python req = TransactionRequest( payment_method=PaymentMethods.card(pan, expiry, cvv), line_items=[LineItem("SKU-1", 1, Money.of("10.00", "USD"))], ) req.customer = Customer(first_name="Ada", last_name="Lovelace", email="ada@example.invalid", ip="203.0.113.10") req.billing_address = Address(line1="123 Main St", city="Austin", state="TX", zip="78701", country="US") req.idempotency = Idempotency(xtl_order_id="ORDER-555") ``` ## Operations Every public method on `InovioClient`, with the `REQUEST_ACTION` it maps to: | Method | Action | Notes | |---|---|---| | `sale(req: TransactionRequest) -> TransactionResult` | `CCAUTHCAP` | Authorize and capture in one step. See [Sale](https://developer.inoviopay.com/api/sale.md). | | `authorize(req: TransactionRequest) -> TransactionResult` | `CCAUTHORIZE` | Hold funds; capture later. See [Authorize](https://developer.inoviopay.com/api/authorize.md). | | `capture(order: OrderRef, amount: Optional[Money] = None) -> TransactionResult` | `CCCAPTURE` | Omit the amount to capture in full. See [Capture](https://developer.inoviopay.com/api/capture.md). | | `capture_line_item(order: OrderRef, item: LineItemRef, amount: Money) -> TransactionResult` | `CCCAPTURE` | All three arguments are required. | | `reverse(order: OrderRef) -> TransactionResult` | `CCREVERSE` | Void the original authorization. See [Reversal](https://developer.inoviopay.com/api/reversal.md). | | `reverse_capture(order: OrderRef) -> TransactionResult` | `CCREVERSECAP` | Void a capture rather than the original auth. | | `refund(order: OrderRef, amount: Optional[Money] = None) -> TransactionResult` | `CCCREDIT` | Omit the amount to refund in full. See [Credit](https://developer.inoviopay.com/api/credit.md). | | `force_credit(req: TransactionRequest) -> TransactionResult` | `CCCREDIT` + `FORCE_CREDIT` | Credit with no referenced original. Needs MID provisioning. | | `status(ref: Union[OrderRef, XtlOrderId]) -> OrderStatus` | `CCSTATUS` | Order-level net position. See [Status](https://developer.inoviopay.com/api/status.md). | | `update_order(order: OrderRef, update: OrderUpdate) -> TransactionResult` | `CCTRANSUPDATE` | Attach a receipt and UDF metadata. | | `tokenize(card: Card, unique_id: Optional[str] = None) -> TokenizeResult` | token service | Needs `site_key`. See [Tokenization](https://developer.inoviopay.com/api/tokenization.md). | | `test_auth() -> HealthResult` | `TESTAUTH` | Verify credentials without transacting. | | `test_availability() -> HealthResult` | `TESTGW` | Verify gateway availability. Safe to poll. | `capture_line_item()` requires the parent order as well as the line item. The gateway rejects `REQUEST_REF_PO_LI_ID` on its own with API 113 "Invalid Data", and `LineItemRef` does not carry its order, so both must be passed. Omitting the amount raises a `ValidationError` locally rather than letting the gateway reject it. Both behaviours were verified against the live gateway. > **Two capabilities are PHP-only in this alpha** > `reverse()` and `reverse_capture()` take only the order reference here and > never send `CREDIT_ON_FAIL`, and there is no `three_d_secure()` sub-client. > See [Reverse versus credit](https://developer.inoviopay.com/sdks/concepts.md#reverse-versus-credit) and > [3D Secure](https://developer.inoviopay.com/sdks/concepts.md#3d-secure) for what to do instead. ## Handling results `result.status` carries the answer. A decline is a return value, not an exception, so branch on the status rather than wrapping in try/except for the payment outcome: ```python from inovio_gateway import TransactionStatus result = client.sale(req) print(result.status.value) print(result.order_ref.po_id if result.order_ref else "-") print(f"{result.amount.to_wire()} {result.amount.currency}" if result.amount else "-") if result.status is TransactionStatus.APPROVED: ... # fulfil the order elif result.status is TransactionStatus.DECLINED: # The service tier carries the decline taxonomy your dunning logic needs. retryable = result.service_classification and result.service_classification.retryable ... # retry later, or do not retry elif result.status is TransactionStatus.PENDING: ... # complete result.next_action.kind else: ... # inspect result.outcome ``` Reference keys sit flat on the result, not inside a nested bag, because they are the fields you reach for most: `client.capture(result.order_ref, amount)`. The available refs are `order_ref`, `xtl_order_ref`, `transaction_id`, `request_id`, `batch_id`, `customer_ref`, `saved_card_ref`, `membership_ref` and `line_item_refs`. The gateway sends codes; the SDK adds labels. `result.service_classification.retryable`, `.terminal` and `.stop_recurring`, plus `result.avs.classification`, are labels the SDK derives from the response codes, not values the gateway sent. The codes themselves are on `result.outcome` and the untouched wire fields on `result.raw`. See [Outcome tiers](https://developer.inoviopay.com/sdks/concepts.md#outcome-tiers). Exceptions extend `InovioError`: `AuthenticationError`, `ValidationError` (carrying the offending `ref_field`), `ConfigurationError`, `TransportError`, `InovioTimeoutError` (a subclass of `TransportError`) and `RateLimitError`. ## Tokenization `tokenize()` exchanges a PAN for a single-use `TOKEN_GUID` that replaces `PMT_NUMB` on a later sale or authorize. It hits `token_service.cfm` with HMAC header authentication rather than username and password, so it needs `site_key` on the client. Without it the call raises a `ValidationError` before any network traffic, and the service itself would answer error 121. ```python # Tokenize on the site that holds the HMAC key... t = token_client().tokenize(PaymentMethods.card(demo.pan, demo.expiry, demo.cvv)) t.token.guid t.token_req_id # quote this to support # BIN metadata is best-effort — None when the BIN is not in the lookup table. bits = [b for b in (t.card.brand, t.card.type, t.card.bank) if b] " / ".join(bits) or "(BIN not found)" # The token replaces the PAN ONLY: expiry (and CVV) still travel with it, which # tokenize() carries forward for you. sale = client().sale(request(tag="TOK", payment_method=t.token)) ``` The signed message **excludes the PAN**, contrary to what the v4.14 PDF says. The gateway validates `hmac_sha256(timestamp || unique_id || site_id, site_key)`, verified against the live token service. The SDK signs the way the gateway behaves. For a merchant-hosted signature endpoint that lets a browser post the PAN directly, use the exported helpers on their own: ```python from inovio_gateway import sign_token_request, token_timestamp timestamp = token_timestamp() # YYYYMMDDHHMMSS UTC signature = sign_token_request(site_key, timestamp, unique_id, site_id) # Return {signature, timestamp, site_id} to the browser. Never the site key. ``` > **tokenize() runs on your server** > The card number passes through your infrastructure. The browser Hosted Fields > client that would keep it in the cardholder's browser is not yet available. > See [Where the card number goes](https://developer.inoviopay.com/sdks/index.md#where-the-card-number-goes). ## 3D Secure This SDK carries the request-side and result-side 3DS surface, but not the server-leg driver. What is here: - **`BrowserData`** on a request, which the request builder maps to the `P3DS_BROWSER_LANGUAGE`, `USER_AGENT_XTL` and `P3DS_BROWSER_HEADER` fields plus the optional EMVCo device fields. **These are required for gateway 3DS**: without them the gateway silently skips authentication entirely. - **The challenge next action.** When a result is `PENDING` because a challenge is required, `result.next_action.kind` is `"threeDSChallenge"` and the object carries `redirect_url`, `jwt` and `proc_trans_id`, which is everything your page needs to post into the visible iframe. ```python if (result.status is TransactionStatus.PENDING and result.next_action and result.next_action.kind == "threeDSChallenge"): result.next_action.redirect_url result.next_action.jwt # Browser: POST jwt to redirect_url in a visible iframe. ``` > **The 3DS server legs are PHP-only in this alpha** > There is no `client.three_d_secure()` here. The prepare leg against > `3dsrequest.cfm` and the completion leg after the ACS challenge have to be > driven against [the 3DS endpoints](https://developer.inoviopay.com/api/3ds-gateway.md) directly, or through > the [PHP SDK](https://developer.inoviopay.com/sdks/php.md#3d-secure), which implements both. ## Timeouts and reconciliation A timeout means the state is **unknown**, not failed. The gateway may have approved the charge and lost the response. ```python from inovio_gateway import InovioTimeoutError, Refs try: client.sale(req) except InovioTimeoutError as e: print(e.recovery_hint) actual = client.status(Refs.xtl_order(e.xtl_order_id)) # Do NOT retry blindly — that risks a double charge. ``` Setting `Idempotency(xtl_order_id=...)` defaults the mode to `RETURN_ORIGINAL`, so a retry of the same request returns the original result rather than charging twice. `status()` is also the reconciliation primitive. Partial captures, refunds and voids are separate transactions sharing one order, so net position is an order-level question: ```python # Build a multi-leg order: authorize 100, capture 60, refund 10. order = seed_order(c, "STATUS", amount="100.00") c.capture(order.order_ref, Money.of("60.00", "USD")) c.refund(order.order_ref, Money.of("10.00", "USD")) s = c.status(order.order_ref) len(s.transactions) # every leg: auth, captures, refunds, voids s.authorized.to_wire() s.captured.to_wire() s.refunded.to_wire() s.net.to_wire() # captured - refunded s.outstanding.to_wire() # authorized - captured # You can also look an order up by YOUR id: # c.status(Refs.xtl_order("ORDER-555")) ``` `Refs` exposes `order()`, `xtl_order()` and `line_item()` in this SDK. ## Running the conformance tests ```bash PYTHONPATH=src python3 -m unittest discover -s tests python3 scripts/generate_enums.py # regenerate enums ``` The suite replays the shared cross-language corpus in `spec/conformance-fixtures.json`, the same fixtures the other three SDKs run, which is what keeps the four honest about producing identically shaped results. The enums in `src/inovio_gateway/enums/generated.py` are generated from `spec/spec-enums.json`, extracted from the v4.14 specification appendices. Do not hand-edit them; regenerate. ## Examples in the repo Fourteen runnable files in `examples/`, one per operation. They are real, executed code rather than markdown snippets, so they cannot silently drift from the API. ```bash PYTHONPATH=src python3 examples/run_all.py # all 14, against a mock transport PYTHONPATH=src python3 examples/03_sale.py # just one ``` | File | Operation | |---|---| | `01_test_availability.py` | `test_availability()`, the `TESTGW` health check | | `02_test_auth.py` | `test_auth()`, credential verification with no transaction | | `03_sale.py` | `sale()`, authorize and capture in one step | | `04_authorize.py` | `authorize()`, holding funds and keeping the order ref | | `05_capture.py` | `capture()`, full or partial | | `06_capture_line_item.py` | `capture_line_item()`, which needs order, item and amount | | `07_reverse.py` | `reverse()`, voiding an authorization | | `08_reverse_capture.py` | `reverse_capture()`, voiding a capture pre-settlement | | `09_refund.py` | `refund()`, returning captured funds | | `10_force_credit.py` | `force_credit()`, which needs MID provisioning | | `11_status.py` | `status()`, reconciliation and net position | | `12_update_order.py` | `update_order()`, attaching receipts for compliance | | `13_tokenize.py` | `tokenize()`, PAN to single-use token | | `14_timeout_recovery.py` | The pattern that prevents double charges | By default they run against a mock transport: no credentials, no network, no money moves, safe in CI. Set `INOVIO_LIVE=1` plus credentials to run the same code against a real gateway: ```bash INOVIO_LIVE=1 \ INOVIO_USER=... INOVIO_PASS=... INOVIO_SITE_ID=... INOVIO_MERCH_ACCT_ID=... \ INOVIO_SITE_KEY=... # token service HMAC key INOVIO_TOKEN_SITE_ID=... # only if tokenizing on a different site PYTHONPATH=src python3 examples/run_all.py ``` Live mode creates **real transactions**, so point it at a test environment. Running live is worth doing before you trust an integration: mocks only replay responses you already believed in, so they cannot catch request-side errors. Every example that operates on an existing order builds its own first, rather than hardcoding an id that resolves only against a mock. Two examples need provisioning that a fresh account will not have. `force_credit` fails with API 104 "Invalid service action" unless the MID has `FORCE_CREDIT` enabled, which is an authentication-tier error rather than a decline. `tokenize` needs the per-site HMAC key from Inovio support, which may live on a different site than your gateway credentials. --- # Java SDK The Inovio gateway SDK for Java 11, with zero runtime dependencies and a BigDecimal money type. Source: https://developer.inoviopay.com/sdks/java.html Markdown: https://developer.inoviopay.com/sdks/java.md Repository: https://github.com/Inoviopay/inovio-gateway-sdk-java The Inovio payment gateway for Java 11 and newer. Card transactions covering authorize, capture, refund and tokenize, with a typed API and no runtime dependencies. > **The SDK is alpha** > Version 0.1.0-alpha and **not published to Maven Central**. Build it from the > public GitHub repository as shown below, and pin a commit until a tagged > release lands. ## Status and install **Build and install locally.** This is the path that works today, because the repository has no published release tag for a build service to resolve: ```bash git clone https://github.com/Inoviopay/inovio-gateway-sdk-java.git cd inovio-gateway-sdk-java mvn install ``` That puts `com.inoviopay:inovio-gateway-sdk:0.1.0-alpha.0` in your local repository. Then depend on it: ```xml com.inoviopay inovio-gateway-sdk 0.1.0-alpha.0 ``` Or in Gradle: ```groovy repositories { mavenLocal(); mavenCentral() } dependencies { implementation 'com.inoviopay:inovio-gateway-sdk:0.1.0-alpha.0' } ``` **JitPack**, which builds Maven artifacts on demand straight from a public GitHub repository, is the way to consume it without a local install once the repository carries a release tag or you are willing to resolve a commit. Add the repository and depend on the coordinate JitPack derives from the GitHub path: ```xml jitpack.io https://jitpack.io com.github.Inoviopay inovio-gateway-sdk-java ``` Note that JitPack's coordinate is `com.github.Inoviopay:inovio-gateway-sdk-java`, derived from the repository path, not the `com.inoviopay:inovio-gateway-sdk` coordinate in the project's own `pom.xml`. Only the import statements are unaffected; the package names come from the source either way. ## Requirements **Java 11 or newer.** Zero runtime dependencies: HTTP goes through `java.net.http`, and the flat key-value response is read by a small inline JSON reader rather than pulling in Jackson for a response shape that does not need it. JUnit is a test-scope dependency only. ## Quick start ```java InovioClient client = new InovioClient( new InovioClient.Credentials(user, password, "123")); TransactionRequest req = new TransactionRequest( PaymentMethods.card("4111111111111111", "122030", "123"), new LineItem("SKU-1", 1, Money.of("10.00", "USD"))) .idempotency("ORDER-555"); // retry-safe by default TransactionResult r = client.sale(req); switch (r.status()) { case APPROVED: /* fulfil */ break; case DECLINED: /* r.outcome().service(), r.serviceClassification() */ break; case PENDING: /* r.nextAction() — 3DS challenge, redirect, voucher */ break; case RUNNING: case FAILED: break; } ``` Configuration goes through a mutable `Options` object rather than a long constructor: ```java InovioClient.Options o = new InovioClient.Options(); o.environment = Transport.Environment.PRODUCTION; o.endpoint = "http://localhost:8080/payment/pmt_service.cfm"; // overrides environment o.timeoutMs = 30_000; o.httpClient = myInstrumentedClient; o.siteKey = System.getenv("INOVIO_SITE_KEY"); // required only for tokenize() InovioClient client = new InovioClient(creds, o); ``` ## Java 11 and the sealed hierarchy This SDK targets **Java 11** for enterprise reach rather than newer-JDK ergonomics. The cost is real and worth knowing: | Type | Java 17+ would be | What this SDK does | |---|---|---| | `PaymentMethod` sealed | `sealed interface ... permits` | interface plus `final` implementations with **package-private constructors** | | `Money`, refs, value types | `record` | hand-written `final` classes | | Exhaustive status handling | switch expressions | `switch` plus an explicit `default` | `PaymentMethod` is therefore **sealed by convention**. Every implementation is `final` and its constructor is package-private, so the only way to build one is `PaymentMethods.card(...)`, `.token(...)` or `.savedCard(...)`, and outside code cannot add a variant. What is lost is *compiler-enforced* exhaustiveness: you get no error for an unhandled variant in a `switch`, so write the `default` branch yourself. ## Java-specific notes **Amounts are `BigDecimal`.** `Money.of` has three overloads: `String`, `BigDecimal`, and a `double` overload that exists **purely to reject floating point** with a clear message. `Money.of(1.25, "USD")` throws; pass `"1.25"` or a `BigDecimal`. `Money.equals` uses `compareTo`, so `"1.5"` equals `"1.50"`. **All exceptions are unchecked.** `InovioException` extends `RuntimeException`, so nothing forces a `throws` clause onto your call sites. Catch deliberately. **The timeout exception is `GatewayTimeoutException`,** not `TimeoutException`, so it is never confused with `java.util.concurrent.TimeoutException`. It subclasses `TransportException` and carries `xtlOrderId()` and `recoveryHint()`. **`Card.toString()` prints only the last four digits**, so a card object landing in a log line does not leak a PAN. **Overloads stand in for optional arguments.** `capture()`, `refund()`, `tokenize()` and `status()` each have a shorter overload. `status()` in particular is overloaded on `Refs.OrderRef` and `Refs.XtlOrderId` rather than taking a union type. ## Operations Every public method on `InovioClient`, with the `REQUEST_ACTION` it maps to: | Method | Action | Notes | |---|---|---| | `TransactionResult sale(TransactionRequest req)` | `CCAUTHCAP` | Authorize and capture in one step. See [Sale](https://developer.inoviopay.com/api/sale.md). | | `TransactionResult authorize(TransactionRequest req)` | `CCAUTHORIZE` | Hold funds; capture later. See [Authorize](https://developer.inoviopay.com/api/authorize.md). | | `TransactionResult capture(Refs.OrderRef order, Money amount)` | `CCCAPTURE` | Partial capture. See [Capture](https://developer.inoviopay.com/api/capture.md). | | `TransactionResult capture(Refs.OrderRef order)` | `CCCAPTURE` | Capture in full. | | `TransactionResult captureLineItem(Refs.OrderRef order, Refs.LineItemRef item, Money amount)` | `CCCAPTURE` | All three arguments are required. | | `TransactionResult reverse(Refs.OrderRef order)` | `CCREVERSE` | Void the original authorization. See [Reversal](https://developer.inoviopay.com/api/reversal.md). | | `TransactionResult reverseCapture(Refs.OrderRef order)` | `CCREVERSECAP` | Void a capture rather than the original auth. | | `TransactionResult refund(Refs.OrderRef order, Money amount)` | `CCCREDIT` | Partial refund. See [Credit](https://developer.inoviopay.com/api/credit.md). | | `TransactionResult refund(Refs.OrderRef order)` | `CCCREDIT` | Refund in full. | | `TransactionResult forceCredit(TransactionRequest req)` | `CCCREDIT` + `FORCE_CREDIT` | Credit with no referenced original. Needs MID provisioning. | | `OrderStatus status(Refs.OrderRef order)` | `CCSTATUS` | Order-level net position. See [Status](https://developer.inoviopay.com/api/status.md). | | `OrderStatus status(Refs.XtlOrderId xtlOrder)` | `CCSTATUS` | The same, keyed by your own order id. | | `TransactionResult updateOrder(Refs.OrderRef order, OrderUpdate update)` | `CCTRANSUPDATE` | Attach a receipt and UDF metadata. | | `Tokenize.Result tokenize(Card card)` | token service | Needs `Options.siteKey`. See [Tokenization](https://developer.inoviopay.com/api/tokenization.md). | | `Tokenize.Result tokenize(Card card, String uniqueId)` | token service | The same, with your own request id. | | `HealthResult testAuth()` | `TESTAUTH` | Verify credentials without transacting. | | `HealthResult testAvailability()` | `TESTGW` | Verify gateway availability. Safe to poll. | `captureLineItem()` requires the parent order as well as the line item. The gateway rejects `REQUEST_REF_PO_LI_ID` on its own with API 113 "Invalid Data", and `LineItemRef` does not carry its order, so both must be passed. This was verified against the live gateway. > **Two capabilities are PHP-only in this alpha** > `reverse()` and `reverseCapture()` take only the order reference here and > never send `CREDIT_ON_FAIL`, and there is no `threeDSecure()` sub-client. See > [Reverse versus credit](https://developer.inoviopay.com/sdks/concepts.md#reverse-versus-credit) and > [3D Secure](https://developer.inoviopay.com/sdks/concepts.md#3d-secure) for what to do instead. ## Handling results `r.status()` carries the answer. A decline is a return value, not an exception, so branch on the status rather than wrapping in try/catch for the payment outcome: ```java TransactionResult r = client.sale(req); show("status", r.status()); show("order", r.orderRef() == null ? "-" : r.orderRef().poId()); show("amount", r.amount() == null ? "-" : r.amount().toWire() + " " + r.amount().currency()); switch (r.status()) { case APPROVED: show("next", "fulfil the order"); break; case DECLINED: // The service tier carries the decline taxonomy dunning needs. boolean retry = r.serviceClassification() != null && r.serviceClassification().retryable(); show("next", retry ? "retry later" : "do not retry"); break; case PENDING: show("next", "complete " + (r.nextAction() == null ? "?" : r.nextAction().kind())); break; default: show("next", "inspect result.outcome()"); } ``` Because the status enum is not compiler-checked for exhaustiveness on Java 11, that `default` branch is doing real work. Do not omit it. Reference keys sit flat on the result, not inside a nested bag, because they are the ones you reach for most: `client.capture(r.orderRef(), amount)`. The available accessors are `orderRef()`, `xtlOrderRef()`, `transactionId()`, `requestId()`, `batchId()`, `customerRef()`, `savedCardRef()`, `membershipRef()` and `lineItemRefs()`. The gateway sends codes; the SDK adds labels. `r.serviceClassification().retryable()`, `.terminal()` and `.stopRecurring()`, plus `r.avs().classification()`, are labels the SDK derives from the response codes, not values the gateway sent. The codes themselves are on `r.outcome()` and the untouched wire fields on `r.raw()`. See [Outcome tiers](https://developer.inoviopay.com/sdks/concepts.md#outcome-tiers). Exceptions extend `InovioException`, itself a `RuntimeException`: `AuthenticationException`, `ValidationException` (carrying the offending `refField`), `ConfigurationException`, `TransportException`, `GatewayTimeoutException` (a subclass of `TransportException`) and `RateLimitException`. ## Tokenization `tokenize()` exchanges a PAN for a single-use `TOKEN_GUID` that replaces `PMT_NUMB` on a later sale or authorize. It hits `token_service.cfm` with HMAC header authentication rather than username and password, so it needs `Options.siteKey`. Without it the call throws a `ValidationException` before any network traffic, and the service itself would answer error 121. ```java // Tokenize on the site that holds the HMAC key... Tokenize.Result t = tokenClient().tokenize( PaymentMethods.card(PAN, EXPIRY, CVV)); t.token().guid(); t.tokenReqId(); // quote this to support // BIN metadata is best-effort — null when the BIN is not in the table. t.card().brand; t.card().bank; // The token replaces the PAN ONLY: expiry (and CVV) still travel with it, // which tokenize() carries forward for you. TransactionRequest req = Harness.request("TOK", "10.00"); req.paymentMethod = t.token(); TransactionResult sale = client().sale(req); ``` The signed message **excludes the PAN**, contrary to what the v4.14 PDF says. The gateway validates `hmac_sha256(timestamp || unique_id || site_id, site_key)`, verified against the live token service. The SDK signs the way the gateway behaves. For a merchant-hosted signature endpoint that lets a browser post the PAN directly, use the static helpers on their own: ```java import com.inoviopay.gateway.Tokenize; String timestamp = Tokenize.timestamp(); // YYYYMMDDHHMMSS UTC String signature = Tokenize.signRequest(siteKey, timestamp, uniqueId, siteId); // Return {signature, timestamp, siteId} to the browser. Never the site key. ``` > **tokenize() runs on your server** > The card number passes through your infrastructure. The browser Hosted Fields > client that would keep it in the cardholder's browser is not yet available. > See [Where the card number goes](https://developer.inoviopay.com/sdks/index.md#where-the-card-number-goes). ## 3D Secure This SDK carries the request-side and result-side 3DS surface, but not the server-leg driver. What is here: - **`RequestParts.BrowserData`** on a request, which the request builder maps to the `P3DS_BROWSER_LANGUAGE`, `USER_AGENT_XTL` and `P3DS_BROWSER_HEADER` fields. **These are required for gateway 3DS**: without them the gateway silently skips authentication entirely. - **The challenge next action.** When a result is `PENDING` because a challenge is required, `r.nextAction().kind()` is `"threeDSChallenge"` and the object carries the redirect URL, the JWT and the processor transaction id, which is everything your page needs to post into the visible iframe. ```java req.browser = new RequestParts.BrowserData(language, userAgent, acceptHeader); TransactionResult r = client.sale(req); if (r.status() == TransactionStatus.PENDING && r.nextAction() != null && "threeDSChallenge".equals(r.nextAction().kind())) { r.nextAction().redirectUrl; r.nextAction().jwt; // Browser: POST jwt to redirectUrl in a visible iframe. } ``` > **The 3DS server legs are PHP-only in this alpha** > There is no `client.threeDSecure()` here. The prepare leg against > `3dsrequest.cfm` and the completion leg after the ACS challenge have to be > driven against [the 3DS endpoints](https://developer.inoviopay.com/api/3ds-gateway.md) directly, or through > the [PHP SDK](https://developer.inoviopay.com/sdks/php.md#3d-secure), which implements both. This SDK's > `BrowserData` also omits the optional EMVCo device fields (colour depth, > screen dimensions, time-zone offset) that the PHP one carries. ## Timeouts and reconciliation A timeout means the state is **unknown**, not failed. The gateway may have approved the charge and lost the response. ```java try { client.sale(req.idempotency("ORDER-555")); } catch (GatewayTimeoutException e) { log.warn(e.recoveryHint()); OrderStatus actual = client.status(Refs.xtlOrder(e.xtlOrderId())); // Do NOT retry blindly — that risks a double charge. } ``` `idempotency("ORDER-555")` defaults the mode to `RETURN_ORIGINAL`, so a retry of the same request returns the original result rather than charging twice. `status()` is also the reconciliation primitive. Partial captures, refunds and voids are separate transactions sharing one order, so net position is an order-level question: ```java // Build a multi-leg order: authorize 100, capture 60, refund 10. TransactionResult order = Harness.seedOrder(c, "STATUS", false, "100.00"); c.capture(order.orderRef(), Money.of("60.00", "USD")); c.refund(order.orderRef(), Money.of("10.00", "USD")); OrderStatus s = c.status(order.orderRef()); s.transactions().size(); // every leg: auth, captures, refunds, voids s.authorized().toWire(); s.captured().toWire(); s.refunded().toWire(); s.net().toWire(); // captured - refunded s.outstanding().toWire(); // authorized - captured // You can also look an order up by YOUR id: // c.status(Refs.xtlOrder("ORDER-555")); ``` ## Running the conformance tests ```bash mvn test # 19 conformance tests python3 scripts/generate_enums.py # regenerate enums from spec/spec-enums.json ``` The suite replays the shared cross-language corpus in `spec/conformance-fixtures.json`, the same fixtures the other three SDKs run, which is what keeps the four honest about producing identically shaped results. The enums under `src/main/java/com/inoviopay/gateway/enums/` are generated from `spec/spec-enums.json`, extracted from the v4.14 specification appendices. Do not hand-edit them; regenerate. ## Examples in the repo Fourteen runnable files in `examples/`, one per operation. They are real, executed code rather than markdown snippets, so they cannot silently drift from the API. ```bash javac -cp target/classes -d target/examples examples/*.java \ && java -cp target/classes:target/examples RunAll # all 14, mock transport java -cp target/classes:target/examples Example03Sale # just one ``` | File | Operation | |---|---| | `Example01TestAvailability.java` | `testAvailability()`, the `TESTGW` health check | | `Example02TestAuth.java` | `testAuth()`, credential verification with no transaction | | `Example03Sale.java` | `sale()`, authorize and capture in one step | | `Example04Authorize.java` | `authorize()`, holding funds and keeping the order ref | | `Example05Capture.java` | `capture()`, full or partial | | `Example06CaptureLineItem.java` | `captureLineItem()`, which needs order, item and amount | | `Example07Reverse.java` | `reverse()`, voiding an authorization | | `Example08ReverseCapture.java` | `reverseCapture()`, voiding a capture pre-settlement | | `Example09Refund.java` | `refund()`, returning captured funds | | `Example10ForceCredit.java` | `forceCredit()`, which needs MID provisioning | | `Example11Status.java` | `status()`, reconciliation and net position | | `Example12UpdateOrder.java` | `updateOrder()`, attaching receipts for compliance | | `Example13Tokenize.java` | `tokenize()`, PAN to single-use token | | `Example14TimeoutRecovery.java` | The pattern that prevents double charges | `Harness.java` holds the shared mock transport and request builder; `RunAll.java` runs the set. By default they run against a mock transport: no credentials, no network, no money moves, safe in CI. Set `INOVIO_LIVE=1` plus credentials to run the same code against a real gateway: ```bash INOVIO_LIVE=1 \ INOVIO_USER=... INOVIO_PASS=... INOVIO_SITE_ID=... INOVIO_MERCH_ACCT_ID=... \ INOVIO_SITE_KEY=... # token service HMAC key INOVIO_TOKEN_SITE_ID=... # only if tokenizing on a different site java -cp target/classes:target/examples RunAll ``` Live mode creates **real transactions**, so point it at a test environment. Running live is worth doing before you trust an integration: mocks only replay responses you already believed in, so they cannot catch request-side errors. Every example that operates on an existing order builds its own first, rather than hardcoding an id that resolves only against a mock. Two examples need provisioning that a fresh account will not have. `forceCredit` fails with API 104 "Invalid service action" unless the MID has `FORCE_CREDIT` enabled, which is an authentication-tier error rather than a decline. `tokenize` needs the per-site HMAC key from Inovio support, which may live on a different site than your gateway credentials. --- # Shopping cart plugins Tokenized direct-post card checkout for WooCommerce, Magento 2, PrestaShop 9 and OpenCart 4, on the Inovio gateway. Source: https://developer.inoviopay.com/carts/index.html Markdown: https://developer.inoviopay.com/carts/index.md Inovio ships four first-party cart plugins. They are separate codebases, one per platform, but they are the same integration: the same PHP SDK, the same tokenized direct-post card entry, the same refund contract, and the same five credentials from Inovio. > **Note** > The plugins are functional pre-release. Every verb documented here has been > exercised end to end against the gateway, and each plugin ships a Playwright > suite that drives a real browser through the real storefront and admin. They > are not yet hardened for production. | Platform | Page | Package | |---|---|---| | WooCommerce | [WooCommerce](https://developer.inoviopay.com/carts/woocommerce.md) | `inovio-payment-gateway` | | Magento 2 / Mage-OS | [Magento 2](https://developer.inoviopay.com/carts/magento2.md) | `inovio/module-payment-gateway` | | PrestaShop 9 | [PrestaShop](https://developer.inoviopay.com/carts/prestashop.md) | `inoviopayment` | | OpenCart 4 | [OpenCart](https://developer.inoviopay.com/carts/opencart.md) | `inovio.ocmod.zip` | ## What they have in common All four use **tokenized direct post**. The card number never reaches the store server. 1. Card fields render in the store's own checkout page, in the merchant's own DOM. There is no hosted payment page and no Inovio-hosted iframe to redirect to. 2. The plugin's checkout JavaScript asks the store server for an HMAC signature. The store mints it from the **Site Key** and returns it. The store never sees the card. 3. The browser POSTs the PAN, expiry and CVV **directly to Inovio's `token_service.cfm`**, with that signature in the `X-timestamp` and `X-signature` headers, and gets back a single-use `TOKEN_GUID`. 4. Only the `TOKEN_GUID` (plus the brand and last four digits, which are display-only fields) is submitted to the store. 5. The store server sends the transaction to `pmt_service.cfm` through the [PHP SDK](https://developer.inoviopay.com/sdks/php.md), carrying the token instead of a card number. The signature covers `timestamp + uniqueId + siteId`. **The PAN is not part of the signed message.** The v4.14 PDF says it is, and its worked example agrees; both are wrong. Verified against the gateway: the token service validates the signature without the PAN, and including it yields error 121, "signature match fail". The SDK's `Tokenize::signRequest()` is the single source of truth. Never hand-roll this hash. No Inovio-hosted infrastructure is required: there is no hosted payment page and no iframe you have to redirect to. In each plugin this was verified rather than assumed: a full database dump taken after a live test run contains zero occurrences of the test card number in any representation. All four consume the same **[PHP SDK](https://developer.inoviopay.com/sdks/php.md)** (`inovio/gateway-sdk`). None of them touch raw `REQUEST_ACTION` wire fields. Magento requires it through Composer; PrestaShop and OpenCart vendor a verbatim copy into the shipped package, because the SDK is not on Packagist and both platforms install as a self-contained archive through the admin UI with no Composer step. ## Capability matrix Values come from each plugin's own README. A blank behaviour is not implied anywhere: where a platform does something differently, the cell says so. | | WooCommerce | Magento 2 | PrestaShop 9 | OpenCart 4 | |---|---|---|---|---| | Sale | Yes | Yes | Yes | Yes | | Authorize only | Yes | Yes | Yes | Yes | | Capture, full | Yes | Yes | Yes | Yes | | Capture, partial | No, full capture only (WooCommerce has no capture-amount field) | Yes, via Magento invoicing | Yes | Yes | | Void | Yes | Yes | Yes | Yes | | Refund, full | Yes | Yes | Yes | Yes | | Refund, partial | Yes | Yes | Yes | Yes | | Saved cards | Native `WC_Payment_Tokens` | Magento Vault | Module's own table plus a customer-account UI | Module's own table, rendered into OpenCart's native account hook | | 3D Secure | Yes, two-token flow | Yes, frictionless and challenge | Yes, with a module-created pending order state | Yes, enrollment / DDC / challenge | | Block or modern checkout | Both the classic shortcode and the Block Checkout | Standard Magento checkout | Standard PrestaShop 9 checkout | Standard OpenCart 4 checkout | | Timeout reconciliation | Yes | Yes, via `status()` | Yes, via `status()` | Yes, via `CCSTATUS` | Three details the matrix flattens: - **WooCommerce saved cards are checkout-type dependent.** A shopper can *use* an existing saved card on either the classic checkout or the Block Checkout, but can only *save a new* card on the classic checkout. See [WooCommerce known gaps](https://developer.inoviopay.com/carts/woocommerce.md#known-gaps). - **Magento does not hide itself on incomplete credentials.** WooCommerce, PrestaShop and OpenCart all withhold the payment method at checkout until all five required fields are filled in. Magento will show the method and then fail. - **Wallets are out of scope in all four.** Apple Pay and Google Pay are gateway features (see [Apple Pay](https://developer.inoviopay.com/api/apple-pay.md) and [Google Pay](https://developer.inoviopay.com/api/google-pay.md)) but are not wired into any cart plugin yet, and neither are LatAm rails or raw-card admin MOTO orders. ## Requirements | | WooCommerce | Magento 2 | PrestaShop 9 | OpenCart 4 | |---|---|---|---|---| | Platform | WordPress 6.0+, WooCommerce 7.0+ (Block Checkout needs 8.3+) | Magento 2.4.4+, or Mage-OS | PrestaShop 9.0+ | OpenCart 4.1.x, verified against 4.1.0.4 | | PHP | 8.0+ | 8.1+ | 8.1+ | 8.1+ | | Extensions | `bcmath`, `curl`, `json` | `bcmath`, `curl`, `json` | `bcmath`, `curl`, `json` | `bcmath`, `curl` | `bcmath` is required, not optional, in every one of them. The SDK's `Money` type does every decimal calculation through it so amounts never touch a binary float. Installation fails without it. Magento 2.4 requires `bcmath` in its own right, so a compliant Magento install already has it; OpenCart's settings screen refuses to enable the payment method without it. PrestaShop is 9.x only. PrestaShop 8.0 through 8.2 supports PHP 7.2.5 through 8.1 and no higher, while 9.x requires 8.1+; the two lines overlap at exactly PHP 8.1 and maintain separate documentation branches. PrestaShop 1.7 is end of life. ### What you need from Inovio The same five values on every platform. | Credential | Gateway field | Required | |---|---|---| | API username | `REQ_USERNAME` | Yes | | API password | `REQ_PASSWORD` | Yes | | Site ID | `SITE_ID` | Yes | | Site Key | (HMAC signing secret, not sent as a field) | Yes | | Gateway Product ID | `LI_PROD_ID` | Yes | | Merchant Account ID | `MERCH_ACCT_ID` | No | ## The five credentials explained **API username and API password** are your gateway API credentials, sent as `REQ_USERNAME` and `REQ_PASSWORD` on every transaction. See [Authentication](https://developer.inoviopay.com/api/authentication.md). **Site ID** is `SITE_ID`, the gateway site the transactions belong to. **Site Key** is a **separate per-site HMAC secret issued by Inovio support**. It is not the API password, and it is not something you can generate. It is used for exactly one thing: signing the browser's tokenization request. Without it the browser cannot tokenize and checkout fails with **error 121**, "Get CCtoken GUID signature match fail". This is the single most common misconfiguration, and the symptom is a card form that never produces a token rather than an obvious credential error. **Gateway Product ID** is `LI_PROD_ID`, a product registered on the **gateway**. It is not a SKU from your store catalogue. The whole order bills as one line item under it, regardless of how many products the cart contains. See [Line items](https://developer.inoviopay.com/api/line-items.md). **Merchant Account ID** is `MERCH_ACCT_ID` and is optional in all four plugins. Leave it empty to let the gateway distribute by currency and country. Every plugin also exposes a gateway endpoint setting. In all four, the tokenization endpoint (`token_service.cfm`) and the 3-D Secure endpoint (`3dsrequest.cfm`) are **derived from the `pmt_service.cfm` URL** by suffix rewrite, exactly as the SDK derives them, so the three cannot drift apart. There is one setting, not three. Leave it at the default for production. ## Refunds are settlement-aware This is a shared design across all four plugins, and it is the one place where the obvious implementation loses money. The gateway rejects a credit (`CCCREDIT`) against an order that has **not settled yet** with `SERVICE 536`, "Order not settled: Please reverse". Before settlement the correct undo verb is a reversal (`CCREVERSECAP`), not a credit. A naive refund-only button therefore fails on every same-day refund. The plugins do **not** pre-check settlement and pick a verb themselves. The gateway owns that decision: **Full refund** issues `reverseCapture($ref, creditOnFail: true)`, which sends `CCREVERSECAP` with `CREDIT_ON_FAIL=1`. When the transaction is already settled and cannot be reversed, **the gateway itself re-routes to `CCCREDIT`**. One call, one round trip, and the plugin never has to know the settlement state. This is documented in v4.14 §5.4.2, confirmed in the gateway's own order handling, and used by the gateway internally on system auto-voids. Verified live on both paths: | Case | Request | Response | |---|---|---| | Unsettled capture | `CCREVERSECAP` with `CREDIT_ON_FAIL=1` | `REQUEST_ACTION=CCREVERSECAP`, APPROVED | | Settled capture | `CCREVERSECAP` with `CREDIT_ON_FAIL=1` | `REQUEST_ACTION=CCCREDIT`, APPROVED | | Control: settled, no flag | `CCREVERSECAP` | `SERVICE 515`, "Order fully credited" | **Partial refund** issues `refund($ref, $money)`, which is always `CCCREDIT`, because a reversal takes no amount and voids the entire capture. Against an unsettled order the gateway returns 536, and the plugin **fails loudly**: "order not settled, partial refunds available after settlement". The merchant sees the error, no money moves, and the refund is retried after settlement. > **There is no module-side fallback, and you must not add one** > Every plugin previously carried a settlement pre-check and a 536 fallback > branch. Both are deleted. The pre-check dropped the requested amount on the > unsettled path, so a partial refund of $10 against an unsettled $100 order > reversed the full $100 while the store recorded $10. The fallback branch was > dead code: it tested for status `FAILED`, but a real 536 arrives as > `DECLINED`. If you are extending a plugin, let the gateway route the verb. See [Reversal](https://developer.inoviopay.com/api/reversal.md) and [Credit](https://developer.inoviopay.com/api/credit.md) for the underlying request types, and [service codes](https://developer.inoviopay.com/reference/service-codes.md) for 536. ## The statement descriptor warning > **The statement descriptor must not contain a space, underscore or slash** > The gateway rejects the **entire transaction** with `Invalid Data` if > `PMT_DESCRIPTOR` contains a space, an underscore or a forward slash. Every > sale fails, not just the descriptor. > > | Descriptor | Result | > |---|---| > | `ACME STORE` | Rejected | > | `ACME_STORE` | Rejected | > | `ACME/STORE` | Rejected | > | `ACME-STORE` | Accepted | > | `ACMESTORE` | Accepted | > | `ACME.STORE` | Accepted | > > The full allowed set is `A-Z`, `a-z`, `0-9`, and `.` `-` `*` `+` `&` `@`. > This was mapped empirically by sending each candidate character in an > otherwise-identical approved request. A multi-word descriptor is the natural > thing for a merchant to type, and it kills every sale. The SDK now rejects an > invalid descriptor rather than letting the gateway kill the sale. If you are > unsure, leave the field empty. ## Two-token 3D Secure When 3-D Secure is enabled, checkout mints **two** `TOKEN_GUID`s from the same card entry. Gateway tokens are single-use and the enrollment leg consumes one, so token A drives enrollment and token B completes the transaction after the cardholder finishes the challenge. Both resolve to the same card at the gateway, so the authentication carries across the two legs. Reusing token A on the completion leg fails with `API 401 Invalid TOKEN_GUID`, verified against the live gateway. This is the most tempting thing in any of the four codebases to collapse into one token, and doing so produces the worst possible failure mode: it appears to work for every frictionless card, which never reaches the completion leg at all, and fails only for cards that are actually challenged. See [3-D Secure through the gateway](https://developer.inoviopay.com/api/3ds-gateway.md). ## Testing The same test cards and decline triggers apply on all four platforms, against the **Inovio test gateway** only. | Card | Behaviour | |---|---| | `4111111111111111` | Approves, no 3-D Secure challenge | | `4000000000002503` | Triggers a 3-D Secure challenge. Sandbox OTP `1234` | Any future expiry date and any CVV are accepted. The sandbox decides declines from the **whole order total**, matched exactly. The PAN, expiry and CVV are not consulted at all. | Order total | Result | |---|---| | `6.35` | Declined, Insufficient Funds | | `5.06` | Declined, Fraud | The total must land on the trigger amount exactly, including tax and shipping, not the product price. This is also why a test order can decline unexpectedly: check the total before assuming the card or the credentials are at fault. See [Testing](https://developer.inoviopay.com/api/testing.md). ## Source All four repositories are private. They are available from Inovio. - [Inoviopay/inovio-gateway-woocommerce](https://github.com/Inoviopay/inovio-gateway-woocommerce) - [Inoviopay/inovio-gateway-magento2](https://github.com/Inoviopay/inovio-gateway-magento2) - [Inoviopay/inovio-gateway-prestashop](https://github.com/Inoviopay/inovio-gateway-prestashop) - [Inoviopay/inovio-gateway-opencart](https://github.com/Inoviopay/inovio-gateway-opencart) --- # WooCommerce The Inovio Payment Gateway plugin for WooCommerce, with tokenized direct-post card entry on both the classic and Block checkouts. Source: https://developer.inoviopay.com/carts/woocommerce.html Markdown: https://developer.inoviopay.com/carts/woocommerce.md Repository: https://github.com/Inoviopay/inovio-gateway-woocommerce Tokenized direct-post card checkout for WooCommerce, on the Inovio gateway. The card number never reaches the WordPress server. ## Overview and status > **Functional pre-release** > Sale, authorize, capture, void, refund, vault and 3-D Secure are implemented > and have been exercised end to end against the gateway. A 10-spec Playwright > suite drives the real storefront and the real wp-admin. Not yet hardened for > production. The plugin was ported from the PrestaShop module, which carries the full design rationale. What makes the WooCommerce build different from its siblings is that WooCommerce has a native payment-token API, so saved cards use the platform's own vault with no custom UI, and that WooCommerce ships two entirely different checkouts. Both are supported. ## Requirements | | | |---|---| | WordPress | 6.0+ | | WooCommerce | 7.0+. Block Checkout support needs 8.3+ | | PHP | 8.0+ | | PHP extensions | `bcmath` (required), `curl`, `json` | | From Inovio | API username, API password, Site ID, Site Key, Gateway Product ID | `bcmath` is required, not optional. The SDK's `Money` type does every decimal calculation through it so amounts never touch a binary float. Installation fails without it. ## Install Copy the `inovio-payment-gateway/` directory into `wp-content/plugins/`, or upload the ZIP through **Plugins, Add New, Upload Plugin**. Then activate it. Configure it under **WooCommerce, Settings, Payments, Inovio**. **WP-CLI** ```bash wp plugin activate inovio-payment-gateway ``` **Admin UI** ```text Plugins -> Installed Plugins -> Inovio Payment Gateway -> Activate ``` ## Configuration All settings live on the Inovio payment method screen under **WooCommerce, Settings, Payments**. ### Gateway credentials | Setting | Required | Meaning | |---|---|---| | Enable/Disable | Yes | Turns the payment method on. | | Title | No | The payment method name shoppers see at checkout. Defaults to "Credit card". | | Description | No | Shown beneath the payment method name at checkout. | | API Username | Yes | Gateway `REQ_USERNAME`. | | API Password | Yes | Gateway `REQ_PASSWORD`. | | Site ID | Yes | Gateway `SITE_ID`. | | Merchant Account ID | No | `MERCH_ACCT_ID`. Leave empty to let the gateway distribute by currency and country. | | Site Key | Yes | The per-site HMAC secret. Not the API password. | | Gateway Product ID | Yes | `LI_PROD_ID`. Not a WooCommerce SKU. | | Gateway Endpoint | No | The `pmt_service.cfm` transaction URL. The tokenization and 3-D Secure endpoints are derived from it, exactly as the SDK derives them, so the three cannot drift apart. Leave it at the default for production. | **Site Key** is a separate per-site HMAC secret issued by Inovio support. It is not the API password, and it is not something you can generate. It is used only to sign the browser's tokenization request. Without it the browser cannot tokenize and checkout fails with error 121. **Gateway Product ID** (`LI_PROD_ID`) is a product registered on the gateway, not a product or SKU from your WooCommerce catalogue. The whole order bills as one line item under it. Until API Username, API Password, Site ID, Site Key and Gateway Product ID are all filled in, `is_configured()` returns false and the method hides itself at checkout rather than presenting a card form it cannot process. ### Payment behaviour settings | Setting | Required | Meaning | |---|---|---| | Payment Action | Yes | *Sale* charges at checkout. *Authorize only* reserves the funds and leaves the order on hold until you capture it from the order screen. | | 3-D Secure | No | Enables 3-D Secure authentication. Requires a 3DS-configured merchant account. Off by default. | | Saved Cards | No | Lets logged-in shoppers save cards for reuse. On by default. Stores the gateway's own card references only, never a card number. | | Statement Descriptor | No | `PMT_DESCRIPTOR`, what appears on the cardholder's statement. See the warning below. | | Descriptor Phone | No | `PMT_DESCRIPTOR_PHONE`, support number shown alongside the descriptor. | | Debug Logging | No | Logs gateway activity to **WooCommerce, Status, Logs**. Declines and errors are always logged. Card numbers are never logged, because they never reach this server. | > **The statement descriptor must not contain a space, underscore or slash** > The gateway rejects the **entire transaction** with `Invalid Data` if > `PMT_DESCRIPTOR` contains a space, an underscore or a forward slash. Every > sale fails, not just the descriptor. > > | Descriptor | Result | > |---|---| > | `ACME STORE` | Rejected | > | `ACME_STORE` | Rejected | > | `ACME/STORE` | Rejected | > | `ACME-STORE` | Accepted | > | `ACMESTORE` | Accepted | > | `ACME.STORE` | Accepted | > > The full allowed set is `A-Z`, `a-z`, `0-9`, and `.` `-` `*` `+` `&` `@`. > This was mapped empirically by sending each candidate character in an > otherwise-identical approved request; the SDK now rejects an invalid > descriptor rather than letting the gateway kill the sale. If you are unsure, > leave the field empty. ## Payment behaviour | Capability | Support | |---|---| | Sale | Authorize and capture in one step at checkout. | | Authorize only | Reserve funds, capture later from the order screen. | | Capture | Admin order action when the payment action is authorize-only. | | Void | Reverse an uncaptured authorization, from the order screen. | | Refund | Full and partial, through WooCommerce's own admin refund UI (`process_refund()`). Settlement-aware. | | Saved cards | Native `WC_Payment_Tokens`. Saved cards appear in the shopper's account and at checkout with no custom UI. | | 3-D Secure | Device-data collection and challenge, two-token flow. | | Checkout | Both the classic `[woocommerce_checkout]` shortcode and the Block Checkout (`woocommerce/checkout`). A default WooCommerce install works out of the box. | Refunds follow the shared settlement-aware contract described in [Refunds are settlement-aware](https://developer.inoviopay.com/carts/index.md#refunds-are-settlement-aware). A full refund issues one `reverseCapture` call with `creditOnFail`, and the gateway decides between reversing and crediting. A partial refund issues `refund` and fails loudly with service code 536 if the order has not settled yet; retry it after settlement. A gateway timeout means the transaction state is genuinely unknown. The plugin reconciles via `status()` before failing an order, including on the 3-D Secure completion leg, rather than letting the shopper retry into a possible double charge. ## How checkout works on this platform ### The direct-post flow Card fields live in the checkout page's DOM. The shipped JavaScript reads them, asks the plugin for an HMAC signature, exchanges the PAN for a single-use `TOKEN_GUID` by POSTing directly to Inovio, and submits only that token to WordPress. Nothing in the payment path on your server ever sees a card number. This holds identically on both checkout types, because both scripts tokenize the same way and both hand the same field names to the same `process_payment()`. No Inovio-hosted infrastructure is required: there is no hosted payment page and no iframe you have to redirect to. Saved cards store the gateway's own references (`CUST_ID`, `PMT_ID`) plus the card brand and last four digits, which are display-only fields. No PAN is ever stored. Orders store only gateway references (`_inovio_po_id`, `_inovio_trans_id`, `_inovio_req_id`). This was verified, not assumed: a full `mysqldump` of the WordPress database taken after a live test run contains zero occurrences of either test card number, in any representation (plain, spaced, dashed). ### Two checkouts, one server implementation A default WooCommerce install of 8.3 or later puts the Block Checkout on the checkout page, not the classic shortcode. `WC_Payment_Gateway` classes are invisible to it on their own, so the plugin also registers `Inovio_Blocks_Support` (extending `Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType`) on the `woocommerce_blocks_payment_method_type_registration` hook, and ships a second checkout script registered via `wc.wcBlocksRegistry.registerPaymentMethod`. That script performs the same direct-post tokenization as the classic script. The tokenize, validate and 3DS helper functions are deliberately mirrored between the two files rather than shared, because the Block Checkout and the classic checkout are different JS runtimes with different script-dependency graphs and there is no bundler in this plugin. If you change one, keep the behaviour identical in the other. Server-side there is only **one** implementation. `Inovio_Payment_Gateway::process_payment()` is unchanged. `Automattic\WooCommerce\StoreApi\Legacy::process_legacy_payment()` sets `$_POST` from the Blocks `paymentMethodData` before calling it, so `collect_payment_data()`'s existing `$_POST` reads pick up the Blocks-minted tokens exactly as they pick up the classic form's hidden fields. Same field names, same code path, same PAN-safety guarantees. ### 3-D Secure on the Block Checkout The Store API's `payment_result.payment_details` response schema types every value as a plain `string` (`CheckoutSchema::get_item_schema()`). A nested-array 3DS payload gets silently coerced by WordPress's REST schema sanitizer into the literal string `"Array"` on the Blocks path, a fully swallowed failure with no error anywhere. This was verified empirically. The contract for both checkout types is therefore that `inovio_3ds` travels as a **JSON string**, parsed back into an object by whichever checkout script reads it. The Blocks 3DS challenge overlay is driven from `onCheckoutSuccess` (`window.wc.blocksCheckoutEvents.checkoutEvents`), which the Store API awaits via `emitWithAbort` before marking checkout complete. Returning a promise from the handler holds the shopper on the checkout page, challenge overlaid on top, until the ACS challenge resolves, instead of navigating to a thank-you page for a payment that has not actually completed. A failed challenge resolves with an `ERROR` response instead of `SUCCESS`, which keeps the shopper on checkout with a notice; the order itself was already left `pending` server-side by `begin_challenge()`, so there is nothing to undo. For the two-token mechanics that apply on both checkouts, see [Two-token 3D Secure](https://developer.inoviopay.com/carts/index.md#two-token-3d-secure). ## Admin operations | Operation | Where | What happens | |---|---|---| | Capture | The order screen, when Payment Action is *Authorize only* | Captures the reserved funds. | | Void | The order screen | Reverses an uncaptured authorization. | | Refund | WooCommerce's own refund UI on the order screen | Calls `process_refund()`. Full and partial both supported; the verb is chosen as described above. | Refunds go through WooCommerce's native admin refund control, not a plugin-specific button, so the merchant workflow is the standard one. Debug logging, when enabled, writes gateway activity to **WooCommerce, Status, Logs**. ## Saved cards Saved cards use `WC_Payment_Tokens`, WooCommerce's own vault. Unlike PrestaShop and OpenCart, Woo has a token API, so saved cards appear in the shopper's account and at checkout with no custom UI from the plugin. Saved-card **reuse** works on the Block Checkout without any extra code. WooCommerce's own saved-payment-token UI renders there regardless of what a payment method declares, because it comes from `WC_Payment_Tokens` rather than from `get_supported_features()`, and it submits `wc-inovio-payment-token` generically as part of the Store API's payment data, which `collect_payment_data()` already reads unchanged on either checkout type. This was verified live: an existing saved Visa was charged through the Block Checkout with zero new code and zero new tokens created. When a saved card is selected, the new-card fields are hidden. This matters: WooCommerce auto-selects a stored card, and a saved-card payment needs no tokenization, so anything typed into the new-card fields would be silently dropped and the stored card charged instead. Hiding them prevents a shopper who types a different card from being charged the old one. ## Known gaps - **Saving a new card is classic-checkout-only.** The classic checkout renders its own "Save this card for future purchases" checkbox (`$this->save_payment_method_checkbox()`). The Blocks card-entry component does not render an equivalent, and `Inovio_Blocks_Support::get_supported_features()` deliberately omits `'tokenization'`, so WooCommerce Blocks' own built-in save-option checkbox does not appear either. Verified live: entering a new card on the Block Checkout shows no save-card control anywhere on the page. A shopper can charge a new card there, with and without 3DS, but cannot opt to save it for later. Reusing an already-saved card works on both checkouts. ## Verified end to end A 10-spec Playwright suite in `e2e/` drives the classic checkout through a real browser, with screenshots and video: ```bash cd e2e && npx playwright test ``` | Spec | What it proves | |---|---| | `01-sale.spec.js` | Add to cart, pay with a new card, order confirms. | | `02-vault.spec.js` | Save a card, then reuse it on a later order. | | `03-3ds.spec.js` | A real Cardinal ACS challenge is presented and completed by the shopper. | | `04-frictionless-3ds.spec.js` | With 3DS active and a frictionless PAN, no challenge appears and the order confirms. | | `05-decline.spec.js` | A declined card does not create a paid order. | | `06-refund.spec.js` | Refunding a Sale order from the WooCommerce admin order screen. | | `07-invalid-card-input.spec.js` | Luhn-failing PAN, short PAN, short CVV and a past expiry are all rejected client-side, with no order created. | | `08-blocks-checkout.spec.js` | A real order completes through the actual Block Checkout DOM, placed by the same server-side `process_payment()` the classic specs use. | | `09-saved-card-ux.spec.js` | With a saved card selected, the new-card fields are hidden rather than editable-but-ignored. | The suite requires the checkout page to be on the classic `[woocommerce_checkout]` shortcode; `playwright.config.js`'s global setup switches the page to it and the teardown restores the Block Checkout. Spec 08 needs the opposite and switches the page itself, so it is self-contained regardless of run order. Block Checkout was also verified live outside the suite, with the checkout page switched to ``: a new card without 3DS (`PO_ID 18193587`, `TRANS_ID 2001994863`, zero browser console errors, genuine `.wp-block-woocommerce-checkout` DOM confirmed rather than a fallback render), and a new card with a real Cardinal step-up OTP entered through the nested cross-origin ACS iframe (`PO_ID 18193577`, `TRANS_ID 2001994852`, `ECI 05`, with an order note confirming 3-D Secure authentication completed). A full `mysqldump` after these runs contained zero occurrences of either test PAN. There is also a CLI script, `tests/e2e-order.php`, that places a real order through the plugin's own gateway class, performing exactly the two steps the checkout JS does in the browser and handing only the resulting token to `process_payment()`. ## Troubleshooting | Symptom | Cause | Fix | |---|---|---| | No payment method appears at checkout | One of the five required credential fields is empty, so the method hides itself. | Fill in API Username, API Password, Site ID, Site Key and Gateway Product ID. | | Still no payment method, and credentials are complete | The checkout page is neither the classic `[woocommerce_checkout]` shortcode nor the `woocommerce/checkout` block, for example a page builder's own checkout. | Put the checkout page on one of the two supported checkout types. | | Error 121 at checkout, or the card form never tokenizes | Site Key missing or wrong. | Enter the per-site HMAC Site Key from Inovio support. It is not the API password. | | `Invalid Data` returned on every transaction | The statement descriptor contains a space, underscore or forward slash. | Remove them, or clear the field. | | No "save this card" control on the Block Checkout | Known gap. Vaulting a new card is classic-checkout-only. | Reusing an already-saved card works on both. | | Refund fails with `SERVICE 536` | The order has not settled, so a partial credit is the wrong verb. | Retry the partial refund after settlement. A full refund is routed by the gateway and does not hit this. | | A test transaction declines with Insufficient Funds | The Inovio test gateway decides from the order total, not the card. | Change the order total. See [Testing](https://developer.inoviopay.com/carts/index.md#testing). | ## Source Repository: [Inoviopay/inovio-gateway-woocommerce](https://github.com/Inoviopay/inovio-gateway-woocommerce) (private, available from Inovio). Plugin directory: `inovio-payment-gateway/`, installed to `wp-content/plugins/inovio-payment-gateway/`. --- # Magento 2 The Inovio Payment Gateway module for Magento 2 and Mage-OS, built on Magento's payment-provider gateway with Vault and 3-D Secure. Source: https://developer.inoviopay.com/carts/magento2.html Markdown: https://developer.inoviopay.com/carts/magento2.md Repository: https://github.com/Inoviopay/inovio-gateway-magento2 Tokenized direct-post card checkout for Magento 2 and Mage-OS, on the Inovio/Argus gateway. The card number never reaches the Magento server. ## Overview and status > **Functional pre-release** > Sale, capture, refund, void, vault and 3-D Secure are implemented and have > been exercised end to end against the gateway. An 8-spec Playwright suite > drives the real storefront and the real admin. Not yet hardened for > production: there is no automated unit-test suite for the module beyond the > correctness-critical gateway logic. The module is a formal Magento payment-provider gateway, with a command pool (`authorize`, `sale`, `capture`, `refund`, `void`) and Magento Vault for stored cards. All settings are scoped to default, website and store view, so a multi-site install can use different credentials per site. ## Requirements | | | |---|---| | Magento | 2.4.4+, or Mage-OS | | PHP | 8.1+ | | PHP extensions | `bcmath` (required), `curl`, `json` | | From Inovio | API username, API password, Site ID, Site Key, Gateway Product ID | `bcmath` is required, not optional. The SDK's `Money` type does every decimal calculation through it so amounts never touch a binary float. Installation fails without it. Magento 2.4 requires `bcmath` in its own right, so a compliant Magento install already has it. ## Install The module is installed with Composer. The SDK it depends on (`inovio/gateway-sdk`) is **not on Packagist**, so the module's own `composer.json` declares the VCS repository for it. You need GitHub access to that private repository. From the Magento root: ```bash composer require inovio/module-payment-gateway bin/magento module:enable Inovio_PaymentGateway bin/magento setup:upgrade bin/magento setup:di:compile bin/magento cache:flush ``` In production mode, also deploy static content: ```bash bin/magento setup:static-content:deploy ``` Then configure under **Stores, Configuration, Sales, Payment Methods, Inovio Payment Gateway**. > **Re-run `setup:di:compile` after any DI change** > Changing a constructor signature invalidates Magento's compiled DI. Removing > `generated/code/` without regenerating makes the payment method > silently vanish from checkout: no exception, just an absent radio button. > Worse, in developer mode Magento tries to regenerate lazily into > `generated/code/`, which is not writable by `www-data`, so every request dies > with a `main.CRITICAL` "Can't create directory" and every checkout fails > instantly. ## Configuration All settings live at **Stores, Configuration, Sales, Payment Methods, Inovio Payment Gateway**. ### Gateway credentials | Setting | Required | Meaning | |---|---|---| | Enabled | Yes | Turns the payment method on. | | Title | No | The payment method name shoppers see at checkout. | | API Username | Yes | Gateway `REQ_USERNAME`. | | API Password | Yes | Gateway `REQ_PASSWORD`. Stored encrypted by Magento. | | Site ID | Yes | Gateway `SITE_ID`. | | Merchant Account ID | No | `MERCH_ACCT_ID`. Leave empty to let the gateway distribute by currency and country. | | Site Key | Yes | The per-site HMAC secret. Not the API password. Stored encrypted. | | Gateway Product ID | Yes | `LI_PROD_ID`. Not a Magento SKU. | | Environment | Yes | *Production*, or *Custom URL (QA / testing)*. | | Custom Gateway URL | Only when Environment is *Custom* | The `pmt_service.cfm` URL for a non-production gateway. The token and 3-D Secure endpoints are derived from it, exactly as the SDK derives them, so the three cannot drift apart. | **Site Key** is a separate per-site HMAC secret issued by Inovio support. It is not the API password, and it is not something you can generate. It is used only to sign the browser's tokenization request. Without it the browser cannot tokenize and checkout fails with error 121. **Gateway Product ID** (`LI_PROD_ID`) is a product registered on the gateway, not a product or SKU from your Magento catalogue. The whole order bills as one line item under it. > **Set credentials through the admin UI, not directly in `core_config_data`** > `req_password` and `site_key` are obscure/encrypted fields. Writing a raw value > into the database bypasses Magento's encryption, and the module's `decrypt()` > then returns binary garbage, producing a signing key that is silently wrong. > The symptom is checkout stalling at the payment step with "Card could not be > processed", and the token service returning `error_code 121, Get CCtoken GUID > signature match fail`. An encrypted value in the database starts with `0:3:`; > a 64-character hex string is a plaintext value that will not work. ### Payment behaviour settings | Setting | Required | Meaning | |---|---|---| | Payment Action | Yes | *Authorize and Capture* charges at checkout. *Authorize Only* reserves the funds and leaves the order awaiting an invoice you raise from the order screen. | | Enable 3D Secure | No | Runs 3DS 2 device-data collection and, when the issuer demands it, a challenge at checkout. Requires a 3DS-configured merchant account. | | Enable Stored Cards (Vault) | No | Magento Vault: lets logged-in shoppers save cards for reuse. Stores the gateway's own card references only, never a card number. Off by default. | | Allowed Card Types | No | Which card brands the checkout form accepts. | | Statement Descriptor | No | `PMT_DESCRIPTOR`, what appears on the cardholder's statement. See the warning below. | | Descriptor Phone | No | `PMT_DESCRIPTOR_PHONE`, support number shown alongside the descriptor. | ### Availability | Setting | Meaning | |---|---| | Payment from Applicable Countries | All countries, or a specific list. | | Payment from Specific Countries | The list, when the setting above is restricted. | | Sort Order | Position of the method in the checkout payment list. | > **The statement descriptor must not contain a space, underscore or slash** > The gateway rejects the **entire transaction** with `Invalid Data` if > `PMT_DESCRIPTOR` contains a space, an underscore or a forward slash. Every > sale fails, not just the descriptor. > > | Descriptor | Result | > |---|---| > | `ACME STORE` | Rejected | > | `ACME_STORE` | Rejected | > | `ACME/STORE` | Rejected | > | `ACME-STORE` | Accepted | > | `ACMESTORE` | Accepted | > | `ACME.STORE` | Accepted | > > The full allowed set is `A-Z`, `a-z`, `0-9`, and `.` `-` `*` `+` `&` `@`. > This was mapped empirically by sending each candidate character in an > otherwise-identical approved request. A multi-word descriptor is the natural > thing for a merchant to type, and it kills every sale, so if you are unsure, > leave the field empty. ## Payment behaviour | Capability | Support | |---|---| | Sale | Authorize and capture in one step at checkout. | | Authorize only | Reserve funds, invoice later from the order screen. | | Capture | Full and partial, through Magento's own invoicing. | | Void | Reverse an uncaptured authorization. | | Refund | Full and partial, through Magento's own credit memos. | | Saved cards | Magento Vault, the platform's native vault, using the gateway's `CUST_ID` and `PMT_ID` references. | | 3-D Secure | Device-data collection and challenge; frictionless and challenge flows. | | Timeout recovery | Reconciles via `status()` before failing an order. | | Other | Multi-currency read-back, dynamic descriptors, AVS/CVV policy, idempotency on order increment ID. | Out of scope in v1: wallets (Apple Pay and Google Pay), LatAm rails, and raw-card admin MOTO orders. Refunds follow the shared settlement-aware contract described in [Refunds are settlement-aware](https://developer.inoviopay.com/carts/index.md#refunds-are-settlement-aware). A credit memo that covers everything still refundable on the order is a **full** refund and calls `reverseCapture($ref, creditOnFail: true)` in one gateway call; the gateway reverses a still-unsettled capture, or auto-credits an already-settled one. A credit memo for less is a **partial** refund and calls the amount-scoped `refund()`, which is always `CCCREDIT`, and which the gateway refuses with service code 536 on an order that has not settled. The module surfaces that as a loud, actionable error. Amounts are handled in the **base** currency throughout. Magento's `SubjectReader::readAmount()` supplies base amounts, and pairing those with the order's display currency undercharges a multi-currency store. The enrollment leg's exact base amount is persisted and reused by the 3DS completion leg. Timeout reconciliation is filtered by the expected action per command, so a timed-out refund cannot be confirmed by the original sale leg. ## How checkout works on this platform ### The direct-post flow Card fields render in the checkout page. A module-shipped script fetches an HMAC signature from a module controller, which holds the per-site key via `Tokenize::signRequest`, POSTs the PAN from the browser **directly to Inovio's token service**, and submits the order with only the single-use `TOKEN_GUID`. Nothing in the payment path on your server ever sees a card number. The token service's CORS support for this flow is verified against production. No Inovio-hosted infrastructure is required: there is no hosted payment page and no iframe you have to redirect to. Saved cards store the gateway's own references (`CUST_ID`, `PMT_ID`) plus the card brand and last four digits, which are display-only fields. No PAN is ever stored. This was verified, not assumed: a full database dump taken after a live test run contains zero occurrences of the test card number. ### 3-D Secure The SDK provides the server legs (`ThreeDSecureClient`); the module supplies a hidden device-data-collection iframe and a visible ACS challenge iframe. Both frictionless and challenge flows are supported. The challenge return comes back through a module controller that calls `completeSale()` or `completeAuthorize()`. For the two-token mechanics, see [Two-token 3D Secure](https://developer.inoviopay.com/carts/index.md#two-token-3d-secure). Collapsing the two tokens into one appears to work for every frictionless card, which never reaches the completion leg at all, and fails only for cards that are actually challenged. **Leaving Payment Review after a challenge.** The 3DS enrollment leg parks the order in `payment_review`. Magento's `RegisterCaptureNotificationCommand` only promotes an order to `STATE_PROCESSING` from `new`, `pending_payment` or empty, never from `payment_review`. So `Gateway/ThreeDSCompletionProcessor.php` explicitly calls `$payment->accept()` after registering the notification, mirroring the decline path's `$payment->deny(false)`. Without it the payment is captured but the order sits in `payment_review` forever. ## Admin operations | Operation | Where | What happens | |---|---|---| | Invoice (capture) | The order's **Invoice** action | Captures against the authorization. Full and partial both supported through Magento's own invoicing. | | Void | The order screen | Reverses an uncaptured authorization. | | Refund | Open the **Invoice**, then its **Credit Memo** button, then **Refund** | Calls `RefundCommand` and reaches the gateway. | > **Only the invoice-scoped "Refund" button reaches the gateway** > Magento exposes two differently-behaved credit-memo entry points, confirmed by > hand. The order-view toolbar's own **Credit Memo** button pops a confirm dialog > reading "This will create an offline refund", and that path never calls the > gateway. Opening the order's **Invoice** and clicking **its** Credit Memo > button reaches the real New Memo form, which offers **Refund Offline** and > **Refund**. Only **Refund** (online) reaches `RefundCommand`. ## Saved cards Saved cards use **Magento Vault**, the platform's native vault. Tokens carry the gateway's `CUST_ID` and `PMT_ID` references plus brand and last four digits. Vaulting is off by default; turn on **Enable Stored Cards (Vault)**. The gateway returns the **same `PMT_ID` for the same PAN**, and `vault_payment_token`'s unique key is `(payment_method_code, customer_id, gateway_token)`, which ignores `is_active` and `is_visible`. `VaultTokenBuilder` therefore looks up the existing row via `PaymentTokenManagementInterface::getByGatewayToken()` and reuses or reactivates it, rather than always inserting. Without that, a customer who saves a card, deletes it (a soft delete), then saves the same card again hits a duplicate-key exception and Magento aborts the **whole order** with a generic error. The reactivation needs an **explicit repository save**, which is worth knowing before anyone simplifies it. Magento core's `AfterPaymentSaveObserver` only *links* a token that already has an `entity_id`; it short-circuits before calling `PaymentTokenRepositoryInterface::save()`, because its own path only ever sets `entity_id` on a token it inserted in the same request. A row fetched from a prior request therefore never persists its reactivation. New tokens still go through the normal observer flow, and guest checkout (`customer_id = 0`) is unaffected. ## Known gaps - **No automated unit-test suite for the module** beyond the correctness-critical gateway logic. The SDK's conformance suite covers gateway behaviour; module tests cover the Magento side (request builders, order lifecycle, vault, 3DS controllers). - **The method does not hide itself on incomplete credentials.** Unlike the other three plugins, Magento shows the payment method and then fails if one of the five required fields is empty. - **Wallets, LatAm rails and raw-card admin MOTO orders** are out of scope in v1. ## Verified end to end An 8-spec Playwright suite lives in `e2e/`. Every step in every spec goes through the real storefront or admin UI. Nothing writes to the database, calls the module's PHP directly, or POSTs to the module's own controllers to advance a test; a handful of specs use a read-only query strictly to verify an outcome after the fact. ```bash npm test # all specs npx playwright test tests/03-3ds.spec.js # one spec ``` | Spec | What it proves | |---|---| | `01-sale.spec.js` | Card checkout approves and confirms. | | `02-vault.spec.js` | Save a card and reuse it on a later order; the saved card is listed in the customer account. | | `03-3ds.spec.js` | A real Cardinal ACS challenge is presented and completed, and the order reaches `processing`. | | `04-frictionless-3ds.spec.js` | With 3DS active and a frictionless PAN, no challenge appears and the order confirms. | | `05-backoffice.spec.js` | The order detail screen shows the Inovio gateway references. | | `06-invalid-card-input.spec.js` | Luhn-failing PAN, short PAN, past expiry and short CVV are all rejected client-side, with no order created. | | `07-refund.spec.js` | A full online credit memo against the invoice produces a real `CCREVERSECAP` leg against the original AuthCap, confirmed by a read-only query against the gateway's own `pmt.transaction` table. | | `08-partial-refund.spec.js` | Two-phase: pre-settlement, the partial online refund fails visibly in the admin with no money movement; then, with settlement simulated gateway-side, the same partial refund succeeds as a `CCCREDIT` for the smaller amount. | Specs 03, 07 and 08 are **regression guards** for real, live bugs this suite found and that have since been fixed: - **03** guards the `payment_review` bug: a completed, gateway-captured 3DS challenge used to leave the order permanently in `payment_review` instead of advancing to `processing`, so the shopper was charged and the merchant's order never became actionable. - **08** guards a money-losing full-versus-partial classification bug in `RefundCommand`. Core's `Creditmemo/RefundOperation::execute()` adds the memo to `base_total_refunded` on the same in-memory order **before** calling `refund()`, so subtracting it raw double-counted and every first partial refund classified as full. A $15 refund on a $20 order reversed all $20 while Magento recorded $15: a silent $5 loss with no error anywhere in the admin. The unit test hid it, because its fixture fed a pre-refund figure that production never produces. Settlement in spec 08 is simulated gateway-side. Settlement is an acquirer batch process with no UI anywhere, so this fakes the acquirer, not the shop; every Magento interaction in the spec stays real UI. ## Troubleshooting | Symptom | Cause | Fix | |---|---|---| | No payment method appears at checkout | The method is disabled, or restricted by **Payment from Specific Countries**. | Set **Enabled** to Yes and check the country restriction. This module does not hide itself on incomplete credentials, so also fill in all five required fields. | | Still no payment method after saving config | Magento's config cache. | `bin/magento cache:flush`. | | Error 121 at checkout, or the card form never tokenizes | Site Key missing or wrong, or written directly into `core_config_data`. | Enter the per-site HMAC Site Key through the admin UI. It is not the API password. | | `Invalid Data` returned on every transaction | The statement descriptor contains a space, underscore or forward slash. | Remove them, or clear the field. | | Refund fails with `SERVICE 536` | The order has not settled, so a partial credit is the wrong verb. | Retry the partial refund after settlement. | | A test transaction declines with Insufficient Funds | The Inovio test gateway decides from the order total, not the card. | Change the order total. See [Testing](https://developer.inoviopay.com/carts/index.md#testing). | | Class-not-found or a stale layout after install | DI compilation or static content not regenerated. | Re-run `setup:di:compile` and, in production mode, `setup:static-content:deploy`. | ## Source Repository: [Inoviopay/inovio-gateway-magento2](https://github.com/Inoviopay/inovio-gateway-magento2) (private, available from Inovio). Composer package `inovio/module-payment-gateway`, module `Inovio_PaymentGateway`, installed to `vendor/inovio/module-payment-gateway/`. --- # PrestaShop The Inovio Payment Gateway module for PrestaShop 9, with tokenized direct-post card entry, module-built vaulting and 3-D Secure. Source: https://developer.inoviopay.com/carts/prestashop.html Markdown: https://developer.inoviopay.com/carts/prestashop.md Repository: https://github.com/Inoviopay/inovio-gateway-prestashop Tokenized direct-post card checkout for PrestaShop 9, on the Inovio gateway. The card number never reaches the shop server. ## Overview and status > **Functional pre-release** > Sale, authorize, capture (full and partial), void, refund and vaulting are > implemented and exercised end to end. An 18-spec Playwright suite drives the > real storefront and the real back office. Two things are not yet proven in a > real browser: see [Known gaps](#known-gaps). This is the reference implementation of the four. The other three plugins were ported from it, and the design document behind it is the shared design rationale for all of them. PrestaShop gives the least framework of the four platforms. It has no native payment-token or vault API, no platform concept of authorize versus capture, no pending-authentication order state, and no order-submission JavaScript event. Each of those is a verified absence against official PrestaShop 9 documentation, not an assumption, and each is a thing this module builds itself. The module creates its own saved-card table and its own two order states (awaiting capture, awaiting 3-D Secure) at install time. ## Requirements | | | |---|---| | PrestaShop | 9.0+ (9.x only) | | PHP | 8.1+ | | PHP extensions | `bcmath` (required), `curl`, `json` | | From Inovio | API username, API password, Site ID, Site Key, Gateway Product ID | `bcmath` is required, not optional. The SDK's `Money` type does every decimal calculation through it so amounts never touch a binary float. Installation fails without it. PrestaShop 8.x is not supported. 8.0 through 8.2 supports PHP 7.2.5 through 8.1 and no higher, while 9.x requires 8.1+ and supports through 8.5. The two lines overlap at exactly PHP 8.1 and PrestaShop maintains separate documentation branches for them, so supporting both would mean runtime version shims or two builds. PrestaShop 1.7 is end of life as of the 9.0 release. ## Install Copy the `inoviopayment/` directory into your shop's `modules/` directory, or upload the ZIP through **Modules, Module Manager, Upload a module**. Then install it. Configure it under **Payment, Payment Methods, Inovio Payment Gateway, Configure**. Installation creates the module's own saved-card table and the two order states it needs. **CLI** ```bash php bin/console prestashop:module install inoviopayment ``` **Admin UI** ```text Modules -> Module Manager -> search "Inovio" -> Install ``` ## Configuration All settings are on the module's Configure screen. ### Gateway credentials | Setting | Required | Meaning | |---|---|---| | API Username | Yes | Gateway `REQ_USERNAME`. | | API Password | Yes | Gateway `REQ_PASSWORD`. Leave blank when saving to keep the stored password. | | Site ID | Yes | Gateway `SITE_ID`. | | Merchant Account ID | No | `MERCH_ACCT_ID`. Leave empty to let the gateway distribute by currency and country. | | Site Key | Yes | The per-site HMAC secret. Not the API password. | | Gateway Product ID | Yes | `LI_PROD_ID`. Not a PrestaShop SKU. | | Gateway Endpoint | No | The `pmt_service.cfm` transaction URL. The tokenization endpoint (`token_service.cfm`) and the 3-D Secure endpoint are derived from it by suffix rewrite, exactly as the SDK derives them, so the three cannot drift apart. Leave it at the default for production. | **Site Key** is a separate per-site HMAC secret issued by Inovio support. It is not the API password, and it is not something you can generate. It is used only to sign the browser's tokenization request. Without it the browser cannot tokenize and checkout fails with error 121. **Gateway Product ID** (`LI_PROD_ID`) is a product registered on the gateway, not a product or SKU from your catalogue. The whole order bills as one line item under it. Until all five required fields are filled in, the module hides itself at checkout rather than presenting a card form it cannot process. ### Payment behaviour settings | Setting | Required | Meaning | |---|---|---| | Payment Action | Yes | *Sale* charges at checkout. *Authorize only* reserves the funds and leaves the order awaiting a capture you trigger from the order page. | | Enable 3D Secure | No | Runs 3-D Secure authentication. Requires a 3DS-configured merchant account. | | Enable Saved Cards | No | Lets logged-in customers save a card for reuse. | | Statement Descriptor | No | `PMT_DESCRIPTOR`, what appears on the cardholder's statement. See the warning below. | | Descriptor Phone | No | `PMT_DESCRIPTOR_PHONE`, support number shown alongside the descriptor. | | Debug Logging | No | Logs gateway activity. Card numbers are never logged, because they never reach this server. | > **The statement descriptor must not contain a space, underscore or slash** > The gateway rejects the **entire transaction** with `Invalid Data` if > `PMT_DESCRIPTOR` contains a space, an underscore or a forward slash. Every > sale fails, not just the descriptor. > > | Descriptor | Result | > |---|---| > | `ACME STORE` | Rejected | > | `ACME_STORE` | Rejected | > | `ACME/STORE` | Rejected | > | `ACME-STORE` | Accepted | > | `ACMESTORE` | Accepted | > | `ACME.STORE` | Accepted | > > The full allowed set is `A-Z`, `a-z`, `0-9`, and `.` `-` `*` `+` `&` `@`. > This was mapped empirically by sending each candidate character in an > otherwise-identical approved request. A multi-word descriptor is the natural > thing for a merchant to type, and it kills every sale, so if you are unsure, > leave the field empty. ## Payment behaviour | Capability | Support | |---|---| | Sale | Authorize and capture in one step at checkout. | | Authorize only | Reserve funds, capture later from the order page. | | Capture | Full and partial, from the order page. | | Void | Reverse an uncaptured authorization. | | Refund | Full and partial, settlement-aware. | | Saved cards | The module's own storage plus a customer-account UI. PrestaShop has no vault API. | | 3-D Secure | Device-data collection and challenge, with a module-created pending order state. | | Timeout recovery | Reconciles via `status()` before failing an order. | Refunds follow the shared settlement-aware contract described in [Refunds are settlement-aware](https://developer.inoviopay.com/carts/index.md#refunds-are-settlement-aware). A full refund issues one `reverseCapture` call with `creditOnFail` and the gateway decides between reversing and crediting. A partial refund issues `refund` and fails loudly with service code 536 if the order has not settled; retry it after settlement. The merchant gets one Refund control and is never asked to guess at the settlement state, because they have no way to know it. Refunds are driven from PrestaShop's `actionOrderSlipAdd` hook. All three of PrestaShop 9's refund flows go through `OrderSlipCreator`, which fires that hook once per operation with the created slip; the amount is `total_products_tax_incl + total_shipping_tax_incl`. The `TransactionResult` is consumed, so a declined refund throws rather than producing a credit slip that tells the customer they were refunded. A gateway timeout means the transaction state is genuinely unknown. The module reconciles via `status()` before failing an order, including on the 3-D Secure completion leg. A non-challenge `PENDING` response is parked in a pending state with the references recorded, not treated as a decline, because the gateway may still settle it. ## How checkout works on this platform ### The direct-post flow Card fields live in the checkout page's DOM, inside the embedded PaymentOption form. The shipped JavaScript reads them, requests an HMAC signature from the module's `signature` front controller, exchanges the PAN for a single-use `TOKEN_GUID` by POSTing directly to Inovio, and only that token is submitted to PrestaShop. Nothing in the payment path on your server ever sees a card number. The card fields carry **no `name` attribute**, so a JavaScript failure cannot POST the PAN to the shop server by accident. Because PrestaShop has no order-submission JavaScript event, the module intercepts the payment form's own `submit` event and re-submits it once tokenization completes. This is undocumented but standard practice across PrestaShop payment modules. What reaches the PrestaShop server is the whole payload and nothing else: `token_guid`, `token_guid_completion` (3DS only), `pmt_expiry`, `cc_brand`, `cc_last4`, and the 3DS `ddc_reference_id` and browser data. **The CVV is never in that list.** It goes browser-direct to the token service alongside the PAN and is stored nowhere, by anyone. No Inovio-hosted infrastructure is required: there is no hosted payment page and no iframe you have to redirect to. Saved cards store the gateway's own references (`CUST_ID`, `PMT_ID`) plus the card brand, last four digits and expiry, which are display-only fields. There is no column that could hold a PAN or CVV. This was verified, not assumed: a full `mysqldump` of the shop database taken after a live test run contains zero occurrences of the test card number in any representation. `ps_order_payment.card_number` holds the last four digits only. > **PrestaShop core ships columns that will happily hold a PAN** > Core provides `ps_order_payment.card_number`, `card_brand`, `card_expiration` > and `card_holder`, which any module may populate, with no guardrail against a > full PAN being written there. This module writes last four only. Treat these > columns as display fields; they are the one place on this platform where a > careless implementation would put cardholder data into the shop database. ### The signature endpoint `controllers/front/signature.php` mints HMACs on demand. Left open it would let anyone tokenize cards against the merchant's site, so it is CSRF-protected with a per-visitor random nonce, cart-bound, and rate-limited with a server-side database-backed counter. The 3DS prepare endpoint is rate-limited too, because every hit there is a paid gateway call. ### 3-D Secure The module creates its own "awaiting 3-D Secure" order state at install, because PrestaShop has no pending-authentication state and no equivalent state machine. The order is parked there during the challenge and exited by an explicit `OrderHistory` transition. The ACS return arrives at the `threedsreturn` front controller, which is CSRF-exempt and authenticated by `TransactionId` instead. For the two-token mechanics, see [Two-token 3D Secure](https://developer.inoviopay.com/carts/index.md#two-token-3d-secure). ## Admin operations | Operation | Where | What happens | |---|---|---| | Capture | The order page, when Payment Action is *Authorize only* | Captures the reserved funds, full or partial. The order leaves the module's "awaiting capture" state. | | Void | The order page | Reverses an uncaptured authorization. | | Refund | PrestaShop's own partial-refund and standard-refund flows on the order page | Fires `actionOrderSlipAdd`; the module issues the transaction and throws on a non-approved result. | PrestaShop has no documented admin hook for a merchant-triggered capture, so the module owns its own back-office controls for capture and void, alongside the two order states it created at install. ## Saved cards PrestaShop has **no native payment-token or vault API**. Core ships `ps_order_payment.card_number`, `card_brand`, `card_expiration` and `card_holder` columns, but there is no storage, encryption, UI or API behind them, and no official example module demonstrates vaulting. This is what real modules do: PayPlug, SIBS and Axerve all ship one-click this way. So the module owns an `inovio_stored_card` table (an `ObjectModel`) plus a `storedcards` front controller for the customer-account UI, surfaced through the `displayCustomerAccount` hook. Rows hold the gateway's `CUST_ID` and `PMT_ID` references plus brand, last four and expiry. The save-card opt-in is persisted on the order reference and restored after the ACS return, so it is not dropped across a 3-D Secure challenge. Deleting a saved card removes the local row. ## Known gaps - **The checkout JavaScript has not been exercised in a real browser** as part of the module's own developer tests. Because PrestaShop has no order-submission event, the script intercepts the form's native submit, and that path needs a browser pass plus a theme-compatibility matrix. The Playwright suite does drive a real browser through checkout; the gap is systematic theme coverage. - **3DS challenge completion needs a browser and an HTTPS-reachable return URL.** Test 3-D Secure on an HTTPS-reachable host. - **Whether deleting a saved card should also revoke the gateway-side payment method is open** (design document §11). Behaviour is currently local-row-only, and the OpenCart extension deliberately matches it rather than inventing different behaviour. ## Verified end to end An 18-spec Playwright suite lives in `e2e/`, with a local stack at `stack/prestashop/docker-compose.yml`. | Spec | What it proves | |---|---| | `01-sale.spec.js` | Card checkout approves and confirms. | | `02-vault.spec.js` | Save a card and reuse it on a later order; the saved card is listed in the customer account. | | `03-3ds.spec.js` | A real ACS challenge is presented and completed by the shopper. | | `04-backoffice.spec.js` | The order detail screen shows the Inovio gateway references. | | `05-authorize-capture.spec.js` | Authorize at checkout, then capture from the back office. | | `06-void.spec.js` | Authorize at checkout, then void from the back office. | | `07-refund.spec.js` | Refunding a Sale order from the back office. | | `08-decline.spec.js` | A declined card does not create a confirmed order. | | `09-vault-delete.spec.js` | The shopper deletes a saved card. | | `10-frictionless-3ds.spec.js` | With 3DS active and a frictionless PAN, no challenge appears and the order confirms. | | `11-vault-idor.spec.js` | Shopper B cannot pay with shopper A's saved card. | | `12-invalid-card-input.spec.js` | Luhn-failing PAN, short PAN, past expiry and short CVV are all rejected client-side, with no order created. | | `13-partial-capture.spec.js` | Authorize, then partially capture from the back office. | | `14-partial-refund.spec.js` | Two-phase: pre-settlement the partial refund fails with "order not settled, partial refunds available after settlement" and no money moves; with settlement simulated gateway-side, the same refund is approved as a credit slip strictly smaller than the order total, with the partial flag and refunded quantity set. | Spec 14 is the regression guard for the money-losing bug this design eliminated: the old code would have silently reversed the entire order total in phase A. Settlement is simulated gateway-side because settlement is an acquirer batch process with no UI anywhere, so it fakes the acquirer, not the shop. There are also CLI developer scripts under `tests/`, which need a booted Symfony kernel and so run through a small runner: `e2e_module.php` (sale, order, vault, refund through module code), `e2e_module_verbs.php` (authorize, capture, void, partial capture, saved card) and `sdk_verbs.php` (raw SDK verbs, deliberately exercising the 536 path). They are CLI-only, guarded so they are not web-reachable. ## Troubleshooting | Symptom | Cause | Fix | |---|---|---| | No payment method appears at checkout | One of the five required credential fields is empty. The module hides itself rather than showing a card form it cannot process. | Fill in API Username, API Password, Site ID, Site Key and Gateway Product ID. | | Error 121 at checkout, or the card form never tokenizes | Site Key missing or wrong. | Enter the per-site HMAC Site Key from Inovio support. It is not the API password. | | `Invalid Data` returned on every transaction | The statement descriptor contains a space, underscore or forward slash. | Remove them, or clear the field. | | Refund fails with `SERVICE 536` | The order has not settled, so a partial credit is the wrong verb. | Retry the partial refund after settlement. | | A test transaction declines with Insufficient Funds | The Inovio test gateway decides from the order total, not the card. | Change the order total. See [Testing](https://developer.inoviopay.com/carts/index.md#testing). | | 3-D Secure challenge never completes | The ACS return URL must be reachable over HTTPS. | Test 3DS on an HTTPS-reachable host. | ## Source Repository: [Inoviopay/inovio-gateway-prestashop](https://github.com/Inoviopay/inovio-gateway-prestashop) (private, available from Inovio). Module technical name `inoviopayment`, installed to `modules/inoviopayment/`. ``` inoviopayment.php main class (PaymentModule) classes/ InovioGateway.php SDK requests + transaction verbs InovioStoredCard.php vault ObjectModel InovioVault.php vault write path controllers/front/ signature.php HMAC for browser tokenization validation.php order placement threeds.php 3DS prepare (AJAX) threedsreturn.php ACS return storedcards.php saved-cards management views/js/inovio-checkout.js direct-post + 3DS checkout JS vendor/inovio/gateway-sdk/ vendored SDK ``` The SDK is vendored rather than Composer-required because it is not on Packagist and PrestaShop Addons requires a self-contained single-module ZIP. The SDK has zero Composer dependencies, which makes this clean. --- # OpenCart The Inovio Payment Gateway extension for OpenCart 4, with tokenized direct-post card entry, configurable order statuses and an IDOR-guarded vault. Source: https://developer.inoviopay.com/carts/opencart.html Markdown: https://developer.inoviopay.com/carts/opencart.md Repository: https://github.com/Inoviopay/inovio-gateway-opencart Accept credit and debit cards through the Inovio/Argus Payments gateway on OpenCart 4. The card number never reaches the OpenCart server. ## Overview and status > **Functional pre-release** > Sale, authorize, capture (full and partial), void, refund, vaulting and 3-D > Secure are implemented and exercised end to end, including a real Cardinal ACS > challenge. A 7-file, 10-spec Playwright suite drives the real storefront and > the real admin. Not yet hardened for production. Target is **OpenCart 4.1.x**, developed and verified against 4.1.0.4 on PHP 8.2. This is the same direct-post architecture proven in the Magento 2 and PrestaShop 9 modules, ported to OpenCart 4, with a handful of deliberate divergences where OpenCart's own conventions are a better fit or where the PrestaShop reference had a behaviour worth not repeating. ## Requirements | | | |---|---| | OpenCart | 4.1.x (developed and verified against 4.1.0.4) | | PHP | 8.1+ | | PHP extensions | `bcmath` (required), `curl` | | From Inovio | API username, API password, Site ID, Site Key, Gateway Product ID | `bcmath` is required, not optional. The SDK computes every monetary amount through it so money never passes through a binary float. The settings screen refuses to enable the payment method without it. ## Install The extension ships as a standard OpenCart `.ocmod.zip`. ### Building the package ```bash cd inovio-gateway-opencart rm -rf build && mkdir build cp -R upload/. build/ cp install.json build/install.json cd build && zip -r ../inovio.ocmod.zip . && cd .. ``` > **The zip must be FLAT** > `admin/`, `catalog/`, `system/` and `install.json` go at the archive root. Do > not wrap them in an `upload/` directory. OpenCart 4.1.0.4's installer copies > zip entries verbatim into `extension//`; despite a comment in its own > source claiming it "only extracts the contents of the upload folder", there is > no code that strips an `upload/` prefix. A wrapped zip installs to > `extension/inovio/upload/...`, where nothing is autoloadable and the extension > silently never appears. Verified against the running 4.1.0.4 > `admin/controller/marketplace/installer.php`. ### Installing 1. Admin, **Extensions, Installer**: upload `inovio.ocmod.zip`, then press Install on the row that appears. 2. Admin, **Extensions, Extensions**: choose extension type **Payments**, find *Inovio Payment Gateway*, press the green **+** (Install). This step creates the extension's two database tables. 3. Press **Edit** on the same row, fill in the settings below, set **Status** to enabled, and Save. The customer-facing "Saved cards" list appears automatically under **My Account, Payment Methods** once vaulting is enabled. There is no extra step. ### Uninstalling Uninstalling removes the settings but **deliberately leaves the two tables in place**. Dropping `oc_inovio_order_ref` would destroy the gateway `PO_ID` of every historical order, permanently removing the merchant's ability to refund anything ever taken through the extension. Uninstall and reinstall is also how OpenCart upgrades an extension, so silent data destruction there is indefensible. Drop the tables by hand if you genuinely want the data gone. ## Configuration All settings live at **Extensions, Payments, Inovio Payment Gateway, Edit**. ### Gateway credentials | Setting | Required | Meaning | |---|---|---| | API Username | Yes | Gateway `REQ_USERNAME`. | | API Password | Yes | Gateway `REQ_PASSWORD`. Write-only in the UI: rendered blank, and saving it blank keeps the stored value. | | Site ID | Yes | Gateway `SITE_ID`. | | Merchant Account ID | No | `MERCH_ACCT_ID`. Leave empty to let the gateway distribute by currency and country. | | Site Key | Yes | A separate per-site HMAC secret issued by Inovio support, **not the API password**. Used only to sign browser tokenization requests. Without it the browser cannot tokenize, checkout fails with error 121, and the payment method stays hidden. Also write-only. | | Gateway Product ID | Yes | The Inovio product (`LI_PROD_ID`) orders are billed under. This is a gateway product id, **not a catalogue SKU from your store**. The whole order bills as one line item under it. | | Gateway Endpoint | No | The `pmt_service.cfm` transaction URL. The tokenization endpoint (`token_service.cfm`) and the 3-D Secure endpoint (`3dsrequest.cfm`) are derived from it by suffix rewrite, exactly as the SDK derives them, so one setting cannot drift from the others. The settings screen shows you the derived token URL. | Until all five required fields are filled in, the extension **hides itself at checkout** rather than presenting a card form it cannot process. ### Payment behaviour settings | Setting | Required | Meaning | |---|---|---| | Payment Action | Yes | *Sale* charges at checkout. *Authorize only* reserves funds and leaves the order awaiting a capture you trigger from the order screen. | | Enable 3-D Secure | No | Runs 3DS authentication. Requires a 3DS-configured merchant account. When on, checkout mints two tokens per card. | | Enable Saved Cards | No | Lets logged-in customers save a card for reuse. Stores the gateway's own card references only, never a card number. | | Statement Descriptor | No | `PMT_DESCRIPTOR`, what appears on the cardholder's statement. Optional. See the warning below. | | Descriptor Phone | No | `PMT_DESCRIPTOR_PHONE`, support number shown alongside the descriptor. Optional. | ### Order statuses Each transition is merchant-configurable. Defaults reference stock OpenCart statuses read from `oc_order_status`, not guessed. | Setting | Default | Notes | |---|---|---| | Approved Order Status | Processing (2) | A completed sale, or a fully-captured authorization. | | Awaiting Capture Status | Pending (1) | **Must be an unpaid status.** Funds are only reserved. | | Awaiting 3-D Secure Status | Pending (1) | **Must be an unpaid status.** An order parked here has not been charged and must not count as revenue. | | Failed Order Status | Failed (10) | Declines and gateway errors. | | Refunded Order Status | Refunded (11) | Reached by either refund verb. | | Voided Order Status | Voided (16) | A reversed authorization. | OpenCart ships Pending, Processing, Failed, Refunded and Voided already, so the extension exposes them as settings instead of adding rows to `oc_order_status`. This is a deliberate divergence from PrestaShop, which has no unpaid "awaiting capture" or "awaiting 3DS" states and so creates them at install. ### Display and availability **Geo Zone**, **Status**, **Sort Order** and **Debug Logging** behave as in any OpenCart payment extension. Debug logging writes gateway activity to `system/storage/logs/error.log`. Card numbers are never logged, because they never reach this server, so there is nothing to redact. > **The statement descriptor must not contain a space, underscore or slash** > The gateway rejects the **entire transaction** with `Invalid Data` if > `PMT_DESCRIPTOR` contains a space, an underscore or a forward slash. Every > sale fails, not just the descriptor. > > | Descriptor | Result | > |---|---| > | `ACME STORE` | Rejected | > | `ACME_STORE` | Rejected | > | `ACME/STORE` | Rejected | > | `ACME-STORE` | Accepted | > | `ACMESTORE` | Accepted | > | `ACME.STORE` | Accepted | > > The full allowed set is `A-Z`, `a-z`, `0-9`, and `.` `-` `*` `+` `&` `@`. > This was mapped empirically by sending each candidate character in an > otherwise-identical approved request; the SDK now rejects an invalid > descriptor rather than letting the gateway kill the sale. A multi-word > descriptor is the natural thing for a merchant to type, and it kills every > sale, so if you are unsure, leave the field empty. ## Payment behaviour | Capability | Status | |---|---| | Sale (authorize and capture in one step) | Supported. | | Authorize only, capture later from the admin | Supported. | | Capture, full and partial | Supported. | | Void (reverse an uncaptured authorization) | Supported. | | Refund, settlement-aware | Supported. | | 3-D Secure (enrollment, DDC, challenge) | Fully verified end to end, including a real Cardinal ACS challenge. | | Saved cards, with an IDOR guard | Supported. | | Gateway timeout reconciliation | Implemented. | Refunds follow the shared settlement-aware contract described in [Refunds are settlement-aware](https://developer.inoviopay.com/carts/index.md#refunds-are-settlement-aware). A full refund issues one `reverseCapture` call with `creditOnFail`, and the gateway decides between reversing and crediting. A partial refund issues `refund` and fails loudly with service code 536 if the order has not settled; retry it after settlement. ### No silent fallback There is no catch-and-ignore anywhere in the payment path. - A **3DS prepare failure** is logged loudly, returned to the browser as an error, and the checkout JavaScript refuses to place the order. This is a deliberate divergence from the PrestaShop module, which returned an empty object and let checkout proceed with no 3DS. On an SCA-mandated transaction that produces an unexplained soft decline with nothing in the shop recording why. A merchant who does not want 3DS turns it off in the settings; the extension never decides that at runtime. - A **gateway timeout** means the transaction state is genuinely unknown. The extension reconciles via `CCSTATUS` on the external order id before failing, and never lets the shopper simply retry into a possible double charge. - **Declines** surface the gateway's own cardholder-safe advice verbatim. Every other refusal shows a neutral message while the specific reason goes to the log for the operator. - **An order that already carries a status** is refused rather than charged again. - **If the settlement state cannot be determined at all**, the extension refuses and says so rather than guessing between two money-moving verbs. This is a deliberate divergence from PrestaShop's original `isSettled()`, which returned false on error and so quietly turned an unknown state into a reversal. ## How checkout works on this platform ### The direct-post flow Card fields live in the checkout page's DOM. The browser exchanges them for a single-use `TOKEN_GUID` by POSTing directly to Inovio's `token_service.cfm`, and only that token is submitted to OpenCart. Unlike the PrestaShop module, the extension does **not** intercept a form submit. It owns its own confirm button and drives the flow with `fetch()`, replying with a JSON `{redirect}` or `{error}`. That is the same contract OpenCart's own `cod` extension uses. OpenCart hands the whole payment area to the extension, so the form-interception complexity in the PrestaShop build was PrestaShop-specific, not payment-specific. The order row also already exists before payment runs. OpenCart's `checkout/confirm` writes the order at status 0 before the payment extension is called, whereas PrestaShop creates it only after approval. This is a better fit: the amount, currency and addresses are frozen on one authoritative record instead of being recomputed from a live cart. No Inovio-hosted infrastructure is required: there is no hosted payment page and no iframe you have to redirect to. The PAN-safety property is **structural**, not a rule operators must remember: - The card fields in `catalog/view/template/payment/inovio.twig` have **no `name` attribute** and are not inside a form that posts to OpenCart. They cannot be submitted to the shop server even by accident. - `inovio-checkout.js` reads them, exchanges them for a token against Inovio directly, and **blanks both fields** before anything else happens. - No server-side PHP file in this extension contains the strings `card_pan`, `card_cvv`, `cardNumber` or `PMT_NUMB`, excluding the SDK's own server-tokenization helper, which this extension does not call. - The vault table stores gateway references (`CUST_ID`, `PMT_ID`) plus brand, last four and expiry, which are display-only fields. **There is no column that could hold a PAN or CVV.** This was verified, not assumed: a full database dump taken after a live test run contains zero occurrences of the test card number. ### The signature endpoint is a minting oracle `extension/inovio/payment/inovio.signature` mints HMACs on demand. Left open, it would let anyone tokenize cards against the merchant's site, which is card testing. It enforces three checks: 1. **Session-bound.** `session.data['payment_method']['code']` must be `inovio.inovio`. OpenCart has no per-form CSRF token on the storefront (the `customer_token` in account URLs does not exist during checkout), so the session's own checkout state is the binding. A cross-site attacker cannot set it. 2. **Cart-bound.** The session must hold a non-empty cart. 3. **Rate-limited.** Twelve signatures per 60 seconds per session. Every refusal is logged with its reason. A silent 403 here would mean the browser cannot tokenize and the shopper sees an unexplained failure. ### 3-D Secure For the two-token mechanics, see [Two-token 3D Secure](https://developer.inoviopay.com/carts/index.md#two-token-3d-secure). Token B is persisted to `oc_inovio_order_ref` at the moment the order is parked, because the ACS return arrives on a separate request that has no access to the original POST. **Securing the ACS return.** The ACS POSTs the challenge outcome cross-site, and it may arrive with no session cookie, so the return leg cannot rely on session state. It is bound to a legitimate order by the order id on the return URL and by a **constant-time** comparison of the ACS `TransactionId` against the `procTransId` stored at enrollment. The second is what makes the first safe: an order id is a guessable integer, but the `procTransId` is not, and only the real ACS knows it. A replay guard additionally refuses any order that has already left the awaiting-3DS status. ## Admin operations Capture, void and refund are driven from the extension's own panel on the order screen, rendered by `admin/view/template/payment/inovio_order.twig`. | Operation | Where | What happens | |---|---|---| | Capture | The order screen panel, when Payment Action is *Authorize only* | Captures the reserved funds, full or partial. The order moves to the configured Approved status. | | Void | The order screen panel | Reverses an uncaptured authorization. The order moves to the configured Voided status. | | Refund | The order screen panel | One Refund control. The gateway routes reverse versus credit for a full refund; a partial refund before settlement fails loudly with 536. The order moves to the configured Refunded status. | Order history is written directly by the panel rather than through `addHistory()`. The admin `sale/order` model has no `addHistory()` at all; only the catalog model does, and OpenCart's own admin reaches it by spinning up a second store instance and proxying to the `api/order` route, which requires a configured API user and fails without one. Worse, the catalog method re-runs anti-fraud extensions, subtracts stock and redeems coupons on a transition into a processing status, all of which are wrong to re-run when capturing or refunding an order that already completed checkout. The panel writes the history row and status itself: exactly the two intended effects, nothing else. ## Saved cards OpenCart 4.1.0.4 has **no native payment-token API**. It does ship `catalog/controller/account/payment_method.php`, but that only asks each enabled payment extension for a rendered HTML fragment (`extension//account/`). There is no storage, schema or CRUD behind it, and a stock install has no `oc_customer_payment*` table of any kind. So the extension owns `oc_inovio_stored_card` and renders into that native hook, which is strictly better wiring than PrestaShop's `displayCustomerAccount`. **The ownership (IDOR) guard is mandatory and explicit.** Stored-card ids are small integers that appear in form posts, so guessing another shopper's id is trivial. Every lookup or charge goes through `Vault::findForCustomer()`, which requires `customer_id` to match the logged-in session as a checked condition, and re-asserts it on the returned row so a future SQL edit cannot quietly drop it. "Row does not exist" and "row is not yours" are answered identically, so the endpoint cannot be used as an oracle for which ids are live. Deleting a saved card removes the local row only. Whether it should also revoke the gateway-side `PMT_ID` is an open question against Inovio's customer API; behaviour is kept identical to the PrestaShop module rather than invented here. ## Known gaps - **Only `en-gb` translations ship.** Other languages fall back to OpenCart's default behaviour for a missing language file. - **Multi-store installs were not tested.** The vault is keyed on `customer_id` only, with no `store_id` column. PrestaShop's equivalent carries `id_shop`. - **Apache access and error logs could not be scanned for the PAN.** `docker exec` hung reliably on those files in the test environment. `docker logs`, the same streams, was scanned and returned zero. A PAN could never appear in a request URL in any case, since the only request that carries it is a POST body sent to Inovio, not to OpenCart. - Whether deleting a saved card should revoke the gateway-side `PMT_ID` is open. ## Verified end to end A Playwright suite lives in `e2e/`, covering seven spec files and ten tests. | Spec | What it proves | |---|---| | `01-sale.spec.js` | Card checkout approves and confirms. | | `02-vault.spec.js` | Save a card, then pay a second order with it. | | `03-3ds.spec.js` | A Cardinal Purchase Authentication PAN triggers a real ACS challenge which is completed; and a standard PAN completes checkout frictionlessly with no challenge overlay. | | `04-decline.spec.js` | A declined card does not create a paid order. | | `05-refund.spec.js` | Refunding a Sale order from the admin. | | `06-invalid-card-input.spec.js` | Luhn-failing PAN, short PAN and short CVV are all rejected client-side, with no paid order created. | | `07-vault-idor.spec.js` | Shopper B, in a separate browser context, cannot pay with shopper A's saved card. This is the guard for the vault ownership check. | ## Divergences from the PrestaShop reference | Divergence | Rationale | |---|---| | No form-submit interception; the extension owns its confirm button and uses `fetch()`. | PrestaShop has no order-submission JS event, forcing that module to intercept the payment form's native submit. OpenCart hands the payment area to the extension and expects a JSON reply, the same contract its own `cod` extension uses. | | The order row already exists before payment. | OpenCart's `checkout/confirm` writes the order at status 0 before the payment extension runs. The amount, currency and addresses are frozen on one authoritative record. | | A 3DS prepare failure is fatal, not skipped. | Silently skipping 3DS was a real bug in the PrestaShop build's history. It is not repeated here. | | A settlement-check failure refuses instead of assuming "unsettled". | Choosing the wrong verb is a money-moving mistake, so the merchant is told instead. | | Order history is written directly rather than via `addHistory()`. | See [Admin operations](#admin-operations). | | Custom order statuses are configured, not created. | OpenCart already ships the statuses this extension needs. | | The vault UI uses a native hook. | OpenCart's `account/payment_method` page already asks each payment extension for a fragment. | | Uninstall keeps the tables. | See [Uninstalling](#install). | ## Troubleshooting | Symptom | Cause | Fix | |---|---|---| | No payment method appears at checkout | One of the five required credential fields is empty, so `getMethods()` returns an empty array and the method withholds itself. | Fill in API Username, API Password, Site ID, Site Key and Gateway Product ID, and set **Status** to enabled. | | Still no payment method, credentials complete | A **Geo Zone** is set and the shopper's address falls outside it. | Clear the Geo Zone setting, or check the address. | | The extension never appears under Extensions, Payments after install | The `.ocmod.zip` was wrapped in an `upload/` directory. | Rebuild it flat. See [Building the package](#install). | | Install dies with `Class "Opencart\System\Library\Extension\Inovio\Gateway" not found` | Files are at `system/library/inovio/gateway.php` instead of `system/library/gateway.php`. | See [File placement is load-bearing](#source). | | Error 121 at checkout, or the card form never tokenizes | Site Key missing or wrong. | Enter the per-site HMAC Site Key from Inovio support. It is not the API password. | | `Invalid Data` returned on every transaction | The statement descriptor contains a space, underscore or forward slash. | Remove them, or clear the field. | | Refund fails with `SERVICE 536` | The order has not settled, so a partial credit is the wrong verb. | Retry the partial refund after settlement. | | Refund refuses with a "settlement state unknown" error | The `CCSTATUS` call itself failed, so the extension will not guess between two money-moving verbs. | Retry once the gateway is reachable. This is deliberate. | | A test transaction declines with Insufficient Funds | The Inovio test gateway decides from the order total, not the card. | Change the order total. See [Testing](https://developer.inoviopay.com/carts/index.md#testing). | | Checkout fails and the log shows a refused signature request | The signature endpoint is session-bound, cart-bound and rate-limited to 12 per 60 seconds. | Every refusal is logged with its reason. Read `system/storage/logs/error.log`. | ## Source Repository: [Inoviopay/inovio-gateway-opencart](https://github.com/Inoviopay/inovio-gateway-opencart) (private, available from Inovio). Extension code `inovio`, packaged as `inovio.ocmod.zip`, installed to `extension/inovio/`. ``` upload/ admin/ controller/payment/inovio.php settings, install/uninstall, capture/void/refund language/en-gb/payment/inovio.php view/template/payment/inovio.twig settings screen view/template/payment/inovio_order.twig order-screen panel catalog/ controller/payment/inovio.php index/signature/threeds/confirm/threedsReturn controller/account/inovio.php "Saved cards" UI + delete model/payment/inovio.php offers the method to checkout view/template/payment/inovio.twig card form view/javascript/inovio-checkout.js direct-post + 3DS client system/ library/gateway.php gateway service (verbs, refs, request building) library/vault.php saved-card storage + IDOR guard vendor/inovio/ vendored PHP SDK + classmap autoloader install.json ``` ### File placement is load-bearing Both startup controllers register `Opencart\System\Library\Extension\` to `extension//system/library/`, and OpenCart's autoloader then appends only the class tail after that namespace. So `Opencart\System\Library\Extension\Inovio\Gateway` resolves to `extension/inovio/system/library/gateway.php`. There is **no** second `inovio/` directory beneath `system/library/`. This build originally shipped the files at `system/library/inovio/gateway.php`, which looks natural and is wrong. The failure mode is nasty: everything appears fine until the moment you press Install, which dies with `Class "Opencart\System\Library\Extension\Inovio\Gateway" not found`, after the `oc_extension` row has already been written but before the schema is created. ### The vendored SDK `system/vendor/inovio/gateway-sdk/` is a verbatim copy of the Inovio PHP SDK, loaded by a small classmap autoloader at `system/vendor/inovio/autoload.php`. It is vendored rather than Composer-required because the package is not on Packagist, and an OpenCart extension installs as a self-contained zip through the admin UI with no Composer step in that path. The SDK has zero Composer dependencies, which makes this clean. The autoloader indexes **by file content**, not by filename, for two reasons. OpenCart's own autoloader lower-cases and snake-cases class tails, so `ResultMapper` would be looked for at `result_mapper.php`; and the SDK's files are not one-to-one with class names anyway, since `Result.php` declares about a dozen classes. Neither OpenCart's convention nor PSR-4 can resolve it. --- # Contact us Get in touch with the Inovio team. Source: https://developer.inoviopay.com/contact.html Markdown: https://developer.inoviopay.com/contact.md Have a question about integrating with Inovio, or want to talk to an expert first? Send us a message and we'll get back to you.
--- # Privacy Policy Inovio Payments is committed to ensuring that your privacy is protected. Read more about our Privacy Policy here. Source: https://developer.inoviopay.com/privacy.html Markdown: https://developer.inoviopay.com/privacy.md As a provider of payment gateway and related services, we understand that the privacy of our clients and/or website visitors (“you” or “your”) is very important. We developed this Privacy Policy (the “Policy”) to ensure that your privacy is protected while using our website (the “Inovio Website”). ## Table of Contents - [Applicability of this policy](#application) - [A note about minors](#aboutnote) - [About information Inovio collects](#personalinfo) - [Our use of your personal information](#ouruseof) - [Our disclosure of your information](#ourdisclosur) - [Do not disclose your password](#donotdisclosur) - [Reviewing, changing or removing your personal information](#reviewing) - [Security](#security) - [Location](#location) - [Update/delete/opt-out](#updateand_del) - [Revisions to this privacy policy](#revisions) ## 1. Applicability of this policy This Policy applies to the website operated by Inovio Payments, LLC. (“Inovio”, “us”, “we” or “our”). However, this Policy does not apply to confidential Information disclosed under the terms that you entered into with Inovio Payment, LLC. Master Payment Processing Services Agreement or any similar written agreements that contain a confidentiality provision (collectively, the “Inovio Written Agreements”). In case there is a conflict between this Policy and the Inovio Written Agreements, the Inovio Written Agreements shall govern. ## 2. A note about minors Persons under eighteen years of age are prohibited from using the Inovio website or any Inovio services. We ask that minors not submit information to us. ## 3. Personal information Inovio collects. During the registration process for, or while using the Inovio Website, you may provide us with certain “Personal Information”, which can include your name, email address, other data that can be used to personally identify you. We may also collect Personal Information as part of the merchant application process, from you or our third party vendors, including, without limitation, consumer reporting agencies or financial institutions. The Inovio Website also uses “cookies” to store and sometimes track information to make your online experience easier and more personalized. Cookies are small pieces of data that are stored by a user’s web browser on the user’s computer. Cookies may record information a user accesses on one webpage to simplify subsequent interactions with that website by the same user, or to use the information to streamline the user’s transactions on related webpages. Cookies make it easier for a user to move from webpage to webpage and to complete transactions over the Internet. Most major web browsers are set up so that they will initially accept cookies, but you may modify your computer’s preferences to issue you an alert when a cookie is downloaded, or to disable the ability of third parties to download a cookie to you. If you choose to reject all cookies, the Inovio Website may not function properly. You may obtain further information about cookies and how they function at: http://en.wikipedia.org/wiki/HTTP_cookie. We may also use standard Internet technology, such as web beacons or 1x1 “gifs” and other similar technologies (collectively “Pixel Tags”), to track your use of the Inovio Website. A Pixel Tag is an electronic image, often a single pixel (1x1), that is ordinarily not visible to you and may be associated with cookies on the your storage drives. We also may include Pixel Tags in advertisements or promotional or other e-mail messages, or to determine whether messages have been opened and acted upon. The information enables us to customize the services we offer. ## 4. Our use of your personal information. We use your Personal Information for many purposes, including to analyze trends, administer the website, improve site performance and/or for security purposes. We may also use your Personal Information to provide you with information about products and services provided by Inovio or third parties. We may also use Personal Information to resolve disputes, troubleshoot problems and enforce any agreements, policies and rules governing the use of the Inovio Website. ## 5.Our disclosure of your information. We also disclose Personal Information to third party vendors involved in providing merchant account services, e.g., consumer reporting agencies, relevant financial institutions, card association processors, card associations, as reasonably necessary or appropriate to provide services to Inovio. In these cases, relevant third parties have agreements with Inovio that include confidentiality obligations. Because Inovio may use service providers located in different countries, Personal Information may be transferred to a country in which you do not reside. In response to compulsory (required) governmental and/or third party inquiries, we may disclose Personal Information to comply with a court order, subpoena, and search warrant. We expressly reserve the right to disclose your Personal Information when we have a good faith belief that disclosure is necessary to protect our rights, or to enforce our agreements, policies, and rules governing your use of the Inovio Website, or to cooperate with law enforcement. We DO NOT sell or rent any of your Personal Information to third parties for marketing purposes without your permission or consent. We may, however, disclose Personal Information in the aggregate to third parties for marketing and promotional, security or anti-fraud, and/or business analytical purposes in any form that could not be used to identify you personally. By using the Inovio Website, you consent that we may provide your Personal Information to third parties as described in this Policy. ## 6. Do not disclose your password. Please DO NOT disclose or share your password with anyone else. If you lose control of your password, you may lose substantial control over your Personal Information and may be subject to legally binding actions taken on your behalf. If your password has been compromised for any reason, you should immediately change your password and/or contact us at privacy@inoviopay.com. ## 7. Reviewing, changing or removing your personal information. Once you register, you will be able to review and change your Personal Information. Please promptly update your information by logging in to your account and following the screen prompts. We strongly urge you to change your password periodically to help reduce the risk of unauthorized access to your account information. Upon your request, we will remove your data from our main databases. To make this request, email us at privacy@Inoviopay.com. Despite the foregoing and even though you have requested us to remove information, we will retain in our files certain data used to resolve disputes, troubleshoot problems, comply with credit card merchant banking and association agreements, enhance security, comply with audit obligations, reduce fraud, comply with the law, and/or enforce any agreements policies, and rules governing your use of the Inovio Website. Removed information also may persist in backup copies. ## 8. Security. Inovio uses industry standard efforts, such as firewalls, to safeguard your Personal Information. While “perfect security” does not exist on the Internet, or elsewhere, our technical staff works hard to help ensure your secure use of our services. ## 9. Location. Inovio is located in the United States. If you are visiting the Inovio Website from outside the United States, you are on notice that your information may be transferred to, stored, and processed in the United States or other countries in which the Inovio or its service providers are located. The data protection and other laws of the United States and these other countries might not be as comprehensive as those in your country. By using the Inovio Website, you consent to us transferring your Personal Information from your country to the United States or countries in which the Inovio or its service providers are located for the purposes described in this Policy. ## 10. Update/delete/opt-out. You may request access to Personal Information we hold about you and we will provide this to you except in limited circumstances in which we are permitted not to. You may contact us to modify or delete your Personal Information from our database by: sending email privacy@Inoviopay.com; logging in with your password and modify/delete your account; mailing us at Inovio Payments Inc. 250 Stephenson Highway Troy, MI 48083 Attn: Copyright Agent ## 11. Revisions to this privacy policy. Inovio reserves its right, in its sole and absolute discretion, to revise, amend, modify or revoke this Policy at any time and in any manner to the fullest extent permitted by law. Changes to this Policy will be effective by posting revisions on the Inovio Website. Inovio Payments Inc. 250 Stephenson Highway Troy, MI 48083 Attn: Copyright Agent --- # Terms of Use Learn more about Inovio Payments Terms and Conditions here. Source: https://developer.inoviopay.com/terms-conditions.html Markdown: https://developer.inoviopay.com/terms-conditions.md Last Updated February 1, 2012, Version 1.0 **IMPORTANT LEGAL NOTICE.** **PLEASE READ THE FOLLOWING TERMS OF USE ("TERMS") CAREFULLY. THESE TERMS GOVERN YOUR USE OF THIS WEBSITE AND THE SERVICES OFFERED THROUGH THE WEBSITE. THESE TERMS SET FORTH A BINDING AGREEMENT BETWEEN YOU AND INOVIO PAYMENTS INC.** **YOU MUST BE AT LEAST 18 YEARS OLD AND THE AGE OF MAJORITY AND LEGAL CONSENT IN THE JURISDICTION IN WHICH YOU LIVE OR RESIDE TO AGREE TO THESE TERMS.** ## 1. Acceptance of Terms of Use These Terms of Use ("Terms") constitute a binding agreement between you and Inovio Payments Inc. (“Inovio”,“we”,“us” or “our”) and govern your use of this website (the “Website”) and the content, products and services offered through it (collectively with the “Services”). By accessing, viewing or using any Services, you represent and warrant that you are at least 18 years old and the age of majority and legal consent in the jurisdiction in which you live or reside, and you agree to be bound by and subject to these Terms. If you do not agree to these Terms, you should not check or click on, or otherwise agree to, these Terms, and you should immediately leave this page and not access or use the Website or any other Services. Upon our request, you agree to sign a non-electronic version of these Terms. ## 2. Changes to Terms of Use and Services THESE TERMS MAY BE AMENDED OR CHANGED BY US IN OUR DISCRETION, WITH OR WITHOUT NOTICE, AT ANY TIME. We indicate at the top of the page when these Terms were last updated. Your continued access or use of the Website or any other Services following such changes will be deemed acceptance of such changes. In addition, we reserve the right to modify or cease providing all or any portion of the Services at any time, with or without notice. Be sure to return to this page periodically to ensure familiarity with the most current version of these Terms. ## 3. Privacy Policy We are committed to protecting the privacy of the personal information you provide to us through the Website. Any personal information submitted through the Website by you is subject to our Privacy Policy, which is incorporated herein by reference. PLEASE REVIEW OUR PRIVACY POLICY TO UNDERSTAND OUR PRACTICES WITH RESPECT TO YOUR PERSONAL INFORMATION. We do not knowingly collect personal information from persons under the age of 18. The date of the last update to our Privacy Policy will be noted at the top of our Privacy Policy. ## 4. Account In order to participate in or receive certain Services, you will be required to create an account with us (“Account”), which will be governed by the Master Payment Processing Services Agreement. In the event that there is a conflict between the terms of these Terms and the Master Payment Processing Services Agreement, the Master Payment Processing Services Agreement shall govern. ## 5. Your Additional Representations and Warranties You further represent and warrant to us, under penalty of perjury, as follows: (a) You will not provide or permit access or use of the Services, or your Account, by any minors; (b) Your Account information is current, complete and accurate and you will promptly update all information to keep your Account and billing information complete and accurate upon any change (such as change of billing address, credit card number or expiration date); (c) You have not and will not access or use the Services from any place or jurisdiction where such use is prohibited or contrary to applicable laws, rules, regulations, ordinances, edicts or customs, and you are not a national or resident of any country which the United States has (i) embargoed goods; (ii) identified as a “Specially Designated National”; or (iii) placed on the Commerce Department’s Table of Deny Orders; (d) Your use of the Services is and will be in compliance with all applicable laws, rules, regulations, ordinances, edicts or customs; (e) If you establish an Account, you (i) have never been convicted of a felony; and (ii) are not required to register as a sex offender with any government entity or agency. ## 6. Third Party Links and Pages; Reliance on Content and Advice (a) The Services may include hyperlinks or banner ads to third-party websites, content and/or resources (“Resources”). You acknowledge and agree that we have no control over and are not responsible for the availability of any such Resources, and we do not endorse any advertising, products or other materials on or available from such Resources. Because we cannot control the activities of such Resources, we cannot accept responsibility for any use of your personal information by such third parties, and we cannot guarantee that they will adhere to the same privacy and security practices as us. If you visit or link to a Resource, you should consult that Resource’s privacy policy before providing any personal information. You agree that we shall have no liability for any losses, damages, liabilities or expenses you may incur due to your use of such Resources, and you agree to indemnify us and hold us harmless for any such use. (b) Opinions, advice, statements, offers, or other information or content made available through the Services are those of their respective authors, and should not necessarily be relied upon. Such authors are solely responsible for such content. We do not: (i) guarantee the accuracy, completeness, or usefulness of any information through the Services, or (ii) adopt, endorse or accept responsibility for the accuracy or reliability of any opinion, advice, or statement made by any party that appears through the Services. Under no circumstances will we or our affiliated entities be responsible for any loss or damage resulting from your reliance on information or other content posted through the Services or transmitted to or by any of our users or members. ## 7. Proprietary Rights The content provided through the Services, including but not limited to, the text, data, software, manuscripts, graphics, photographs, music, sounds, videos, interactive features, blogs, posts, feedback, messages, tags and other materials (collectively, “Content”) and the trademarks, service marks and logos contained therein (“Marks”) are owned by or licensed to us, subject to copyright and other intellectual property rights under United States and foreign laws and international conventions. All Content is provided to you solely for your information and personal, non-commercial use. You agree to not engage in the use, copying, or distribution of any Content other than as expressly permitted herein. If you download or print a copy of the Content for personal use, you must retain all copyright and other proprietary notices contained therein. You agree not to circumvent, disable or otherwise interfere with security related features of the Services or features that prevent or restrict use or copying of any Content or enforce limitations on the use of the Services or Content. We or our licensors retain all intellectual and proprietary rights in and to the Services and Content, except as expressly provided herein. No right is granted to you herein to use any Marks. ## 8. Content Provided "AS IS"; Access to Content You understand that Content, whether publicly posted or privately transmitted, is the sole responsibility of the person from whom such Content originated. We do not control this Content and do not guarantee its accuracy, integrity or quality. All such Content is provided “AS IS” without representation or warranty of any kind. Under no circumstances shall we be liable to you in any way for any Content, including but not limited to, any errors or omissions in any Content, or any loss or damage of any kind incurred as a result of the use of any Content. We claim immunity from liability to the fullest extent permitted by law, and as further provided under the Communications Decency Act, for any Content provided by third parties. Neither our actions nor any provision in these Terms is intended to waive, remove or usurp such immunity. Your Conduct. ## 9. You further agree not to use the Services to: (a) upload, post, email, transmit or otherwise make available any Content that is unlawful, harmful, threatening, abusive, harassing, tortious, defamatory, obscene, libelous, invasive of another’s privacy, hateful, or racially, ethnically or otherwise objectionable; (b) harm minors in any way or commit abuse; (c) impersonate or misrepresent your affiliation with, including acting as an employee of, us or our affiliated entities; (d) forge headers or otherwise manipulate identifiers in order to disguise the origin of any Content transmitted through the Services; (e) upload, post, email, transmit or otherwise make available any Content that you do not have a right to make available under any law or under contractual or fiduciary relationships (such as inside information, proprietary and confidential information learned or disclosed as part of employment relationships or under nondisclosure agreements); (f) upload, post, email, transmit or otherwise make available any Content that infringes any patent, trademark, trade secret, copyright or other proprietary rights of any person; (g) upload, post, email, transmit or otherwise make available any unsolicited or unauthorized advertising, promotional materials, “affiliate marketing codes,” “link referral code,” or any other form of commercial solicitation; (h) upload, post, email, transmit or otherwise make available any material that contains software viruses or any other computer code, files or programs designed to interrupt, destroy or limit the functionality of any computer software, hardware, networks or telecommunications equipment; (i) disrupt the normal flow of dialogue, cause a screen to “scroll” faster than other users or members of the Services are able to type, or otherwise act in a manner that negatively affects other users’ or members’ ability to engage in real-time exchanges; (j) interfere with or disrupt the Services or servers or networks connected to the Services, or disobey any requirements, procedures, policies or regulations of networks connected to the Services, including using any device, software or routine to bypass our robot exclusion headers; (k) violate any applicable local, state, national or international law, including, but not limited to, regulations promulgated by the U.S. Securities and Exchange Commission, any rules of any national or other securities exchange, including, but not limited to, the New York Stock Exchange, the American Stock Exchange or the NASDAQ, and any regulations having the force of law; and (l) provide material support or resources (or conceal or disguise the nature, location, source, or ownership of material support or resources) to any organization(s) designated by the United States government as a foreign terrorist organization pursuant to section 219 of the Immigration and Nationality Act. ## 10. DMCA Notice We strive to comply with the Digital Millennium Copyright Act of 1998, as amended (“DMCA”), at all times and maintain a repeat offender policy which may result in the termination of your right to use the Services if you violate such policy. If you believe that your work has been copied, posted or otherwise made available through the Services in a way that constitutes copyright infringement, please notify our DMCA Copyright Agent of your complaint, as set forth in the DMCA. Please consult the DMCA to confirm these requirements. You must provide our DMCA Copyright Agent with the following information in writing, to the extent required by the DMCA: (a) an electronic or physical signature of the person authorized to act on behalf of the copyright owner that is allegedly infringed; (b) a description of the copyrighted work that you claim has been infringed (or, if multiple copyrighted works on a site are covered by a single complaint, a representative list of the allegedly infringing works on the site); (c) identification of the material that is claimed to be infringing and to be removed, and information reasonably sufficient to permit us to locate the material; (d) information reasonably sufficient to permit us to contact you, such as your address, telephone number and e-mail address; (e) a written statement by you that you have a good faith belief that use of the material in the manner complained of is not authorized by the copyright owner, its agent or the law; and (f) a statement by you, made under penalty of perjury, that the above information in your notice and complaint is accurate and that you are the copyright owner or authorized to act on the copyright owner’s behalf. Please be aware that the foregoing information in your complaint may be forwarded to the person who provided the allegedly infringing content. The foregoing information must be submitted to our: DMCA Copyright Agent as follows: Inovio Payments Inc. 250 Stephenson Highway Troy, MI 48083 Attn: Copyright Agent Email: [copyrightagent@inoviopayments.com](https://developer.inoviopay.com/mailto:copyrightagent@inoviopayments.com) If you believe that your material has been mistakenly removed or disabled pursuant to this Section 10, you may submit a counter notice by notifying our DMCA Copyright Agent at the address provided above. Pursuant to Section 512(f) of the DMCA, any person who knowingly materially misrepresents that material or activity was removed or disabled by mistake or misidentification may be subject to liability. If you believe that your material has been mistakenly removed or disabled pursuant to this Section 10, you may submit a counter notice by notifying our DMCA Copyright Agent at the address provided above. Pursuant to Section 512(f) of the DMCA, any person who knowingly materially misrepresents that material or activity was removed or disabled by mistake or misidentification may be subject to liability. ## 11. Disclaimer of Warranties THE SERVICES ARE PROVIDED “AS-IS” AND WE EXPRESSLY DISCLAIM ANY IMPLIED WARRANTIES TO THE FULLEST EXTENT PROVIDED BY LAW, INCLUDING BUT NOT LIMITED TO, ANY WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE OR NON-INFRINGEMENT. TO THE EXTENT APPLICABLE LAWS PROHIBIT TERMS OF USE FROM DISCLAIMING ANY IMPLIED WARRANTY, SUCH IMPLIED WARRANTY SHALL BE LIMITED TO THE MINIMUM WARRANTY PERIOD REQUIRED BY LAW, AND IF NO SUCH PERIOD IS REQUIRED, THEN THIRTY (30) DAYS FROM FIRST USE OF THE SERVICES. WE CANNOT GUARANTEE AND DO NOT PROMISE ANY SPECIFIC RESULTS FROM USE OF THE SERVICES. WITHOUT LIMITING THE FOREGOING, WE DO NOT WARRANT THAT THE SERVICES WILL BE UNINTERRUPTED OR ERROR-FREE. ## 12. Limitation of Liability IN NO EVENT WILL WE BE LIABLE TO YOU OR ANY OTHER PERSON FOR ANY INDIRECT, CONSEQUENTIAL, EXEMPLARY, INCIDENTAL, SPECIAL OR PUNITIVE DAMAGES, INCLUDING BUT NOT LIMITED TO, LOST PROFITS ARISING OUT OF YOUR USE, OR INABILITY TO USE, THE SERVICES, EVEN IF WE HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. YOU FURTHER AGREE TO INDEMNIFY US AND HOLD US HARMLESS FOR ANY AND ALL CLAIMS, DAMAGES, LIABILITIES AND EXPENSES IN THE EVENT THAT YOU FIND OTHER USERS’ OR MEMBERS’ CONTENT TO BE OFFENSIVE, HARMFUL, OBSCENE, INACCURATE AND/OR DECEPTIVE. UNDER NO CIRCUMSTANCES SHALL OUR LIABILITY TO YOU FOR ANY CLAIM OR CAUSE OF ACTION WHATSOEVER, AND REGARDLESS OF THE FORM OF THE ACTION, WHETHER ARISING IN CONTRACT, TORT OR OTHERWISE, EXCEED THE AMOUNT PAID BY YOU TO US, IF ANY, DURING THE 90 DAY PERIOD IMMEDIATELY PRECEDING THE DATE ON WHICH YOU FIRST ASSERT ANY SUCH CLAIM. THE FOREGOING LIMITATIONS SHALL APPLY TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW. ## 13. Indemnification You agree to indemnify and hold us, our parent, subsidiaries, and affiliated entities, and ours and their shareholders, directors, officers, employees, agents, contractors, licensors and licensees, harmless from any loss, liability, claim, demand or expense, including but not limited to, reasonable attorney’s fees, made by any third party due to or arising out of your use of the Services or any breach or violation of these Terms. ## 14. U.S. Export Controls Software and Content provided through the Services is subject to United States export controls. No software or Content from the Services may be downloaded or otherwise exported or re-exported (a) into (or to a national or resident of) Cuba, Iraq, Libya, North Korea, Iran, Syria, or any other country to which the U.S. has embargoed goods; or (b) to anyone on the U.S. Treasury Department’s list of Specially Designated Nationals or the U.S. Commerce Department’s Table of Deny Orders. By downloading or using any such software or Content, you represent and warrant that you are not located in, under the control of, or a national or resident of any such country or on any such list. ## 15. Choice of Law These Terms shall be governed by the laws of the State of California, without regard to its conflict of laws rules or principles. ## 16. Venue You agree to exclusive jurisdiction in California and venue in Santa Clara County, California for all arbitration and other proceedings arising out of these Terms. ## 17. Arbitration of Disputes ANY CLAIM, DISPUTE, OR CONTROVERSY (WHETHER IN CONTRACT, TORT, OR OTHERWISE, WHETHER PREEXISTING, PRESENT OR FUTURE, AND INCLUDING STATUTORY, CONSUMER PROTECTION, COMMON LAW, INTENTIONAL TORT AND EQUITABLE CLAIMS) BETWEEN YOU AND US OR ANY OF OUR AFFILIATED ENTITIES OR OURS OR THEIR AGENTS, EMPLOYEES, PRINCIPALS, SUCCESSORS, OR ASSIGNS ARISING FROM OR RELATING TO THESE TERMS, ITS INTERPRETATION, OR THE BREACH, TERMINATION OR VALIDITY HEREOF, OR THE RELATIONSHIPS WHICH RESULT FROM THESE TERM (INCLUDING, TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, RELATIONSHIPS WITH THIRD PARTIES WHO ARE NOT SIGNATORIES TO THIS AGREEMENT), SHALL BE RESOLVED EXCLUSIVELY AND FINALLY BY BINDING ARBITRATION ADMINISTERED BY JAMS before a retired judge in Santa Clara County, California. In the event such a JAMS proceeding is unavailable for any reason, such disputes shall be governed by the Commercial Arbitration Rules and the Supplementary Procedures for Consumer Related Disputes (collectively, “AAA Rules”) of the American Arbitration Association ("AAA"), as modified by these Terms, and will be administered by the AAA before a single retired judge. The arbitrator shall be empowered to grant whatever relief would be available in a court under law or in equity. This Section and Section ____ below are subject to the Federal Arbitration Act, 9 U.S.C. sec. 1-16 (FAA), as amended. Any award of the arbitrator shall be final and binding on each of the parties, and may be entered as a judgment in any court of competent jurisdiction. The arbitration proceeding will be limited solely to the dispute or controversy between you and us. YOU ACKNOWLEDGE THAT YOU ARE GIVING UP YOUR RIGHTS TO LITIGATE CLAIMS IN A COURT OR BEFORE A JURY WITH RESPECT TO ANY SUCH CLAIM. Nothing in this Section 24 shall be deemed to prohibit us from seeking an injunction or other equitable relief in any court of competent jurisdiction to protect or preserve ours or our licensors’ rights in and to intellectual property or confidential information. ## 18. Class Action Waiver IN ANY DISPUTE, NEITHER YOU NOR ANY OTHER PERSON SHALL BE ENTITLED TO JOIN OR CONSOLIDATE CLAIMS BY OR AGAINST OTHER AFFILIATES OR PERSONS, OR ARBITRATE ANY CLAIM AS A REPRESENTATIVE OR CLASS ACTION OR IN A PRIVATE ATTORNEY GENERAL CAPACITY. YOU ACKNOWLEDGE THAT YOU ARE GIVING UP YOUR RIGHTS TO PARTICIPATE IN A CLASS ACTION OR REPRESENTATIVE ACTION WITH RESPECT TO ANY SUCH CLAIM. ## 19. Electronic Communications By using the Services, you consent to receiving electronic communications, e.g., email, from us or our subsidiaries and affiliated entities. These communications will include notices about your Account and information concerning or related to the Services. These communications are part of your relationship with us and you receive them as part of your membership. You agree that any notice, agreements, disclosures or other communications that we send to you electronically will satisfy any legal communication requirements, including but not limited to, any requirements that such communications be in writing. ## 20. Severability If any provision of this Agreement is held to be unenforceable under applicable law, such provision shall be excluded from this Agreement, and the balance of this Agreement shall be interpreted as if such provision was so excluded and shall be enforceable in accordance with its modified terms. ## 21. Merger; Translations These Terms represent the entire understanding between the parties with respect to the subject matter hereof and supersede all previous understandings, written, oral or implied. Where we have provided you with a translation of the English language version of these Terms, then you agree that the translation is provided for your convenience only and that the English language versions of these Terms will govern your relationship with us. If there is any contradiction between what the English language version of these Terms and any translation, the English language version shall take precedence. ## 22. Force Majeure Neither you nor we shall be held responsible for any delay or failure in performance hereunder caused by acts of God (or natural disasters), terrorism, strikes, embargoes, fires, war, or other causes beyond the affected party’s reasonable control. ## 23. Construction The headings used herein are for convenience only and shall not be deemed to define, limit or construe the content of any provision of these Terms. The meanings given to terms defined herein will be equally applicable to both the singular and plural forms of such terms. Whenever the context may require, any pronoun includes the corresponding masculine, feminine and neuter forms. ## 24. Notices Except as explicitly stated otherwise, legal and other notices (including but not limited to notices of legal proceedings) shall be delivered to Inovio Payments, Inc. by U.S. mail at 220 Humboldt Ct, Sunnyvale, CA 94089 Attn. Legal, or to you at the email address you provided us (a) at the time you registered; (b) through a subsequent notice of an address change; or (c) through a posting through the Services. Physical notices shall be effective when received. Email notices allowed hereunder shall be deemed given 24 hours after email is sent, unless the sending party is notified that the email address is invalid. In addition, we may provide notice by certified mail, postage prepaid and return receipt requested. In such case, notice shall be deemed given when received. ## 25. Waiver Failure to enforce any provision of these Terms shall not constitute a waiver of any term hereof. No waiver of a breach of any provision of these Terms shall constitute a waiver of any prior, concurrent or subsequent breach of the same or any other provision hereof, and no waiver shall be effective unless granted in writing and signed by an authorized representative of us at our director level or above. ## 26. Limitations of Claims You agree that any claim or cause of action arising out of or related to these Terms or your use of the Services must be filed within one (1) year after such claim or cause of action arose or be forever barred. ## 27. Non-Assignment You may not resell, assign or transfer any of your rights or obligations under these Terms without our prior written consent. We may resell, assign or transfer our rights and obligations under these Terms at any time without restriction and without notice or consent. ## 28. Agreement Binding This Agreement shall be binding upon the parties and their successors and permitted assigns. --- # Thank you We received your message. Source: https://developer.inoviopay.com/thank-you.html Markdown: https://developer.inoviopay.com/thank-you.md Thanks for reaching out. We've received your message and someone will get back to you soon. [Back to home](https://developer.inoviopay.com/index.md)