Airwallex logo

Secure iframes

Copy for LLMView as Markdown

Use secure iframes to display or change card details and PINs in a PCI-compliant manner without requiring PCI compliance on your end.

To retrieve or change sensitive card details in a PCI-compliant manner, use secure iframes powered by the PAN delegation feature. This lets you embed card number, expiry, CVV, PIN display, and PIN change functionality directly in your application without requiring PCI compliance on your end.

Normally, retrieving sensitive details of individual cards or cards issued to connected accounts (if you have a Scale implementation) requires PCI compliance on your end when using the Get sensitive card details API API. The PAN delegation feature removes this requirement by handling sensitive data within Airwallex-hosted iframes secured by a short-lived token.

Before you begin

  • Contact your Airwallex Account Manager to enable Issuing APIs, Cards, and PAN delegation for your Airwallex account.
  • Obtain your access token API by authenticating to Airwallex using your unique Client ID and API key. You need the access token to make API calls.

Step 1: Retrieve a token

Use the Create a PAN token API with the card_id parameter (the ID of the individual card or the card issued to the connected account). Note that as a platform account, you must specify the connected account's open ID (starting with the prefix acct_) in the x-on-behalf-of header as you are acting on behalf of the connected account.

A successful response returns a token and its expiration date. The token expires one minute after it is issued. Load the iframe in the next step within this time.

Example request

Shell
1curl -X POST https://api-demo.airwallex.com/api/v1/issuing/pantokens/create \
2 -H 'Authorization: Bearer {{ACCESS_TOKEN}}' \
3 -d '{
4 "card_id": "75c89b87-eb15-453a-85d7-621c104f707d"
5 }'

Example response

JSON
1{
2 "expires_at": "2021-03-22T00:29:34.558+0000",
3 "token": "1HxHDfKJNnITGTULgnNoAADkgE1q+tMecRMVnk0w5LbpuqXeYdytS/EorRNvJZAFftjQCse6+0UPJNe+dzDwfZv+lxuLt06Blc59BSZkwRx1kIMwtkvPmam7PZNf8ZQvOEfTv6+5Cei/jkjUpivhRA4THHc2hX2gpDSIr/mljHhKXMkKxQU+F7USo2x52cNslYhQVl04U5PYdUbnygJnFtmE+Fd3ENy+HHfkErCCTOTcVzwRRA=="
4}

Step 2: Prepare a hash

After retrieving the token, prepare a hash for the iframe. The hash allows you to inject the initial values and options for the iframe. The hash has the following structure:

JavaScript
1const hash = {
2 token: "The token fetched via api for the card",
3 rules: {
4 ".details": { CSSType },
5 ".pin": { CSSType }
6 },
7 langKey: "zh"
8 };
9
10 const hashURI = encodeURIComponent(JSON.stringify(hash));

The hash accepts three fields described in the following sections.

token [Required]

JavaScript
1const res = await get('https://api-demo.airwallex.com/api/v1/issuing/pantokens/create', {
2 card_id
3});
4
5const hash = {
6 token: res.token
7};

rules [Optional]

To provide a consistent, seamless user experience when displaying the card details or card PIN, you can provide rules to style the iframe. Rules are a map of selectors and CSS properties. Both the selectors and the CSS properties are allowlisted.

The following example shows a hash with style configuration.

JavaScript
1const hash = {
2 token: YOUR_TOKEN,
3 rules: {
4 '.pin': {
5 fontSize: '40px',
6 fontWeight: '800',
7 letterSpacing: '2px'
8 }
9 }
10}
11
12const hashURI = encodeURIComponent(JSON.stringify(hash));

The available CSS properties are listed below. For a list of available selectors, see Style iframe.

