Markdown

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.

POSThttps://api.inoviopay.com/payment/token_service.cfm

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 HMAC signature excludes the card number

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 is not your API password

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.

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".

Treat the token as single-use and short-lived

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"

Signing the request

  1. Take the current UTC time as YYYYMMDDHHMMSS. This is x-timestamp.
  2. Generate a new unique_id for every request (alphanumeric, up to 32 characters).
  3. Build the message by concatenating x-timestamp, unique_id and site_id with nothing between them. Do not include card_pan.
  4. Compute HMAC-SHA256 over that message with your Shared Key as the key, and hex-encode the result. This is x-signature.
  5. 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

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