# Card-present payments over the API

What a terminal application sends on the transactions API for a magnetic stripe, chip, or contactless read, which reader encryption needs setup first, what each processor does with healthcare amounts, how to tokenize a read without charging it, how to identify the terminal, and how to attribute a webhook to it.

**Category:** Payments

**Last reviewed:** 4 November 2026

# Card-present payments over the API

This guide is for developers writing a terminal application that reads a card at a counter, a lane, or a handheld device and sends the read to WinkPG. It covers the fields that make a transaction card-present, what reader encryption needs set up before the first sale, how to tell which terminal took a transaction, and how a webhook receiver attributes an event to that terminal.

Read [Quickstart: Direct API](https://devportal.signapay-sbx.winkpg.io/docs/guides/quickstart-direct-api.md) first if you haven't taken a payment through the transactions API yet. Everything here is an addition to that request.

## When this applies

This guide applies when the terminal application calls the transactions API directly and WinkPG authorizes the payment with the processor. The terminal reads the card, builds the create request, and reads the result from the response.

There's nothing terminal-specific to set up on the API side:

- **No terminal endpoint.** A card-present sale is a `POST` to `/api/transactions`, the same create request every other payment uses.
- **No device registration.** WinkPG doesn't keep a list of devices, and a new terminal doesn't need to be enrolled before it sends its first sale.
- **No per-device credential.** The merchant's API key, with the `transactions:write` scope, is the whole handshake. Every terminal the merchant runs can send with the same key, or you can issue one key per site or per device if you want to revoke one without affecting the others.

A terminal that authorizes with the processor itself and then reports the finished transaction to WinkPG is a different model, and this guide doesn't describe it.

## What the merchant configures first

Two things on the merchant's processor profile decide whether a card read reaches the processor as a card-present transaction. Both are set once, by whoever boards the merchant, and apply to every terminal. Neither is configured per device.

- **An active processor profile with a card-present industry**, such as **Retail** or **Food/Restaurant**. Routing doesn't look at the entry mode, so a card-present request goes to whichever profile the merchant's routing selects. The processor message is built for that profile's industry: on a profile set to **Electronic Commerce** or **Direct Marketing**, the message isn't a card-present one, and the chip data may not be sent at all.
- **The processor's terminal number on that profile.** Depending on the processor, the field is labeled **Terminal Number** or **Terminal ID**. It's the value the processor assigned to the merchant's terminal configuration, and every terminal application sending through the profile shares it.

A merchant with no active processor profile can't take any payment. The create request comes back with the `ProcessorNotConfigured` result code.

## The request

A card-present request is the ordinary create request with three additions: the entry mode, the data the reader produced, and a `deviceData` block describing the device.

### Entry mode

`cardData.entryMode` says how the card was read on this transaction. It's what makes the transaction card-present, so always send it from a terminal.

| `entryMode` | Send it when |
|---|---|
| `Manual` | The card number was keyed on the terminal. A keyed number is processed as a card-not-present entry unless you also set `cardholderPresence` (see below). |
| `UnencryptedCardReaderSwipe` | The magnetic stripe was read and the reader produced cleartext track data. |
| `EncryptedCardReaderSwipe` | The magnetic stripe was read and the reader encrypted the track data. |
| `Icc` | The chip was read by contact (a dip). |
| `Proximity` | The card or phone was tapped (contactless). |
| `Fallback` | The chip couldn't be read, so the magnetic stripe was read instead. |
| `TechnicalFallback` | The chip couldn't be read because of a fault in the chip or the reader, so the magnetic stripe was read instead. |
| `EmptyCandidateFallback` | The chip was read, but the card and the terminal share no application, so the magnetic stripe was read instead. |

Every value except `Manual` classifies the transaction as card-present. Leaving `entryMode` out stores it as `Unknown`, and the processor message then infers presence from the profile's industry instead of from the read. For a terminal read, that inference is the wrong place to start.

### Cleartext read fields

When the reader hands you the card data in the clear, send it on these fields. The encrypted case is in [Encrypted reads](#encrypted-reads) below.

| Field | Carries | Required |
|---|---|---|
| `cardData.trackData` | The track data from a magnetic stripe read. | For `UnencryptedCardReaderSwipe`, unless you send `emvData` instead. |
| `cardData.emvData` | The chip data the reader produced, as a TLV string. | For `Icc` and `Proximity`. |
| `deviceData.cvmResults` | The cardholder verification method the reader used: `1` for offline PIN, `2` for online PIN, `3` for signature. | No. Send it when the reader reports it and the chip data has no tag `9F34`. WinkPG reads it only as a fallback for that tag. |

A request whose fields don't match its entry mode is rejected with HTTP `400` and a `CardData:` code. For example, `Icc` with no `emvData` returns `CardData:EmvDataRequiredForIccOrProximity`, and `UnencryptedCardReaderSwipe` with neither `trackData` nor `emvData` returns `CardData:TrackOrEmvRequiredForUnencryptedSwipe`.

### Cardholder presence

`cardholderPresence` describes whether the cardholder is at the point of sale. That's a different question from how the card was read. Its values are `CardPresent`, `MOTO`, `ECommerce`, and `Recurring`.

Leave it unset for a magnetic stripe, chip, contactless, or fallback read. WinkPG infers `CardPresent` from any of those entry modes, which is the right answer.

Set it when the inference would be wrong. The common case is a keyed entry at a counter: with `entryMode` set to `Manual`, WinkPG infers `MOTO`, so a cardholder standing at the terminal needs `cardholderPresence` set to `CardPresent` explicitly.

`cardholderPresence` doesn't choose the channel the processor receives. On Fiserv and TSYS, the industry on the merchant's processor profile does: a retail profile sends card-present transactions, a direct marketing profile sends MOTO transactions, and an electronic commerce profile sends e-commerce transactions. When you set `cardholderPresence` to `CardPresent`, `MOTO`, or `ECommerce` on an authorization or a sale, and the profile sends a different channel, the transaction is processed as the profile's channel and `responseData.cardholderPresenceProfileMismatch` is `true`. It's `false` when the two agree, and it's absent when there's nothing to compare. A mismatch doesn't change the outcome, so if you see it, ask the merchant's administrator to check the processor profile.

### Terminal capability

`cardData.terminalCapability` declares what the terminal hardware can read, which can differ from how this card was read. Its values are `MagneticStripe`, `MagneticStripeWithPin`, `Chip`, `ChipWithPin`, `ChipContactless`, and `ChipContactlessWithPin`.

It's optional. Leave it unset and the processor message carries the processor's default capability. Set it when the terminal is stripe-only, so the processor doesn't see a chip capability the device doesn't have. A stripe-only capability combined with a chip, contactless, or fallback entry mode is a contradiction, and it's rejected with HTTP `400` and `CardData:EntryModeIncompatibleWithTerminalCapability`.

### Device data

The `deviceData` block holds two kinds of field, and they behave differently.

These three fields are telemetry. WinkPG stores them and shows them back, and they don't change how the payment is processed:

| Field | Carries |
|---|---|
| `deviceData.serialNumber` | The device's serial number. This is the field that identifies the physical device; see [Identifying the terminal](#identifying-the-terminal). |
| `deviceData.sdkVersion` | The version of the terminal application or the reader SDK. |
| `deviceData.batteryLevel` | The battery level, as a whole-number percentage, for a device that reports one. |

These two fields change processing:

| Field | Effect |
|---|---|
| `deviceData.isEncrypted` | `true` tells WinkPG that every card data field on the request holds ciphertext, including `emvData`. Defaults to `false`. |
| `deviceData.encryptionType` | The scheme the reader encrypted with. It selects the decryption path; see [Encrypted reads](#encrypted-reads). |

> [!WARNING]
> Setting `isEncrypted` to `true` on a cleartext read makes WinkPG treat `emvData` as an opaque encrypted payload instead of TLV. The chip data isn't parsed, so the card application isn't identified from it.

### Example

A cleartext chip read, sent from lane 4:

```bash
curl -X POST "https://your-gateway-host/api/transactions" \
  -H "api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "transactionType": "Sale",
    "idempotencyKey": "lane-04-000123",
    "merchantReference": "TICKET-000123",
    "register": {
      "id": "00000000-0000-0000-0000-000000000004",
      "name": "Lane 4"
    },
    "cardData": {
      "entryMode": "Icc",
      "emvData": "EMV_TLV_FROM_THE_READER"
    },
    "deviceData": {
      "serialNumber": "DEVICE_SERIAL_NUMBER",
      "sdkVersion": "TERMINAL_APP_VERSION",
      "isEncrypted": false
    },
    "invoiceData": {
      "amounts": { "base": 10.00, "total": 10.00 }
    }
  }'
```

`EMV_TLV_FROM_THE_READER` stands for the TLV string the reader returned for this one read. Never build it by hand or reuse one from an earlier read: the chip produces a new cryptogram for each authorization.

## Encrypted reads

When the reader encrypts the card data, set `deviceData.isEncrypted` to `true`, set `deviceData.encryptionType` to the reader's scheme, and send the key serial number the reader reports on `cardData.keySerialNumber`. The ciphertext goes on these fields:

- `cardData.encryptedTrack2Data`, plus `encryptedTrack1Data` when the reader produces it. This is the only payload WinkPG decrypts itself, so every scheme it decrypts needs it, including on a chip read.
- `cardData.emvData`, for an encrypted chip or contactless read. WinkPG never decrypts this field. It's forwarded as ciphertext on the schemes that forward, and ignored by the decryption on the schemes that don't.

Whether anything has to be set up first depends on the scheme. The comparison on `encryptionType` ignores case and surrounding spaces.

| `encryptionType` | What it needs before the first sale | What WinkPG does with the read |
|---|---|---|
| `MAGENSA` | Nothing on the WinkPG side. | Decrypts nothing. Forwards the encrypted tracks, the encrypted chip data, and the key serial number to the decryption service in front of the processor. |
| `PAXDUKPT` | The reader is injected with WinkPG's base derivation key. That's a provisioning step with the key injection facility, not an API call. | Decrypts the encrypted track 2 data with that key before authorization. If the merchant is boarded on a payment encryption provider, the read is decrypted through the provider instead, on the same terms as `DUKPT` below. |
| `DUKPT` or `ONGUARD` | An administrator boards the merchant on a payment encryption provider and adds a key entry for each key serial identifier the merchant's readers use. | Decrypts through the provider, using the key entry the read's key serial number matches. A key serial number with no matching entry is refused with HTTP `403` and `Decryption:UnknownKeySerialIdentifier`, and a read with no encrypted track data or no key serial number with HTTP `400` and `Decryption:InvalidPayload`. Neither creates a transaction. A merchant that isn't boarded has the read forwarded to the processor still encrypted. |
| Any other value | Nothing on the WinkPG side. | Decrypts nothing. Forwards the payload to the processor as sent. |

`PAXDUKPT` reads the key serial number from a different place than the other schemes:

- When the merchant isn't boarded on a provider, the key serial number is read from `deviceData.serialNumber` and nowhere else. A read with an empty `serialNumber` isn't decrypted.
- When the merchant is boarded, `cardData.keySerialNumber` is read first, and `deviceData.serialNumber` is the fallback.

Every other scheme reads `cardData.keySerialNumber` only, and `deviceData.serialNumber` stays the device's own serial number.

If the merchant is boarded on a payment encryption provider, the **Encrypted card reader payments** guide in the help center covers the decryption record on the response and every refusal code to handle.

## Online PIN

For a PIN debit or EBT sale, send the encrypted PIN block the PIN pad produced on `cardData.pin`, together with the key serial number the block was encrypted under. WinkPG never decrypts or translates a PIN block. It forwards the block and its key serial number to the processor unchanged, so the PIN pad's PIN key has to be one the processor can derive.

Which field carries that key serial number depends on how the reader was injected:

| Reader keys | Send |
|---|---|
| One key for card data and PINs | The key serial number on `cardData.keySerialNumber`. Leave `cardData.pinKeySerialNumber` out. |
| Separate data and PIN keys | The data key's serial number on `cardData.keySerialNumber`, and the PIN key's on `cardData.pinKeySerialNumber`. |

Separate keys are the usual arrangement when a payment encryption provider holds the data key and the processor owns the PIN key. `keySerialNumber` is the one WinkPG uses to decrypt the card data, and `pinKeySerialNumber` travels with the PIN block. `pinKeySerialNumber` is never used to decrypt anything, so it needs no key entry on the merchant's payment encryption provider.

`pinKeySerialNumber` is 10 to 24 hexadecimal characters, with or without the reader's leading `F` padding. Any other value is rejected with HTTP `400` and `CardData:PinKeySerialNumberInvalid`.

## Healthcare (FSA and HSA) amounts

For a sale on a healthcare card, send the healthcare part of the basket on `fsa`, one amount per category, and set `fsa.partialAuthSupport` to `true`. A healthcare card often covers only part of the sale, and a partial approval lets that part approve. Each amount is the part of the sale in that category, not the sale total, and the healthcare total WinkPG sends is the sum of `qhpAmount`, `rxAmount`, `visionAmount`, `dentalAmount`, `clinicalAmount`, and `copayAmount`.

```json
"fsa": {
  "rxAmount": 12.34,
  "copayAmount": 5.00,
  "partialAuthSupport": true
}
```

What reaches the processor depends on the processor:

| Field | Fiserv | TSYS | Chase Paymentech |
|---|---|---|---|
| Healthcare total | Sent on Visa and Mastercard | Sent on Visa and Mastercard | Not sent |
| `rxAmount` | Sent on Visa | Sent | Not sent |
| `visionAmount` | Sent on Visa | Sent | Not sent |
| `clinicalAmount` | Sent on Visa | Sent | Not sent |
| `dentalAmount` | Sent on Visa | Sent | Not sent |
| `copayAmount` | Sent on Visa and Mastercard | In the total only | Not sent |
| `qhpAmount` | In the total only | In the total only | Not sent |
| `transitAmount` | Not sent | Not sent | Not sent |
| `partialAuthSupport` | Honored | Honored | Honored |

On Fiserv, the amounts ride a sale, an authorization, and an online refund. On TSYS:

- The amounts are sent only once healthcare amounts are enabled for your deployment. Until then, a TSYS sale carrying them is authorized for its full amount without the healthcare detail.
- They ride a sale or an authorization from a magnetic stripe read, a keyed card, or a card-not-present payment. A chip read, a refund, a stored-credential or digital wallet payment, a 3-D Secure authenticated payment, a payment with cash back, and a card-not-present payment at a retail or restaurant merchant are sent without them.
- At most three of `rxAmount`, `visionAmount`, `clinicalAmount`, and `dentalAmount` are itemized, taken in that order. When all four are present, `dentalAmount` is sent inside the total.
- Sending the amounts states that the merchant checked the purchase against an Inventory Information Approval System (IIAS). Send them only from a point of sale that does.

`transitAmount` isn't healthcare spend, so it isn't part of the healthcare total, and no processor receives it. On every processor, an amount that isn't sent leaves the sale unchanged.

## What comes back and what's stored

The response to the create request, and every later `GET /api/transactions/{transactionId}`, carries the transaction as stored. For a card-present sale, that includes:

- `cardData.entryMode`, as you sent it. It also classifies the transaction under **POS Terminal** in the **Entry Mode** filter on the transactions list.
- The whole `deviceData` block, the `register`, and the `merchantReference`.
- Presence flags in place of the raw read: `cardData.hasEmvData`, `cardData.hasTrackData`, `cardData.hasEncryptedTrackData`, and `cardData.hasPin`.

The raw track data, chip data, encrypted tracks, PIN, and reader-generated dynamic verification value are never stored. They're used for the authorization and dropped, so a read can't be recovered from the transaction afterward, and the presence flags are the only record that the request carried them.

An abridged response for the example above:

```json
{
  "id": "6f3b2c18-0a4d-4a9e-9d4f-2b71c2f0a911",
  "transactionType": "Sale",
  "merchantReference": "TICKET-000123",
  "register": {
    "id": "00000000-0000-0000-0000-000000000004",
    "name": "Lane 4"
  },
  "cardData": {
    "entryMode": "Icc",
    "hasEmvData": true,
    "hasTrackData": false,
    "hasEncryptedTrackData": false,
    "hasPin": false
  },
  "deviceData": {
    "serialNumber": "DEVICE_SERIAL_NUMBER",
    "sdkVersion": "TERMINAL_APP_VERSION",
    "isEncrypted": false
  }
}
```

## Tokenize a terminal read

A terminal can store a card without charging it, so the merchant can charge it later. There's no single request that does this from a read:

- `POST /api/tokens/create-standalone-async` takes a card number in the clear and no `deviceData`. It doesn't decrypt anything or read track or chip data, so it can't tokenize a read.
- The transactions API refuses `saveCardRequested`, so a payment can't store its own card.

Make two calls instead:

1. A zero-dollar `Authorization` that carries the read. WinkPG decrypts the read before anything else looks at the card, takes the card number and expiration date from the decrypted track, and verifies the card with the processor without holding any funds.
1. `POST /api/tokens/create-from-transaction-async` with that transaction's identifier. WinkPG stores the card number it took from the read and returns a token.

### Step 1: Verify the card for zero

Send the read on an ordinary create request, with `transactionType` set to `Authorization` and every amount set to zero. An encrypted read carries the same fields it carries on a sale (see [Encrypted reads](#encrypted-reads)): `deviceData.isEncrypted`, `deviceData.encryptionType`, `cardData.keySerialNumber`, `cardData.encryptedTrack2Data`, and `cardData.encryptedTrack1Data` when the reader produces it. Add `cardData.emvData` for a chip or contactless read.

```bash
curl -X POST "https://your-gateway-host/api/transactions" \
  -H "api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "transactionType": "Authorization",
    "idempotencyKey": "lane-04-verify-000124",
    "merchantReference": "TICKET-000124",
    "register": {
      "id": "00000000-0000-0000-0000-000000000004",
      "name": "Lane 4"
    },
    "cardData": {
      "entryMode": "Icc",
      "emvData": "ENCRYPTED_EMV_FROM_THE_READER",
      "encryptedTrack2Data": "ENCRYPTED_TRACK2_FROM_THE_READER",
      "keySerialNumber": "KSN_FROM_THE_READER"
    },
    "deviceData": {
      "serialNumber": "DEVICE_SERIAL_NUMBER",
      "sdkVersion": "TERMINAL_APP_VERSION",
      "isEncrypted": true,
      "encryptionType": "DUKPT"
    },
    "invoiceData": {
      "amounts": { "base": 0.00, "total": 0.00 }
    }
  }'
```

Keep the transaction `id` from the response, and go on to step 2 only when `responseData.resultCode` reads `Ok`. Step 2 stores a card only from a transaction the processor approved: it refuses a declined verification, one that returned an error, or one with no result yet, with HTTP `400` and `Transactions:TokenMintTransactionNotApproved`, and stores nothing. Reading the result first saves that call. When the verification is declined, run a new one rather than repeating step 2.

A zero-dollar authorization holds no funds, never settles, and has nothing to void. WinkPG refuses a `Void` against it with `VOID_NOT_APPLICABLE_ZERO_DOLLAR_VERIFICATION`. It needs a processor that supports zero-dollar account verification, the same capability the **Save Payment Method** option on a hosted payment page needs.

On TSYS, a zero-dollar magnetic stripe or keyed read is sent to the processor as an account verification. A zero-dollar chip or contactless read is sent as an authorization for zero with the chip data.

### Step 2: Create the token from the transaction

Pass the transaction's identifier and your merchant identifier on the query string. Add `customerId` when the card belongs to a customer.

```bash
curl -X POST "https://your-gateway-host/api/tokens/create-from-transaction-async?transactionId=TRANSACTION_ID&merchantId=MERCHANT_ID&customerId=CUSTOMER_ID" \
  -H "api-key: YOUR_API_KEY"
```

The response carries the token's `publicReference`, which is the value to send as `tokenData.token` on a later charge, and a masked copy of what was stored. An abridged response:

```json
{
  "publicReference": "pt_4mRt8XwQ2nLc6ZbK9hJ3vD7s",
  "maskedPaymentDetails": {
    "cardData": {
      "cardNumber": "**** **** **** 1111"
    }
  },
  "type": "Card"
}
```

[Reusing a stored payment method with payment tokens](https://devportal.signapay-sbx.winkpg.io/docs/guides/reusing-saved-cards.md) covers charging the stored card later.

### What's stored and what isn't

The token holds the card number and expiration date WinkPG took from the read, together with the name on card, billing address, email, and phone you sent on the authorization. It never holds the track data, the chip data, the encrypted tracks, the key serial number, the PIN, or a cryptogram. Those were used for the verification and dropped, the same as on a sale.

Whether there's a card number to store depends on whether WinkPG decrypted the read:

| Read | Can it be tokenized? |
|---|---|
| `DUKPT` or `ONGUARD` | Yes, when the merchant is boarded on a payment encryption provider. A merchant that isn't boarded has the read forwarded to the processor still encrypted, which leaves no card number to store. |
| `PAXDUKPT` | Yes, when WinkPG decrypts the read. On a merchant that isn't boarded on a provider, a read that can't be decrypted is forwarded still encrypted and leaves no card number to store. |
| `MAGENSA` | No. WinkPG forwards the read to the decryption service without decrypting it, so it never has the card number. The transaction holds only a masked number. |
| Any other `encryptionType` | No, for the same reason: the payload is forwarded as sent. |
| A cleartext magnetic stripe read | Yes. The card number and expiration date come from the track data. |
| A cleartext chip or contactless read with no track data | No. WinkPG doesn't take the card number from chip data. |

An authorization sent over the transactions API holds no other credential WinkPG could store in place of the card number, because the API doesn't let a payment ask to save its card. So when there's no card number, the create call is refused with HTTP `400` and `Transactions:TokenMintTenderMissing`, and no token is created.

### Record the cardholder's consent

A terminal can ask the cardholder, on its screen or for a signature, to let the merchant store the card and charge it later without them present. Card network rules require that agreement before a merchant-initiated charge, and WinkPG checks for it. Record it on the same create call: send the `customerId` of the customer the card is for, and send the agreement in the body as `storedCredentialConsent`. It holds when the cardholder agreed (`capturedAtUtc`), how (`method`, such as `OnScreen` or `SignedForm`), the uses they agreed to (`allowedUsages`), the version of the consent text the terminal showed (`consentTextVersion`), and your own reference to the signature or log line you kept (`evidenceReference`).

```bash
curl -X POST "https://your-gateway-host/api/tokens/create-from-transaction-async?transactionId=TRANSACTION_ID&merchantId=MERCHANT_ID&customerId=CUSTOMER_ID" \
  -H "api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "storedCredentialConsent": {
      "capturedAtUtc": "2026-10-14T15:30:00Z",
      "method": "OnScreen",
      "allowedUsages": "UnscheduledCOF",
      "consentTextVersion": "CONSENT_TEXT_VERSION_YOU_SHOWED",
      "evidenceReference": "terminal-7-sig-000481"
    }
  }'
```

The card is stored on the customer, the consent is recorded against it, and the response also carries the `storedCredentialConsentId`. Recording a consent needs the `StoredCredentialConsents.Create` permission as well as the token permission. Keep the signature image or recording on your own system, and send only your reference to it. The consent appears in the back office under **Stored Credential Consents**, captured via `Api`, with your reference as its **Merchant Reference**.

If you create the token without a consent, the card has none until someone records one. An operator does that in the back office, from the **Stored Credential Consents** section of the customer, and until then WinkPG refuses a merchant-initiated charge against the card. The **Stored credential consents** guide in the help center covers the other ways a consent is captured, and how to revoke one.

## Completing a chip transaction

After an online chip or contactless authorization, the card expects the issuer's answer before it produces its final cryptogram. WinkPG returns that answer on the create response as `responseData.emv`, on an approval and on a decline. Pass it to the terminal's EMV kernel to complete the transaction with the card.

| Field | Carries |
|---|---|
| `responseData.emv.issuerAuthenticationData` | Tag `91`, the value only. The card uses it to authenticate the issuer's response. |
| `responseData.emv.issuerScriptTemplate1` | Every tag `71` template the issuer sent, each with its tag and length, in the order received. The kernel runs these before the final GENERATE AC. |
| `responseData.emv.issuerScriptTemplate2` | Every tag `72` template the issuer sent, each with its tag and length, in the order received. The kernel runs these after the final GENERATE AC. |
| `responseData.emv.authorizationResponseCode` | Tag `8A`, the value only. An approval reads `3030`. |
| `responseData.emv.rawTlv` | The whole EMV response the processor returned, including any tag not broken out above. |

Every value is uppercase hexadecimal, and any field the issuer didn't send is left out. A decline can still carry issuer scripts, such as an instruction to block the card, so read `responseData.emv` on every outcome, not only on an approval.

`responseData.emv` is absent when the processor returned no EMV response data, which is the case for every read that isn't chip or contactless. On TSYS, it's also absent unless the merchant's processor profile sets **EMV Response Data Support** to **Supported**. Whoever boards the merchant sets it once, on the profile.

A later `GET /api/transactions/{transactionId}` returns the same block, until a void or reversal replaces the response data with its own. Webhooks don't carry it. Read it from the create response, which is when the terminal needs it.

An abridged decline that carries an issuer script:

```json
{
  "responseData": {
    "resultCode": "Decline",
    "emv": {
      "issuerAuthenticationData": "1A2B3C4D5E6F70810012",
      "issuerScriptTemplate1": "71169F180411223344860D8424000008AABBCCDDEEFF0011",
      "authorizationResponseCode": "3035",
      "rawTlv": "8A023035910A1A2B3C4D5E6F7081001271169F180411223344860D8424000008AABBCCDDEEFF0011"
    }
  }
}
```

The values above are illustrative. Pass the kernel exactly what the response carries, and never reuse a value from an earlier transaction.

## When the card declines after an online approval

The issuer can approve a chip authorization online and the card can still decline the transaction at the final GENERATE AC, for example when the card rejects the issuer's authentication data. The sale didn't complete, but the issuer is holding the funds. Release the hold with a full `Reversal` on the operations endpoint, with `reversalReasonCode` set to `ChipDeclinedAfterOnlineApproval`:

```bash
curl -X POST \
  "https://your-gateway-host/api/transactions/by-merchant/{merchantId}/{transactionId}/operations" \
  -H "api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "operationType": "Reversal",
    "reversalReasonCode": "ChipDeclinedAfterOnlineApproval",
    "reason": "Card declined at the final GENERATE AC"
  }'
```

Leave out `amount`, so the whole authorization is released. The reversal carries no chip data, and the operations endpoint takes none: the processor finds the original authorization by its identifiers.

How the processor hears the reason depends on the processor:

| Processor | What the reversal states |
|---|---|
| TSYS | Message reason code 2517, chip declined after online issuer approval. |
| Chase Paymentech | The card-declined reversal reason. |
| Fiserv | An ordinary void. Fiserv defines no reason for this case. |

The transaction history records the reason on every processor. For the other reason codes, see [Refunds, voids, and reversals](https://devportal.signapay-sbx.winkpg.io/docs/guides/refunds-voids-and-reversals.md#state-why-youre-reversing).

## When the terminal can't reach WinkPG

WinkPG has no store-and-forward submission. There's no request that carries a sale the card approved offline, with its chip data and offline approval code, for the gateway to settle later. Configure the terminal to authorize online only, and to decline when it can't reach WinkPG, rather than approving under a floor limit.

A `Force` with `originalTransaction.authCode` isn't a substitute. It's built for a voice authorization the merchant obtained by phone, not for a card read, and it isn't processed as a chip, PIN debit, or healthcare (FSA or HSA) sale. Don't send `cardData.pin` or any non-zero `fsa` amount on a `Force`. Depending on the deployment, a `Force` that carries either is refused with HTTP `400` and the code `Transactions:ForceTenderNotSupported`. Submit a PIN debit or healthcare sale online while the cardholder is present.

## Identifying the terminal

Three native fields identify where a transaction came from. Each answers a different question, and you can send all three on the same request.

| Field | Identifies | Where you see it |
|---|---|---|
| `deviceData.serialNumber` | The physical device. | Returned on `GET` and on lifecycle webhooks. Shown as **Serial Number** in the **Device** section of the transaction's detail page. |
| `register.id` and `register.name` | The lane, till, or station, independent of which device is in it today. | Returned on `GET` and on lifecycle webhooks. Receipts print the name after **TID:**. |
| `merchantReference` | The merchant's own ticket, order, or invoice number. | Returned on `GET` and on lifecycle webhooks. It's also carried to the acquirer on the clearing record, so it's the value to reconcile against on the acquirer's transaction report. |

Keep `merchantReference` free of card numbers and other sensitive data, because it's stored on the transaction and sent to the acquirer.

For a `PAXDUKPT` reader on a merchant that isn't boarded, `deviceData.serialNumber` carries the key serial number (see [Encrypted reads](#encrypted-reads)), so use `register` to identify the terminal instead.

Custom fields are a fourth option, for any other value you want to search or report on. The field has to be defined and turned on for the merchant first: once a merchant has at least one custom field turned on, a create request naming a field that isn't one of them is rejected with HTTP `400`. See [Transaction custom fields](https://devportal.signapay-sbx.winkpg.io/docs/guides/transaction-custom-fields.md) for how to define them.

## Webhooks

Every transaction lifecycle event fires for a card-present transaction exactly as it does for any other. WinkPG doesn't filter events by source or entry mode, so a subscription that receives `Transaction.Authorized` for an online payment receives it for a tap at lane 4 too.

The lifecycle event body carries the transaction identifier, the merchant identifier, the status, the amounts, the result and decline reason codes, and on `Transaction.Settled` the batch identifiers. It also carries the values that attribute the transaction to a terminal and to your own sale. They use the same names and nesting as `GET /api/transactions/{transactionId}`, written in the event body's PascalCase, so a case-insensitive parser reads both:

| Field in the event body | Field on `GET` | Carries |
|---|---|---|
| `IdempotencyKey` | `idempotencyKey` | The idempotency key the transaction was created with. For a terminal integration, this is usually your own sale ID. |
| `MerchantReference` | `merchantReference` | The merchant's own ticket, order, or invoice number. |
| `DeviceData.SerialNumber` | `deviceData.serialNumber` | The device serial number. In the event body, `DeviceData` holds the serial number only. |
| `Register.Id` and `Register.Name` | `register.id` and `register.name` | The lane, till, or station. |

Each one is `null` when the transaction was created without it, and `Register` is `null` when the transaction has no register. `Transaction.PartiallyApproved` carries the same four fields.

To attribute an event to a terminal, read these fields from the event body. You don't need to read the transaction back. Read it with `GET /api/transactions/{transactionId}` only when you need something the event doesn't carry. See [Webhook integration](https://devportal.signapay-sbx.winkpg.io/docs/guides/webhook-integration.md) for the envelope, the signature, and retry behavior.

## Receipts

The terminal owns the point-of-sale receipt. Print it from the create response the way your device prints any receipt: WinkPG doesn't send anything to the terminal's printer.

WinkPG also keeps its own receipt for the transaction, and you can read it over the API to print or reprint it. Read it as JSON to lay out your own printout, or as an HTML document ready to print. The receipt is generated in the background shortly after the sale, so a terminal that prints straight away asks for it with `POST /api/transactions/by-merchant/{merchantId}/{transactionId}/receipts` and `receiptType` set to `Authorization`, which returns the receipt and generates it first if it doesn't exist yet. Either way, the receipt shows the card's brand and last four digits only. See [Transaction receipts over the API](https://devportal.signapay-sbx.winkpg.io/docs/guides/transaction-receipts-over-the-api.md) for the requests.

## Refunds, voids, and reversals

A card-present sale is corrected through the same operations as any other transaction, against its transaction identifier. Nothing about the follow-up depends on the terminal. See [Refunds, voids, and reversals](https://devportal.signapay-sbx.winkpg.io/docs/guides/refunds-voids-and-reversals.md) for which operation applies at which point in the transaction's life.

## What to log, and what not to

Never log track data, chip data, the PIN, or any cryptogram, in cleartext or encrypted form. They're card data, and the rules that govern a card number govern them too.

Log the transaction identifier, the entry mode, and the device serial number. Together they answer most support questions about a terminal sale: which transaction it was, how the card was read, and which device read it.

## See also

- [Transaction custom fields](https://devportal.signapay-sbx.winkpg.io/docs/guides/transaction-custom-fields.md) for defining a field before a terminal sends it.
- [Webhook integration](https://devportal.signapay-sbx.winkpg.io/docs/guides/webhook-integration.md) for receiving and verifying the lifecycle events a terminal sale produces.
- [Transaction receipts over the API](https://devportal.signapay-sbx.winkpg.io/docs/guides/transaction-receipts-over-the-api.md) for printing or reprinting the gateway's receipt for a terminal sale.
- [Refunds, voids, and reversals](https://devportal.signapay-sbx.winkpg.io/docs/guides/refunds-voids-and-reversals.md) for correcting a terminal sale after the fact.
- [Reusing a stored payment method with payment tokens](https://devportal.signapay-sbx.winkpg.io/docs/guides/reusing-saved-cards.md) for charging a card stored from a terminal read.
- [Quickstart: Direct API](https://devportal.signapay-sbx.winkpg.io/docs/guides/quickstart-direct-api.md) for the base create request this guide adds to.
- [Transaction lifecycle and settlement](https://devportal.signapay-sbx.winkpg.io/docs/guides/transaction-lifecycle-and-settlement.md) for the stages a terminal sale moves through and when its batch closes.

## See also

- [All documentation](https://devportal.signapay-sbx.winkpg.io/llms.txt): the machine-readable index of every public page on this site.