typescript
1type CSSProperties =
2 | '-moz-osx-font-smoothing'
3 | '-webkit-font-smoothing'
4 | '-webkit-text-fill-color'
5 | 'backgroundColor'
6 | 'border'
7 | 'borderBottom'
8 | 'borderColor'
9 | 'borderLeft'
10 | 'borderRadius'
11 | 'borderRight'
12 | 'borderStyle'
13 | 'borderTop'
14 | 'borderWidth'
15 | 'boxShadow'
16 | 'color'
17 | 'font'
18 | 'fontFamily'
19 | 'fontFeatureSettings'
20 | 'fontSize'
21 | 'fontStyle'
22 | 'fontVariant'
23 | 'fontWeight'
24 | 'letterSpacing'
25 | 'lineHeight'
26 | 'margin'
27 | 'marginBottom'
28 | 'marginLeft'
29 | 'marginRight'
30 | 'marginTop'
31 | 'outline'
32 | 'outlineColor'
33 | 'outlineOffset'
34 | 'outlineStyle'
35 | 'outlineWidth'
36 | 'padding'
37 | 'paddingBottom'
38 | 'paddingLeft'
39 | 'paddingRight'
40 | 'paddingTop'
41 | 'textAlign'
42 | 'textDecoration'
43 | 'textIndent'
44 | 'textJustify'
45 | 'textShadow'
46 | 'textTransform'
47 | 'transition'
48 | 'wordBreak'
49 | 'wordSpacing'
50 | 'wordWrap';

langKey [Optional]

Use langKey to define the language of the iframe. When not provided or if the specified language is unavailable, it defaults to en. The following langKey values are supported:

ValueLanguage
enEnglish
en-USEnglish (US)
zhChinese (Simplified)
zh-HantChinese (Traditional)
frFrench
deGerman
esSpanish
es-419Spanish (Latin America)
es-MXSpanish (Mexico)
jaJapanese
koKorean
JavaScript
1const hash = {
2 ...
3 langKey: 'en',
4};

The card PIN iframe does not accept langKey as it only displays numbers. The change PIN iframe accepts langKey.

Step 3: Embed iframe on your page

You can embed three types of iframes on your page:

  • The card details iframe displays the card number, expiry, and CVV.
  • The card PIN iframe displays the card PIN.
  • The change PIN iframe lets cardholders securely change their card PIN.

Display card details iframe

The card details iframe can be displayed using the following endpoint.

https://airwallex.com/issuing/pci/v2/:cardId/details#:hash

JavaScript
1const res = await get('https://api-demo.airwallex.com/v1/issuing/pantokens/create', {
2 card_id
3});
4
5const hash = {
6 token: res.token
7};
8
9const hashURI = encodeURIComponent(JSON.stringify(hash));
10
11return
12 <iframe src={`https://airwallex.com/issuing/pci/v2/${cardId}/details#${hashURI}`}/>

Display card PIN iframe

The card PIN iframe can be displayed using the following endpoint.

https://airwallex.com/issuing/pci/v2/:cardId/pin/#:hash

JavaScript
1const hashURI = encodeURIComponent(JSON.stringify(hash));
2
3return
4 <iframe src={`https://airwallex.com/issuing/pci/v2/${cardId}/pin#${hashURI}`}/>

Display change PIN iframe

The change PIN iframe lets cardholders enter and confirm a new PIN securely. Use the following endpoint to display it.

https://airwallex.com/issuing/pci/v2/:cardId/change-pin#:hash

JavaScript
1const hashURI = encodeURIComponent(JSON.stringify(hash));
2
3return
4 <iframe src={`https://airwallex.com/issuing/pci/v2/${cardId}/change-pin#${hashURI}`}/>

Not all cards are eligible for PIN change. The iframe automatically checks eligibility when it loads and displays an error message to the cardholder if the card is ineligible. A card is eligible for PIN change when all of the following conditions are met:

  • The card has is_personalised set to true. Cards with is_personalised set to false are not eligible.
  • The card status is Active or Inactive.
  • The card's issuing region supports PIN change. The following table shows which regions support PIN change:
