Embedded Card UI

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.lithic.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.lithic.com/v1/embed/card

Requests

curl https://api.lithic.com/v1/embed/card?embed_request=eyJjc3Mi..&hmac=u...
embed_requestA base64 encoded JSON string of an Embed Request to specify which card to load
hmacSHA2 HMAC of the embed_request JSON string with base64 digest

Embed Request Schema (Issuing)

{
  "token": String,
  "css": String,
  "account_token", String,
  "expiration": String
}
tokenGlobally unique identifier for the card to be displayed
cssA publicly available URI, so the white-labeled card element can be styled with the client's branding
account_tokenOnly needs to be included if one or more end-users have been enrolled
expiration (optional)An ISO 8601 timestamp for when the request should expire.

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 the expiration, 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="https://demo.lithic.com/backend/embedded.css">
<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();
    }

    function copyFailed(divId) {
        var messageDiv = document.getElementById('alert');
        messageDiv.innerText = "error copying " + divId;
        messageDiv.className = "error";
        console.error('Copying to clipboard failed');
        clearAlertDelay();
    }

    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):
    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,
    })

    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 = "http://api.lithic.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)

    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) {
  const queryParams = {
    "token" : cardUuid,
    "css": cssUrl,
    "account_token": null
  }

  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;
}

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:[email protected];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 logoExample card rendering with logo

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 LithicDemo:

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 "LithicDemo"
with open("path/to/LithicDemo/card.html", "w") as f:
    f.write(html)

Serve the assets in LithicDemo:

$ cd LithicDemo/  # 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.

<iframe id="card-iframe"
        src="http://localhost:8080/card.html"
        allow="clipboard-write" class="content">
</iframe>

Did this page help you?