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#
- Your server calls
GET /cards/{cardId}/panwith your key and gets back adetails_url, valid for 600 seconds. - Your page renders that URL in an iframe. The number is displayed by us, inside the frame.
- 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 requiredPermissions 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
sandboxattribute is needed. If your policy forces one,allow-scriptsandallow-same-originare 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. Checkevent.originbefore 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
okas 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.
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.
cardIdstringcard_idstringdetails_urlstringexpiration_monthinteger | nullexpiration_yearinteger | nullUNAUTHORIZEDFORBIDDENNOT_FOUNDBAD_REQUESTRATE_LIMITEDUNAVAILABLEUNAVAILABLE- 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
}
}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.
cardIdstringurlstringexpires_innumbersingle_usebooleanNOT_FOUND- Hand the
urlto 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
404alike. - From the exchange URL,
404means the token is dead (expired, used or tampered): mint again, never retry.502means 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_...'{ "data": { "url": "https://virtual.ktap.io/card-data/example-token", "expires_in": 60, "single_use": true } }