Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.seismic-cards.systems/llms.txt

Use this file to discover all available pages before exploring further.

Seismic Widget.js renders a user’s full card number, expiry date, and CVV inside iframes hosted on Seismic’s PCI-certified domain. Your servers and your front-end code never touch raw card data, which keeps your application out of PCI scope. You mint a short-lived token on your server, pass it to the widget on the client, and Seismic handles everything else.

How it works

Three things happen in three places:
  1. Your server mints a 5-minute access token by calling GET /v1/cards/{cardId}/private-info/access-token and sends it to your client.
  2. Your client loads Seismic Widget.js and calls widget.bootstrap() with the token and three <div> IDs.
  3. Your dashboard allowlists every hostname that will load the widget — iframes silently stay blank otherwise.

1

Mint a client access token

Call this endpoint from your server — never from the browser — every time a user opens the “Show card details” view.
GET /v1/cards/{cardId}/private-info/access-token?accountId={accountId}
Host: sandbox-api.seismic.systems
x-access-token: <your access token>
Path parameters
cardId
string
required
UUID of the card whose details you want to display.
Query parameters
accountId
string
required
UUID of the sub-account that owns the card.
Response (200)
{
  "code": "000000",
  "data": { "accessToken": "eyJhbGciOiJIUzI1NiJ9.eyJjYXJkSWQiOi..." }
}
data.accessToken
string
A JWT scoped to one card and one user, valid for approximately 5 minutes. Send it to your client immediately and discard it — do not store or cache it.
Do not cache the token. Mint a fresh one every time the user opens the card-details view. Cached tokens will expire and cause onFailure to fire with token expired.
2

Load the widget in your client

