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, how to identify the terminal, and how to attribute a webhook to it.
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 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
POSTto/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:writescope, 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 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.
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. |
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. |
[!WARNING] Setting
isEncryptedtotrueon a cleartext read makes WinkPG treatemvDataas 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:
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, plusencryptedTrack1Datawhen 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.serialNumberand nowhere else. A read with an emptyserialNumberisn't decrypted. - When the merchant is boarded,
cardData.keySerialNumberis read first, anddeviceData.serialNumberis 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.
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
deviceDatablock, theregister, and themerchantReference. - Presence flags in place of the raw read:
cardData.hasEmvData,cardData.hasTrackData,cardData.hasEncryptedTrackData, andcardData.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:
{
"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
}
}
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. 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. Receipts print the name after TID:. |
merchantReference |
The merchant's own ticket, order, or invoice number. | Returned on GET. 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), 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 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 doesn't carry the device serial number, the register, or the merchant reference.
To attribute an event to a terminal, take TransactionId from the event body and read the transaction with GET /api/transactions/{transactionId}. The response carries all three identifying fields. See Webhook integration for the envelope, the signature, and retry behavior.
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 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 for defining a field before a terminal sends it.
- Webhook integration for receiving and verifying the lifecycle events a terminal sale produces.
- Refunds, voids, and reversals for correcting a terminal sale after the fact.
- Quickstart: Direct API for the base create request this guide adds to.
- Transaction lifecycle and settlement for the stages a terminal sale moves through and when its batch closes.