# 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"
}
```