Add three empty <div> elements as targets, load Seismic Widget.js, then call widget.bootstrap() with the token you received from your server.
<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <title>My card</title>
  <style>
    .row   { margin: 16px 0; }
    .label { font-size: 12px; opacity: 0.6; margin-bottom: 4px; }
    .pane  { min-height: 44px; padding: 10px; border: 1px solid #ccc; border-radius: 8px; }
  </style>
</head>
<body>
  <div class="row"><div class="label">Card number</div><div id="card-pan" class="pane"></div></div>
  <div class="row"><div class="label">Expires</div>     <div id="card-exp" class="pane"></div></div>
  <div class="row"><div class="label">CVV</div>         <div id="card-cvv" class="pane"></div></div>

  <script src="https://widget.seismic.systems/index.min.js"></script>
  <script>
    fetch("/api/cards/CARD_UUID/access-token", { credentials: "include" })
      .then((r) => r.json())
      .then(({ data }) => {
        widget.bootstrap({
          clientAccessToken: data.accessToken,
          component: {
            showPan: {
              cardPan: { domId: "card-pan", format: true, styles: { span: { color: "#111", "font-size": "17px" } } },
              cardExp: { domId: "card-exp", format: true, styles: { span: { color: "#111", "font-size": "17px" } } },
              cardCvv: { domId: "card-cvv",                  styles: { span: { color: "#111", "font-size": "17px" } } }
            }
          },
          callbackEvents: {
            onSuccess: () => console.log("Widget rendered"),
            onFailure: (err) => console.error("Widget failed", err)
          }
        });
      });
  </script>
</body>
</html>
3

Allowlist your domains

In the Seismic dashboard, go to Widget → Allowed Domains and add every hostname that will load the widget:
app.your-company.com
api.your-company.com
sandbox.your-company.com
localhost:3000
Enter the hostname only — no scheme (https://) and no path. If a host is not on the allowlist, the iframes load but stay blank with no error in the browser console.
localhost:3000 is valid for sandbox environments. Remove it before going to production.

Server-rendered widget page

For mobile WebViews, the recommended approach is to have your server compose the widget HTML — embedding the access token at render time — and serve the whole page. The client just loads the URL.
// Express / Fastify / any Node server
import axios from "axios";

async function pciWidgetHandler(req, res) {
  const { cardId } = req.params;
  const userId = req.user.id;                          // your auth
  const accountId = await getAccountIdForUser(userId); // your DB

  // 1. Mint a 5-minute access token from Seismic
  const access = await axios.get(
    `${SEISMIC}/v1/cards/${cardId}/private-info/access-token`,
    { params: { accountId }, headers: { "x-access-token": SEISMIC_TOKEN } },
  );
  const cardJwt = access.data.data.accessToken;

  // 2. Return an HTML page that bootstraps Seismic Widget.js
  res.type("text/html").send(`<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
  <title>Card</title>
  <style>
    body  { font-family: -apple-system, BlinkMacSystemFont, sans-serif; margin: 24px; color: #111; }
    .row   { margin-bottom: 16px; }
    .label { font-size: 11px; opacity: .55; text-transform: uppercase; margin-bottom: 4px; }
    .pane  { min-height: 44px; padding: 10px 12px; border: 1px solid rgba(0,0,0,.1); border-radius: 10px; background: #f7f7fa; }
  </style>
</head>
<body>
  <div class="row"><div class="label">Card number</div><div id="card-pan" class="pane"></div></div>
  <div class="row"><div class="label">Expires</div>     <div id="card-exp" class="pane"></div></div>
  <div class="row"><div class="label">CVV</div>         <div id="card-cvv" class="pane"></div></div>

  <script src="https://widget.seismic.systems/index.min.js"></script>
  <script>
    var token = ${JSON.stringify(cardJwt)};
    function boot() {
      widget.bootstrap({
        clientAccessToken: token,
        component: {
          showPan: {
            cardPan: { domId: "card-pan", format: true, styles: { span: { "font-size": "17px", color: "#111" } } },
            cardExp: { domId: "card-exp", format: true, styles: { span: { "font-size": "17px", color: "#111" } } },
            cardCvv: { domId: "card-cvv",                  styles: { span: { "font-size": "17px", color: "#111" } } }
          }
        },
        callbackEvents: {
          onFailure: function (err) { console.error(err); }
        }
      });
    }
    if (document.readyState === "complete") boot();
    else window.addEventListener("load", boot);
  </script>
</body>
</html>`);
}

Customizing the look

The widget exposes a single CSS selector per field — span — that you can theme through the styles config object. Fonts, colors, letter spacing, and weight all work.
widget.bootstrap({
  clientAccessToken: token,
  component: {
    showPan: {
      cardPan: {
        domId: "card-pan",
        format: true,
        styles: {
          span: {
            color: "#ffffff",
            "font-size": "20px",
            "font-weight": "700",
            "letter-spacing": "0.06em",
            "font-family": "-apple-system, sans-serif"
          }
        }
      },
      cardExp: { domId: "card-exp", format: true, styles: { span: { color: "#ffffff", "font-size": "16px" } } },
      cardCvv: { domId: "card-cvv",                  styles: { span: { color: "#ffffff", "font-size": "16px" } } }
    }
  }
});
The iframes inherit the dimensions of their target <div>. Give the <div> elements a non-zero width and height before calling bootstrap() — the widget refuses to render into a 0×0 element. A min-height: 44px on each pane is sufficient for all browsers.

Troubleshooting

The host loading the widget is not on the allowlist. Go to Dashboard → Widget → Allowed Domains and add the exact hostname — no scheme, no path. For example, app.your-company.com, not https://app.your-company.com/cards.
The Seismic Widget.js script has not finished loading, or the URL is incorrect. Open the network tab in browser DevTools and confirm the request to https://widget.seismic.systems/index.min.js returns a 200. Also make sure your Content-Security-Policy allows script-src widget.seismic.systems.
The access token you minted is older than approximately 5 minutes. Do not cache it. Mint a fresh token from your server every time the user opens the card-details view.
The parent <div> was measured at 0×0 when bootstrap() was called — typically because the layout had not yet settled. Fix options: set an explicit min-height on the <div> before bootstrapping, or delay the call using two requestAnimationFrame callbacks to ensure the layout has painted.
bootstrap() was called again during a flip or transition animation. Call widget.destroy() first, wait for the animation to finish, then call widget.bootstrap() once the panes have stable dimensions.
The script URL is blocked by a Content Security Policy on your page. Allow widget.seismic.systems in both script-src and frame-src directives of your Content-Security-Policy header.

PCI scope

Because all card data lives inside iframes served from widget.seismic.systems, your application:
  • Qualifies for PCI DSS SAQ A — the lightest assessment tier, approximately 22 controls versus 200+ for full scope.
  • Never logs, stores, transmits, or processes raw PAN, CVV, or track data.
  • Does not need a tokenization vault — Seismic is the vault.
For a copy of Seismic’s PCI DSS Attestation of Compliance, contact your account manager.