Embedded Card UI (Issuing)
Learn how to embed card PANs and CVV codes.
Handling full card PANs and CVV codes requires that you comply with the Payment Card Industry Data Security Standards (PCI DSS). Some clients choose to reduce their compliance obligations by using our embedded card UI solution documented below.
In this setup, PANs and CVV codes are presented to the end-user via a card UI that we provide, optionally styled in the customer's branding using a specified CSS stylesheet.
A user's browser makes the request directly to api.privacy.com, so card PANs and CVVs never touch the API customer's servers while full card data is displayed to their end-users.
The response contains an HTML document. This means that the URL for the request can be inserted straight into the src
attribute of an iframe.
You should compute the request payload on the server-side. You can render it (or the whole iframe) on the server or make an ajax call from your front-end code, but do not embed your API key into front-end code, as doing so introduces a serious security vulnerability.
API Reference: Embedded card UI
GET https://api.privacy.com/v1/embed/card
Requests
curl https://api.privacy.com/v1/embed/card?embed_request=eyJjc3Mi..&hmac=u...
embed_request | A base64 encoded JSON string of an Embed Request to specify which card to load |
hmac | SHA2 HMAC of the embed_request JSON string with base64 digest |
Embed Request Schema (Issuing)
{
"token": String,
"css": String,
"account_token", String,
"expiration": String
}
account_token | Only needs to be included if one or more end-users have been enrolled |
css | A publicly available URI, so the white-labeled card element can be styled with the client's branding |
expiration (optional) | An ISO 8601 timestamp for when the request should expire. |
token | Globally unique identifier for the card to be displayed |
For the expiration
parameter, if no timezone is specified, UTC will be used. If payload does not contain an expiration, the request will never expire.
Using an
expiration
reduces the risk of a replay attack. Without supplying theexpiration
, in the event that a malicious user gets a copy of your request in transit, they will be able to obtain the response data indefinitely.
Response
The endpoint returns an HTML document similar to the one below. It is up to the API client to provide CSS styles for these elements in the Embed Request. You can always rely on the card
, pan
, expiry
, cvv
, and alert
ids, as well as the pan-separator
class. You shouldn't make any other assumptions about the structure of the document as it could change at any time.
Note that using the default style sheet there is no visual indication that copying is happening on-click, and you may need to add on-click styling yourself.
<html>
<head>
<link rel="stylesheet" type="text/css" href="{{ css_uri }}">
<style>
#alert { display: none; }
</style>
<script type="text/javascript">
var timeout;
function clearAlertDelay() {
clearTimeout(timeout);
var messageDiv = document.getElementById('alert');
timeout = window.setTimeout(
function() {
messageDiv.className = "empty";
messageDiv.innerText = "";
},
1200
);
}
function copySuccess(divId) {
var messageDiv = document.getElementById('alert');
messageDiv.innerText = divId + " copied to clipboard";
messageDiv.className = "success";
console.log('Copying to clipboard was successful!');
clearAlertDelay();
// Only included if target_origin is in the embed request
window.parent.postMessage({copyElt: divId, isCopied: true}, '{{ target_origin }}');
}
function copyFailed(divId) {
var messageDiv = document.getElementById('alert');
messageDiv.innerText = "error copying " + divId;
messageDiv.className = "error";
console.error('Copying to clipboard failed');
clearAlertDelay();
// Only included if target_origin is in the embed request
window.parent.postMessage({copyElt: divId, isCopied: false}, '{{ target_origin }}');
}
function copyToClip(divId) {
var messageDiv = document.getElementById('alert');
var copyEl = document.getElementById(divId);
var copyText = copyEl.textContent;
navigator.clipboard.writeText(copyText)
.then(function () {
copySuccess(divId);
clearAlertDelay();
})
.catch(function(err) {
try {
var copied = false;
if (document.createRange) {
range = document.createRange();
range.selectNode(copyEl)
select = window.getSelection();
select.removeAllRanges();
select.addRange(range);
copied = document.execCommand('copy');
select.removeAllRanges();
}
else {
range = document.body.createTextRange();
range.moveToElementText(copyEl);
range.select();
copied = document.execCommand('copy');
}
if (copied) {
copySuccess(divId);
}
else {
copyFailed(divId);
}
}
catch (err) {
copyFailed(divId);
}
clearAlertDelay();
})
}
</script>
</head>
<body>
<div id="card">
<div id="pan" onclick="copyToClip('pan')">9999<span class='pan-separator'></span>9999<span class='pan-separator'></span>9999<span class='pan-separator'></span>9999</div>
<div id="expiry">
<span id="month" onclick="copyToClip('month')">08</span>
<span id="separator">/</span>
<span id="year" onclick="copyToClip('year')">27</span>
</div>
<div id="cvv" onclick="copyToClip('cvv')">574</div>
<div id="alert" class="empty"></div>
</div>
</body>
</html>
Creating a Request
See below for example implementations for creating an embed request and HMAC.
import base64
import hashlib
import hmac
import json
import requests # pip install requests
def to_json_str(json_object):
return json.dumps(json_object, sort_keys=True, separators=(',', ':'))
def hmac_signature(key, msg):
hmac_buffer = hmac.new(
key=bytes(key, 'utf-8'),
msg=bytes(msg, 'utf-8'),
digestmod=hashlib.sha256
)
return base64.b64encode(hmac_buffer.digest()).decode('utf-8')
def embed_request_query_params(api_key, card_uuid, css_url, account_token, target_origin):
embed_request_json = to_json_str({
# Globally unique identifier for the card to display
"token" : card_uuid,
# Stylesheet URL to style the card element
"css": css_url,
# Only required if one or more end-users have been enrolled
"account_token": account_token,
# Only required if you want to post the element clicked to the parent iframe
"target_origin": target_origin,
})
embed_request = base64.b64encode(bytes(embed_request_json, 'utf-8')).decode('utf-8')
embed_request_hmac = hmac_signature(api_key, embed_request_json)
return {
"embed_request": embed_request,
"hmac": embed_request_hmac,
}
def get_embed_html(api_key, card_uuid, css_url, account_token=None):
url = "https://api.privacy.com/v1/embed/card"
headers = {
"Accept": "text/html",
"Authorization": f"api-key {api_key}",
}
params = embed_request_query_params(api_key, card_uuid, css_url, account_token, target_origin)
response = requests.request("GET", url, params=params, headers=headers)
response.raise_for_status()
return response.text
const crypto = require("crypto");
const https = require("https");
function hmacSignature(key, msg) {
return crypto.createHmac("sha256", key)
.update(msg)
.digest("base64");
}
function embedRequestQuery(apiKey, cardUuid, cssUrl, accountToken, targetOrigin) {
const queryParams = {
"token" : cardUuid,
"css": cssUrl,
"account_token": accountToken, // Only required if one or more end-users have been enrolled
"target_origin": targetOrigin, // only required if you want to postMessage to the parent iframe
}
const embedRequestJson = JSON.stringify(queryParams, Object.keys(queryParams).sort());
const embedRequest = new Buffer.from(embedRequestJson).toString("base64");
const embedRequestHmac = hmacSignature(apiKey, embedRequestJson);
return {
"embed_request": embedRequest,
"hmac": embedRequestHmac
};
}
function getEmbedHtml(apiKey, cardUuid, cssUrl, accountToken = null) {
const params = embedRequestQuery(apiKey, cardUuid, cssUrl, accountToken);
const options = {
headers: {
"Accept": "text/html",
"Authorization": `api-key ${apiKey}`
},
hostname: "api.lithic.com",
method: "GET",
port: 443,
path: "/v1/embed/card?" + new URLSearchParams(params)
};
const req = https.get(options, res => {
console.log(`statusCode: ${res.statusCode}`);
res.on("data", d => {
/* HTML output */
process.stdout.write(d);
})
})
req.on("error", error => {
console.error(error)
})
req.end();
return req;
}
/** Crates:
base64 = "0.13.0"
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
hmac = "0.12.1"
*/
use base64;
use hmac::{Hmac, Mac};
use reqwest;
use serde::Serialize;
use serde_json;
use sha2::Sha256;
use std::env;
fn sort_alphabetically<T: Serialize, S: serde::Serializer>(
value: &T,
serializer: S,
) -> Result<S::Ok, S::Error> {
let value = serde_json::to_value(value).map_err(serde::ser::Error::custom)?;
value.serialize(serializer)
}
struct SortAlphabetically<T: Serialize>( T);
struct EmbedRequestParams<'a> {
token: &'a str,
css: &'a str,
account_token: &'a str,
target_origin: &'a str,
}
fn hmac_signature(key: &str, msg: &str) -> String {
type HmacSha256 = Hmac<Sha256>;
let mut mac = HmacSha256::new_from_slice(key.as_bytes()).unwrap();
mac.update(&msg.as_bytes());
let code_bytes = mac.finalize().into_bytes();
return base64::encode(&code_bytes.to_vec());
}
fn embed_request_query(
api_key: &str,
card_token: &str,
css_url: &str,
account_token: &str,
target_origin: &str,
) -> (String, String) {
let params = EmbedRequestParams {
token: card_token,
css: css_url,
account_token: account_token,
target_origin: target_origin,
};
// Embed request params must be sorted alphabetically
let embed_request_json = serde_json::to_string(&SortAlphabetically(¶ms)).unwrap();
let embed_request = base64::encode(&embed_request_json);
// Generate HMAC digest
let hmac = hmac_signature(&api_key, &embed_request_json);
return (embed_request, hmac);
}
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().collect();
let api_key = &args[1];
let card_token = &args[2];
let css_url = &args[3];
let account_token = &args[4];
let target_origin = &args[5];
let client = reqwest::Client::new();
// Generate query parameters by encoding and signing embed request
let (embed_request, hmac) = embed_request_query(
api_key,
card_token,
css_url,
account_token,
target_origin,
);
// Get HTML
let resp = client
.get("https://api.lithic.com/v1/embed/card")
.query(&[("embed_request", &embed_request), ("hmac", &hmac)])
.header("Authorization", format!("api-key {}", api_key))
.send()
.await?
.text()
.await?;
println!("{}", resp);
Ok(())
}
Styling Your Card
As mentioned above, you can provide your own CSS URL in the request to style your card. Below is an example CSS stylesheet for formatting your card, including a visual indication that copying is happening on-click.
@import url("https://fonts.googleapis.com/css2family=Roboto+Mono:wght@400;600&display=swap");
#card {
border-radius: 16px;
box-shadow: 0 10px 20px rgb(40 51 75 / 20%);
box-sizing: border-box;
background-color: #ff2d36;
/* Add your logo as a background image */
background-image: url("https://lithic.com/sterling/img/logo-black.4793a8fe.svg");
background-repeat: no-repeat;
/* background-position will need adjusting depending on your logo */
background-position: bottom 150px left 190px;
color: #151418;
display: flex;
flex-direction: column;
font-family: "Roboto Mono", sans-serif;
font-size: 16px;
height: 12.61rem;
justify-content: space-between;
line-height: 24px;
margin: 40px auto;
overflow: hidden;
padding: 24px;
position: relative;
user-select: none;
width: 20rem;
}
.pan-separator {
margin: 6px;
}
#pan {
border-radius: 6px;
bottom: 65px;
cursor: pointer;
display: flex;
flex-direction: row;
font-size: 16px;
font-weight: 500;
height: 32px;
justify-content: center;
left: 14px;
letter-spacing: 6px;
line-height: 30px;
padding: 2px 10px 0;
position: absolute;
}
#expiry {
border-radius: 6px;
bottom: 24px;
font-size: 16;
font-weight: 400;
left: 20px;
line-height: 30px;
opacity: 0.8;
padding: 2px 4px 0;
position: absolute;
}
#month, #year {
border-radius: 6px;
cursor: pointer;
line-height: 30px;
padding: 4px 1px;
}
#cvv {
border-radius: 6px;
bottom: 24px;
cursor: pointer;
font-size: 16;
font-weight: 400;
left: 92px;
line-height: 30px;
margin-left: 25px;
opacity: 0.8;
padding: 2px 4px 0;
position: absolute;
}
#expiry::before {
content: 'EXP ';
}
#cvv::before {
content: 'CVV ';
}
#cvv:hover, #pan:hover, #month:hover, #year:hover {
background-color:rgba(0, 0, 0, 0.1);
}
#cvv:active, #pan:active, #month:active, #year:active {
background-color:rgba(0, 0, 0, 0.05);
}
#alert {
display: none;
}