RegionPIN change supported
Australia (AU)Yes
Hong Kong (HK)Yes
Singapore (SG)Yes
United States (US)Yes
Europe (EEA)No
United Kingdom (UK)No
Canada (CA)No
Israel (IL)No

The cardholder's new PIN must meet the following requirements:

  • Exactly 4 digits.
  • Numeric only (0–9).
  • No sequential digits — for example, 1234 and 4321 are rejected.
  • No 3 or more repeated digits — for example, 1112 and 1111 are rejected, but 1123 is allowed.
  • The new PIN and confirmation PIN must match.

Step 4: Style iframe

You can style the iframe by providing rules when preparing the hash.

Style card details iframe

The available selectors for the card details iframe are listed below:

  • .details
  • .details__row
  • .details__row--card-number
  • .details__row--expiry-date
  • .details__row--security-code
  • .details__content
  • .details__label
  • .details__value
  • .details__tooltip
  • .details__button
  • .details__button:hover
  • .details__button:active
  • .details__button:focus
  • .details__button svg

Example code

JavaScript
1const hash = {
2 token: token,
3 rules: {
4 '.details': {
5 backgroundColor: '#2a2a2a',
6 color: 'white',
7 borderRadius: '20px',
8 fontFamily: 'Arial'
9 },
10 '.details__row': {
11 display: 'flex',
12 justifyContent: 'space-between',
13 padding: '20px'
14 },
15 '.details__label': {
16 width: '100px',
17 fontWeight: 'bold'
18 },
19 '.details__content': { display: 'flex' },
20 '.details__button svg': { color: 'white' }
21 }
22};

Styled card details iframe

The example code produces the styling shown below.

Before styling vs after styling

Before vs After card iframe styling

Style card PIN iframe

The available selectors for the card PIN iframe are:

  • .pin
  • .pin__value

Example code, PIN iframe

JavaScript
1const hash = {
2 token: token,
3 rules: {
4 '.pin': {
5 backgroundColor: '#2a2a2a',
6 color: 'white',
7 borderRadius: '12px',
8 fontFamily: 'Arial',
9 padding: '20px'
10 },
11 '.pin_value': {
12 fontSize: '24px',
13 fontWeight: 'bold',
14 letterSpacing: '4px'
15 }
16 }
17};

Style change PIN iframe

The available selectors for the change PIN iframe are:

  • .change-pin
  • .change-pin__label
  • .change-pin__input
  • .change-pin__input:focus
  • .change-pin__error
  • .change-pin__button--submit
  • .change-pin__button--submit:hover
  • .change-pin__button--cancel
  • .change-pin__button--cancel:hover
  • body

Example code, change PIN iframe

JavaScript
1const hash = {
2 token: token,
3 rules: {
4 '.change-pin': {
5 backgroundColor: '#2a2a2a',
6 color: 'white',
7 borderRadius: '12px',
8 fontFamily: 'Arial',
9 padding: '20px'
10 },
11 '.change-pin__input': {
12 borderRadius: '4px',
13 fontSize: '16px',
14 padding: '8px'
15 },
16 '.change-pin__button--submit': {
17 backgroundColor: '#0060ff',
18 color: 'white',
19 borderRadius: '4px'
20 }
21 }
22};

Step 5: Handle iframe event lifecycle

The iframe uses the postMessage API to inform the parent page about its lifecycle including load status or errors. Possible event types for the card details iframe are listed below.

JavaScript
1// iframe has completed its first render
2{
3 type: `${cardId}:details:mounted`
4}
JavaScript
1// iframe is loading details
2{
3 type: `${cardId}:details:loading`
4}
JavaScript
1// iframe has loaded details
2{
3 type: `${cardId}:details:loaded`
4}
JavaScript
1// iframe has encountered an error while parsing hash or loading
2{
3 type: `${cardId}:details:error`;
4 error: 'cannot_retrieve_details' | 'unable_to_parse_hash' | 'unknown_hash_format' | 'invalid_hash_format';
5}
JavaScript
1// iframe has resized and is returning its new height
2{
3 type: `${cardId}:details:resize`;
4 height: number;
5};

