KTAP Cards API
API reference

Card details

Card numbers are never returned as data. You request a short-lived URL and embed it, which keeps the number off your servers and out of your logs.

How it works#

  1. Your server calls GET /cards/{cardId}/pan with your key and gets back a details_url, valid for 600 seconds.
  2. Your page renders that URL in an iframe. The number is displayed by us, inside the frame.
  3. When the cardholder uses the copy control, the frame posts a result back to your page.

A complete embed#

const res = await fetch(
  `https://virtual.ktap.io/api/partner/v1/cards/${cardId}/pan`,
  { headers: { 'X-API-Key': process.env.KTAP_API_KEY } },
);
const { data } = await res.json();
// data.details_url is valid for 600 seconds. Render it, do not store it.
allow="clipboard-write" is required

Permissions Policy delegation is per frame, so without it the clipboard permission never reaches the card page and the copy control silently does nothing. If your page sends its own Content-Security-Policy, add frame-src https://virtual.ktap.io.

The contract#

  • Give the iframe an explicit width and height. The page fills whatever container you give it and has a transparent background.
  • No sandbox attribute is needed. If your policy forces one, allow-scripts and allow-same-origin are the minimum, because without scripts you receive no messages at all.
  • The message is exactly { type: "ktap-card-copy-result", ok: boolean } and it arrives only when the page is embedded. Check event.origin before acting on it.
  • Only the immediate parent receives it. If you nest the frame inside another frame of your own, the intermediate frame has to forward it.
  • Treat ok as a cue to show a confirmation, never as an audit record.
  • Render the URL, do not store it. After 600 seconds it stops working and you request a new one.
GET/api/partner/v1/cards/{cardId}/pan#

Returns a URL on the KTAP host that renders the full card details for the cardholder, plus the card expiry as plain integers. The card number and CVV never appear in this JSON and never pass through your servers: they travel from the card processor to the cardholder's browser inside a nested iframe. Every call mints a fresh URL that is valid for 600 seconds.

Scope cards:pan
Path parameters
cardIdstring
The card id returned by POST /cards
Opaque and case-sensitive. Live ids are 32 characters of letters and digits. Match is exact and scoped to your partner account
Response
card_idstring
The card id you requested, as stored
details_urlstring
Page to load in an iframe. Valid for 600 seconds from this response. Anyone holding it can view the card, so never log it or store it
expiration_monthinteger | null
1 to 12. Null when the card metadata lookup at the processor fails
expiration_yearinteger | null
Four digits, for example 2032. Null when the card metadata lookup at the processor fails
Errors
401UNAUTHORIZED
No key, unknown key, revoked key, or your partner account is suspended. The four cases are deliberately indistinguishable
No. Fix the credential
403FORBIDDEN
The key is valid but lacks the cards:pan scope, which is granted separately from cards:read
No. Ask for the scope
404NOT_FOUND
The card id does not exist, or belongs to another partner. Card status is not checked, so a closed card still resolves here
No
405BAD_REQUEST
Any method other than GET. This response is produced before authentication, so it is not rate limited or logged against your key
No
429RATE_LIMITED
Your key exceeded rate_limit_per_minute in a rolling one-minute window counted from the request log. The default allowance is 120 per minute
Yes, after the Retry-After value of 60 seconds
500UNAVAILABLE
An internal fault on the KTAP side
Yes, shortly
502UNAVAILABLE
The card processor did not return a details URL. The route probes the processor before minting the token so a bad card fails here instead of inside a broken iframe
Yes, shortly
Notes
  • The URL is valid for 600 seconds from the moment it is issued. Render it, do not store it.
  • Your iframe must carry allow="clipboard-write", or the copy control inside the frame silently does nothing.
  • An expired or tampered URL renders as plain text instead of the card. Treat a frame that did not load as HTML as a signal to request a new URL.
curl -sS -X GET 'https://virtual.ktap.io/api/partner/v1/cards/7Kd2mQ4rTz9XbW1sYh6vN0pLcE3fJa8u/pan' \
  -H 'X-API-Key: ktap_live_...'
{
  "data": {
    "card_id": "7Kd2mQ4rTz9XbW1sYh6vN0pLcE3fJa8u",
    "details_url": "https://virtual.ktap.io/card-frame/EXAMPLE_TOKEN.EXAMPLE_SIGNATURE",
    "expiration_month": 10,
    "expiration_year": 2032
  }
}
POST/api/partner/v1/cards/{cardId}/pan-token#

Mints a single-use URL, valid for 60 seconds, that returns the card fields as JSON. Call this from your server, hand the url to the end user's DEVICE, and have the device GET it directly: the response is { data: { pan, cvv, expiry_month, expiry_year } }, uncacheable, and the token burns on first use. If your own servers fetch it instead, the card number transits your infrastructure and your PCI DSS obligations change. Route it through the device. Where an iframe is possible, prefer the embeddable frame at GET /cards/{cardId}/pan.

Scope cards:pan
Path parameters
cardIdstring
Cards you do not own return 404
Response
urlstring
Have the device GET this within 60 seconds. It works exactly once
expires_innumber
Seconds until the token dies unused
single_useboolean
Always true
Errors
404NOT_FOUND
Unknown card, or a card that is not yours
Notes
  • Hand the url to the end user's DEVICE and have the device fetch it. If your servers fetch it instead, the card number transits your infrastructure and your PCI DSS obligations change.
  • One token per display, not per session. The token dies after 60 seconds and burns on first use; expired, used and tampered tokens all answer 404 alike.
  • From the exchange URL, 404 means the token is dead (expired, used or tampered): mint again, never retry. 502 means card details are temporarily unavailable on our side: back off, then restart the flow. The URL allows cross-origin fetches (Access-Control-Allow-Origin: *), so the device may call it straight from a browser or web-view.
curl -sS -X POST 'https://virtual.ktap.io/api/partner/v1/cards/7Kd2mQ4rTz9XbW1sYh6vN0pLcE3fJa8u/pan-token' \
  -H 'X-API-Key: ktap_live_...'
Response
{ "data": { "url": "https://virtual.ktap.io/card-data/example-token", "expires_in": 60, "single_use": true } }
Card details · KTAP Cards API