Example card rendering with logo
Putting It All Together
To quickly test your card out locally, you can serve your CSS stylesheet and the generated HTML in a directory.
First, generate HTML of the rendered card and save it in a directory called PrivacyDemo
:
API_KEY = "YOUR_API_KEY"
CARD_TOKEN = "EXAMPLE_CARD_TOKEN"
CSS_URL = "http://localhost:8080/your_card_style.css"
html = get_embed_html(API_KEY, CARD_TOKEN, CSS_URL)
# Write your HTML to a directory called "PrivacyDemo"
with open("path/to/PrivacyDemo/card.html", "w") as f:
f.write(html)
Serve the assets in PrivacyDemo
:
$ cd PrivacyDemo/ # directory containing your stylesheet and card.html
$ python -m http.server 8080 # serve the directory locally on port 8080
Now go to http://localhost:8080/card.html
to view your card.
As mentioned above, in your customer-facing application, you should embed the html as an iframe. Make sure to allow clipboard-write
if you want users to be able to copy the card details.
If you supply target_origin
in the embed request, you can also capture click events in the parent iframe by adding an event listener.
<html>
<head></head>
<body>
<iframe
id="card-iframe"
allow="clipboard-write"
width=600 height=300
src="http://localhost:8080/output.html">
</iframe>
<script>
window.addEventListener("message",
function (e) {
console.log("event", e);
if (e.origin !== 'http://localhost:8080') return;
if (e.data.isCopied === true) {
alert(e.data.copyElt + " copied!");
}
},
false);
</script>
</body>
</html>
Updated about 3 years ago