DEV Community

Payneteasy
Payneteasy

Posted on Originally published at payneteasy.com

Hosted Fields: keep your own checkout and keep raw card data out of your stack

You spent real time on your checkout. Field order, copy, validation, light and dark themes, all tuned to how people actually pay. Then PCI DSS shows up: the moment your page touches a raw card number, that page, its JavaScript and your backend become part of the scope you have to manage and evidence.

The usual way out is a hosted payment page. You redirect the payer to a page you do not control, and the checkout you designed disappears at the most sensitive step.

Hosted Fields is the third option: you keep your checkout, and your page code and backend never receive the raw card data. Here is how it works under the hood, what the integration looks like, and where the limits are.

Why a raw card input pulls so much into scope

A plain <input name="card_number"> looks harmless, but think about everything that can read it:

  • Your page JavaScript. Any script on the page can read the value of an input in the same origin.
  • Third-party scripts. Analytics, tag managers and chat widgets run with the same access to the inputs as your own code. You do not control their release cycle.
  • Your backend. The card number travels in a request to your server, so it passes through your web server, application code and anything that sits in between.
  • Your logs. Request logs, error trackers and APM tools tend to capture more than you expect.

Each of these is a place where card data can be stored, leaked or misconfigured, and each one is part of what you have to defend.

How Hosted Fields isolates the card

Hosted Fields lets you build the payment form on your own site and drop the sensitive inputs into it as fields served by Payneteasy. Three fields, card number, expiry date and CVV, are each loaded as a separate cross-origin iframe from the Payneteasy domain. Everything around them (name, email, cardholder name, headings, the "Pay" button) stays yours.

To the payer it looks like one form. Technically, the card fields live in Payneteasy-origin iframes that your page code cannot read. The card number, expiry and CVV never enter your page's DOM or JavaScript, the requests to your server, or your logs. That isolation also keeps card data away from the third-party scripts on your page, which on a regular form have the same access to the inputs as your own code.

You still control the look. Placement, spacing, borders, backgrounds and field containers are plain CSS on your side, and the inputs inside the iframes are styled through the SDK. Here is the same checkout in a light and a dark theme, with the card number, MM/YY and CVV fields restyled to match the rest of the form:

Hosted Fields checkout in light theme

Hosted Fields checkout in dark theme

No redirect at the moment the card is entered, no unfamiliar page, no lost context.

The flow: ephemeralTicket, hostedFieldsToken, Sale

Two short-lived objects do the work, and they do different jobs:

  1. Your server requests a single-use ephemeralTicket from Payneteasy and places it on the checkout page. The ticket lives for 15 minutes and authorises exactly one tokenization attempt. Your private RSA signing key stays on your server and never reaches the browser. Only the ticket does.
  2. The payer types the card details into the hosted fields on your page.
  3. On "Pay", the SDK exchanges those details for a hostedFieldsToken: a single-use token, valid for 5 minutes, that stands in for the card details in the next request. Your page code only ever sees this token.
  4. Your server sends the hostedFieldsToken to the Sale (or Preauth) API in place of the card parameters.

In short: the ticket authorises one tokenization attempt and is safe to place on the page. The token represents the card in the following Sale request, is accepted once, and the card cannot be reconstructed from it.

On the payment API side the change is small. Four card parameters (credit_card_number, expire_month, expire_year, cvv2) are replaced by one hosted_fields_token. The other Sale parameters stay the same. The token is accepted by Sale and Preauth and, for deposit-to-card transfers, for the receiving card.

Front-end integration

The basic setup is one script tag and one SDK initialization. Add the script (use the sandbox or production host from the integration guide):

<script src="https://GATEWAY_HOST/assets/libs/hosted-fields/latest/index.js"></script>
Enter fullscreen mode Exit fullscreen mode

Then initialize the SDK:

const sdk = HostedFields.init({
  endpointId: ENDPOINT_ID,
  fields: {
    cardNumber: { type: 'pan', placeholder: '1234 1234 1234 1234' },
    expiryDate: { type: 'exp' },
    cvv:        { type: 'cvv' }
  },
  onReady: () => { payButton.disabled = false; },
  onToken: token => sendToYourServer({ hosted_fields_token: token }),
  onError: error => showError(error.payerMessage)
});