The card PIN iframe has the same events except details is replaced with pin.

JavaScript
1// Card details iframe event
2{
3 type: `${cardId}:details:error`;
4 error: 'cannot_retrieve_details' | 'unable_to_parse_hash' | 'unknown_hash_format' | 'invalid_hash_format';
5}
JavaScript
1// Card PIN iframe event
2{
3 type: `${cardId}:pin:error`;
4 error: 'cannot_retrieve_pin' | 'unable_to_parse_hash' | 'unknown_hash_format' | 'invalid_hash_format';
5}

The change PIN iframe uses a changePin: event prefix and has its own set of events:

JavaScript
1// Change PIN iframe has loaded
2{
3 type: `${cardId}:changePin:mounted`
4}
JavaScript
1// Change PIN iframe content height changed
2{
3 type: `${cardId}:changePin:resize`;
4 height: number;
5};
JavaScript
1// PIN change is being processed
2{
3 type: `${cardId}:changePin:submitting`
4}
JavaScript
1// Cardholder successfully changed their PIN
2{
3 type: `${cardId}:changePin:success`
4}
JavaScript
1// Change PIN iframe encountered an error
2{
3 type: `${cardId}:changePin:error`;
4 errorCode: 'cannot_change_pin' | 'change_pin_not_allowed' | 'invalid_pin_consecutive_digits' | 'invalid_pin_repeated_digits' | 'unable_to_parse_hash' | 'unknown_hash_format' | 'invalid_hash_format';
5}
JavaScript
1// Cardholder clicked Cancel
2{
3 type: `${cardId}:changePin:cancel`
4}

The following table describes each change PIN error code:

Error codeDescription
cannot_change_pinAn internal error occurred. Retry after a short delay and contact support if the issue persists.
change_pin_not_allowedThe card is not eligible for PIN change. Inform the cardholder that PIN change is unavailable for this card.
invalid_pin_consecutive_digitsThe PIN contains sequential digits.
invalid_pin_repeated_digitsThe PIN contains 3 or more repeated digits.
unable_to_parse_hashThe hash in the URL is not valid JSON. Ensure valid JSON and proper URL encoding.
unknown_hash_formatThe hash is missing the token field.
invalid_hash_formatThe hash contains unknown keys or invalid CSS properties. Only token, langKey, and rules are accepted.

Full example code (React)

JavaScript
1export const IframeDetails = ({ cardId }) => {
2 const [error, setError] = useState(false);
3 const [loaded, setLoaded] = useState(false);
4 const [height, setHeight] = useState(0);
5
6 function handleMessage(event) {
7 if (!event.origin.includes('airwallex.com')) return;
8 if (event?.data.type === `${cardId}:details:loaded`) setLoaded(true);
9 if (event?.data.type === `${cardId}:details:error`) setError(true);
10 if (event?.data.type === `${cardId}:details:resize`) setHeight(event?.data.height);
11 }
12
13 useEffect(() => {
14 window.addEventListener('message', handleMessage);
15 return () => window.removeEventListener('message', handleMessage);
16 }, []);
17
18 const hash = {
19 token: getAuthToken(),
20 langKey: 'en',
21 rules: {
22 '.details': {
23 font: '14px Arial, sans-serif',
24 margin: '0',
25 },
26 },
27 };
28 const hashURI = encodeURIComponent(JSON.stringify(hash));
29
30 return (
31 <>
32 {!loaded && <p>loading...</p>}
33 {error && <p>Oops something went wrong</p>}
34 <iframe
35 style={{ height, display: loaded ? 'block' : 'none' }}
36 frameBorder="0"
37 src={`https://airwallex.com/issuing/pci/v2/${cardId}/details#${hashURI}`}
38 />
39 </>
40 );
41}
Was this page helpful?