Tokenization Service
Exchange a card number for a TOKEN_GUID to send in place of PMT_NUMB, with the HMAC signing recipe, test vectors, response fields and optional response verification.
Rather than submitting the card number (PMT_NUMB) with a transaction, you can send a token in its place. You obtain the token by posting the card number to the token service endpoint below, which answers with a TOKEN_GUID. On the later sale or authorization request, send TOKEN_GUID instead of PMT_NUMB. Treat each token as single-use and short-lived: request a new one for every transaction and send it promptly.
This endpoint does not use req_username and req_password. The request is authenticated by an HMAC signature over three of the form fields, computed with your Shared Key (see Signing the request). All parameters are sent as application/x-www-form-urlencoded fields in the POST body, exactly as shown in the cURL example. A request containing only the six fields in the table below, with no username, password or Authorization header, is accepted.
There is no separate sandbox host. You test against this production endpoint with a site that is configured to a test-bank merchant account, as described under API Overview.
| Parameter | Description |
|---|---|
card_pan Required |
Customer card number to be tokenized. |
request_api_version Required |
Must be sent as 4.14. |
site_id Required |
Merchant's website ID. Part of the signed message. |
unique_id Required |
Alphanumeric ID for this request (Max 32 chars). Generate a new one for every request. Part of the signed message. |
x-timestamp Required |
Request time in UTC, format YYYYMMDDHHMMSS (for example 20241209230900). The gateway accepts the request for 5 minutes from this time. A timestamp older than that, or ahead of the gateway's clock, is rejected with error 406, so keep your clock synchronized. |
x-signature Required |
Hex-encoded HMAC-SHA256 of x-timestamp + unique_id + site_id (concatenated in that order, no separators, card number excluded), keyed with your Shared Key. Either hex case is accepted. See Signing the request. |
The gateway 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.
Sign x-timestamp, unique_id and site_id only, omit card_pan from the signed string, and check your code against the test vector under Signing the request.
The shared key used to compute the HMAC is a separate client-level HMAC secret; one key covers every site on your Inovio client. It is not the same as your req_password API password used on pmt_service.cfm requests. You generate it yourself in the Inovio portal under Dev Tools → Key Manager, where you can also rotate or revoke it.
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".
Request a new TOKEN_GUID for every transaction, send it as soon as you receive it, and do not store it. A TOKEN_GUID is not a saved card: to charge the same customer again, use the CUST_ID and PMT_ID returned by the first transaction, as described under Card on File.
# x-signature below is HMAC-SHA256("20241209230900" + "REQ999" + "12345") keyed with the
# dummy Shared Key 0123456789abcdef0123456789abcdef. Replace the timestamp with the current
# UTC time, use a fresh unique_id, and recompute with your own key before sending.
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=1f214a2534be7800d79220049541408727c4b5613f5f1cb6fb14b01489c58544"
use Inovio\Gateway\{Credentials, InovioClient};
use Inovio\Gateway\Model\{LineItem, Money, PaymentMethods};
use Inovio\Gateway\Request\TransactionRequest;
// The shared 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";
import { InovioClient, Money, PaymentMethods } from '@inovio/gateway-sdk';
// The shared 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);
from inovio_gateway import Credentials, InovioClient, Money, PaymentMethods
from inovio_gateway.model import Idempotency, LineItem
from inovio_gateway.request import TransactionRequest
# The shared 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)
// The shared 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());
{
"TOKEN_GUID": "3A393EBC3A266B9842FC55DB44B5401C697A5768",
"CARD_BRAND_NAME": "",
"CARD_TYPE": "",
"CARD_BANK": "",
"CARD_COUNTRY": "",
"CARD_ACCOUNT_FUND_SOURCE": "",
"CARD_CLASS": "",
"TOKEN_IP": "192.168.1.1",
"TOKEN_REQID": 73393758223
}
Signing the request
- Take the current UTC time as
YYYYMMDDHHMMSS. This isx-timestamp. - Generate a new
unique_idfor every request (alphanumeric, up to 32 characters). - Build the message by concatenating
x-timestamp,unique_idandsite_idwith nothing between them. Do not includecard_pan. - Compute HMAC-SHA256 over that message with your Shared Key as the key, and hex-encode the result. This is
x-signature. - Send all six fields in the form body.
Check your implementation against these vectors before using a real key. Both use the dummy Shared Key 0123456789abcdef0123456789abcdef and site_id 12345.
| Input | Vector A | Vector B |
|---|---|---|
x-timestamp |
20241209230900 |
20260101120000 |
unique_id |
REQ999 |
REQ001 |
| Message | 20241209230900REQ99912345 |
20260101120000REQ00112345 |
Expected x-signature |
1f214a2534be7800d79220049541408727c4b5613f5f1cb6fb14b01489c58544 |
01e3958a9439e5384571aa2dfb7e4b9df9a7c596e27123924b7a7d8d014fb92f |
Vector A is the request in the cURL example above. Reproduce it from a shell:
printf '%s' "20241209230900REQ99912345" | openssl dgst -sha256 -hmac "0123456789abcdef0123456789abcdef"
# 1f214a2534be7800d79220049541408727c4b5613f5f1cb6fb14b01489c58544
If your code produces 759b1cd0557d418ac04514cdbf06996f69bb616f1c8b45b98cb989a1ea15de8c for the Vector B inputs, it is including the card number 4111111111111111 in the message; the gateway rejects that with error 121.
Postman cannot compute the signature from a static body. Add the pre-request script below to the request and reference {{ts_value}}, {{unique_id_value}} and {{signature_value}} in the x-timestamp, unique_id and x-signature body fields. Keep site_key in a Postman variable's Current Value only, so it is never exported with the collection.
const siteKey = pm.variables.get('site_key');
const siteId = pm.variables.get('site_id');
const d = new Date(), p = n => String(n).padStart(2, '0');
const timestamp = d.getUTCFullYear() + p(d.getUTCMonth() + 1) + p(d.getUTCDate())
+ p(d.getUTCHours()) + p(d.getUTCMinutes()) + p(d.getUTCSeconds());
const uniqueId = ('REQ' + Date.now().toString(36) + Math.random().toString(36).slice(2, 10)).toUpperCase().slice(0, 32);
const signature = CryptoJS.HmacSHA256(timestamp + uniqueId + siteId, siteKey).toString(CryptoJS.enc.Hex);
pm.variables.set('ts_value', timestamp);
pm.variables.set('unique_id_value', uniqueId);
pm.variables.set('signature_value', signature);
Response
The token service answers HTTP 200 for both success and failure, so check the body rather than the status code. A success body carries TOKEN_GUID and the fields below.
| Field | Description |
|---|---|
TOKEN_GUID |
The token. Send it as TOKEN_GUID on the sale or authorization request in place of PMT_NUMB. |
CARD_BRAND_NAME, CARD_TYPE, CARD_BANK, CARD_COUNTRY, CARD_ACCOUNT_FUND_SOURCE, CARD_CLASS |
Best-effort BIN metadata. Always present; empty strings when the BIN is not in the lookup table. Treat empty as unknown. |
TOKEN_IP |
The IP address the request came from. |
TOKEN_REQID |
Numeric request id. Quote it when contacting support. |
A failure body has no TOKEN_GUID and instead carries error_code, error_message, error_detail and error_advice:
{
"error_code": "121",
"error_message": "Get CCtoken GUID signature match fail.",
"error_detail": "",
"error_advice": ". Reference ID 43624025"
}
Error 121 means the signature did not match: check that the message is x-timestamp + unique_id + site_id with no card number, and that the key is the Shared Key from Dev Tools → Key Manager, not the API password. Error 406 means x-timestamp is outside the accepted window: send the current UTC time and check your clock. Both are listed under API Response Codes.
Verifying the response signature (optional)
A successful response carries two headers, X-TIMESTAMP (the gateway's UTC time as YYYYMMDDHHMMSS) and X-SIGNATURE. You can use them to check that the response came from Inovio and was not altered in transit. X-SIGNATURE is the uppercase hex HMAC-SHA256, keyed with your Shared Key, of X-TIMESTAMP + TOKEN_REQID + the response body, concatenated with nothing between them. Use the TOKEN_REQID digits as they appear in the body, and hash the body bytes exactly as received, with only the single trailing newline removed. Compare hex values case-insensitively. Failure responses carry neither header.
import hashlib, hmac
def response_is_authentic(site_key: str, x_timestamp: str, token_reqid: int, raw_body: bytes, x_signature: str) -> bool:
body = raw_body[:-1] if raw_body.endswith(b"\n") else raw_body
message = x_timestamp.encode() + str(token_reqid).encode() + body
expected = hmac.new(site_key.encode(), message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected.upper(), x_signature.upper())