payButton.addEventListener('click', () => sdk.tokenize(EPHEMERAL_TICKET));
Enter fullscreen mode Exit fullscreen mode

cardNumber, expiryDate and cvv are the ids of empty <div> elements in your markup. The SDK creates a Payneteasy-origin iframe inside each one.

Reacting to what happens inside the iframe

You cannot read the input, but you can still react to its state. The SDK toggles three classes on the field containers: hf-field--focus, hf-field--filled and hf-field--error. Your CSS does the rest:

.card-field { border: 1px solid #c8ccd4; border-radius: 6px; }
.card-field.hf-field--focus  { border-color: #3b6df0; }
.card-field.hf-field--filled { background: #f6f8fb; }
.card-field.hf-field--error  { border-color: #d64545; }
Enter fullscreen mode Exit fullscreen mode

(.card-field stands for whatever class you put on your own containers.)

The server-side step

This part is pseudocode. Request signing and exact parameters are in the docs and in the reference examples, so treat this as the shape of the flow rather than copy-paste code.

# 1. When rendering the checkout page
ticket = request_ephemeral_ticket(signed with your RSA key)   # 15 min, one tokenization
render_page(EPHEMERAL_TICKET = ticket)                        # key itself never leaves the server

# 2. When the browser posts the token back
token = request.body.hosted_fields_token                      # single use, 5 min
sale(order_params
     + hosted_fields_token = token)                           # instead of the 4 card params
Enter fullscreen mode Exit fullscreen mode

Two details matter here. The signing key belongs to the server and only the ticket goes to the browser. And because the ticket authorises one attempt and the token is accepted once, nothing long-lived ever sits on the page.

Do not self-host the SDK

Load the SDK with a plain <script> tag straight from the Payneteasy domain. It is not an npm package, and it must not be self-hosted. A copy served from your site would create the fields on your own origin, which is the exact thing Hosted Fields exists to avoid, so init refuses to start. If you were planning to bundle it with your build, this is the one place to stop.

Content Security Policy

If your page sets a CSP, allow the Payneteasy domain in two directives: script-src for the SDK and frame-src for the card field iframes. If the script loads but the fields do not appear, check the browser console for CSP violations first.

What about 3-D Secure?

Hosted Fields changes how the card is collected, not how the payment is authenticated. When the issuer requires a 3-D Secure challenge, the order status response carries it as a URL or as ready-to-render HTML, and you decide how to show it: render it in an iframe inside your checkout so the payer never leaves your page, or redirect to it and return. Authentication itself runs through Payneteasy's 3DS Adapter, exactly as in a server-to-server integration.

What this does and does not do for PCI DSS

Raw card details are captured by Payneteasy, not by your page, your server calls or your logs. Payneteasy handles them inside its PCI DSS Level 1 Service Provider environment, while your systems work only with the token. That isolation reduces the PCI DSS scope you have to defend and evidence.

It is worth being precise about the claim:

Hosted Fields reduces PCI DSS scope; it does not remove PCI DSS obligations altogether. The SAQ and controls applicable to a particular merchant depend on the overall integration and should be confirmed with the merchant's acquirer or QSA.

Do not tell your auditor "we are out of scope" because you added an iframe. Ask your acquirer or QSA which SAQ applies to your whole integration.

Notes for PSPs and white-label partners

If you run a white-label installation of the Payneteasy platform, Hosted Fields is available to your merchants on the same terms. The SDK and the card fields are served under your installation's own domain, and every ticket and token is bound to that installation: a ticket issued on one installation is rejected on another. Your merchants get the same integration guide and reference examples.

Summary of the trade-offs

  • Checkout stays yours. Payers stay on your page, in your design, at the moment they enter the card.
  • Card data stays out of your systems. Number, expiry and CVV never reach your page, servers or logs.
  • Small API change. Four card parameters become one hosted_fields_token.
  • Nothing long-lived on the page. A ticket lives 15 minutes and admits one tokenization. A token lives 5 minutes and is accepted once.

Get started

Hosted Fields is available in both sandbox and production. Ask your account manager to enable it for your endpoints, then start here:

The full write-up, including the payment flow diagram, is on the Payneteasy blog: Hosted Fields: keep your checkout, keep card data out of your systems.

If you have already put card fields in iframes on your own checkout, what was the part that took you longest to get right?

Top comments (0)