Secure iframes
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
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
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:
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 };910 const hashURI = encodeURIComponent(JSON.stringify(hash));
The hash accepts three fields described in the following sections.
token [Required]
1const res = await get('https://api-demo.airwallex.com/api/v1/issuing/pantokens/create', {2 card_id3});45const hash = {6 token: res.token7};
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.
1const hash = {2 token: YOUR_TOKEN,3 rules: {4 '.pin': {5 fontSize: '40px',6 fontWeight: '800',7 letterSpacing: '2px'8 }9 }10}1112const hashURI = encodeURIComponent(JSON.stringify(hash));
The available CSS properties are listed below. For a list of available selectors, see Style iframe.
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:
| Value | Language |
|---|---|
en | English |
en-US | English (US) |
zh | Chinese (Simplified) |
zh-Hant | Chinese (Traditional) |
fr | French |
de | German |
es | Spanish |
es-419 | Spanish (Latin America) |
es-MX | Spanish (Mexico) |
ja | Japanese |
ko | Korean |
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
1const res = await get('https://api-demo.airwallex.com/v1/issuing/pantokens/create', {2 card_id3});45const hash = {6 token: res.token7};89const hashURI = encodeURIComponent(JSON.stringify(hash));1011return12 <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
1const hashURI = encodeURIComponent(JSON.stringify(hash));23return4 <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
1const hashURI = encodeURIComponent(JSON.stringify(hash));23return4 <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_personalisedset totrue. Cards withis_personalisedset tofalseare 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:
| Region | PIN 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
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

Style card PIN iframe
The available selectors for the card PIN iframe are:
.pin.pin__value
Example code, PIN iframe
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:hoverbody
Example code, change PIN iframe
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.
1// iframe has completed its first render2{3 type: `${cardId}:details:mounted`4}
1// iframe is loading details2{3 type: `${cardId}:details:loading`4}
1// iframe has loaded details2{3 type: `${cardId}:details:loaded`4}
1// iframe has encountered an error while parsing hash or loading2{3 type: `${cardId}:details:error`;4 error: 'cannot_retrieve_details' | 'unable_to_parse_hash' | 'unknown_hash_format' | 'invalid_hash_format';5}
1// iframe has resized and is returning its new height2{3 type: `${cardId}:details:resize`;4 height: number;5};
The card PIN iframe has the same events except details is replaced with pin.
1// Card details iframe event2{3 type: `${cardId}:details:error`;4 error: 'cannot_retrieve_details' | 'unable_to_parse_hash' | 'unknown_hash_format' | 'invalid_hash_format';5}
1// Card PIN iframe event2{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:
1// Change PIN iframe has loaded2{3 type: `${cardId}:changePin:mounted`4}
1// Change PIN iframe content height changed2{3 type: `${cardId}:changePin:resize`;4 height: number;5};
1// PIN change is being processed2{3 type: `${cardId}:changePin:submitting`4}
1// Cardholder successfully changed their PIN2{3 type: `${cardId}:changePin:success`4}
1// Change PIN iframe encountered an error2{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}
1// Cardholder clicked Cancel2{3 type: `${cardId}:changePin:cancel`4}
The following table describes each change PIN error code:
| Error code | Description |
|---|---|
cannot_change_pin | An internal error occurred. Retry after a short delay and contact support if the issue persists. |
change_pin_not_allowed | The card is not eligible for PIN change. Inform the cardholder that PIN change is unavailable for this card. |
invalid_pin_consecutive_digits | The PIN contains sequential digits. |
invalid_pin_repeated_digits | The PIN contains 3 or more repeated digits. |
unable_to_parse_hash | The hash in the URL is not valid JSON. Ensure valid JSON and proper URL encoding. |
unknown_hash_format | The hash is missing the token field. |
invalid_hash_format | The hash contains unknown keys or invalid CSS properties. Only token, langKey, and rules are accepted. |
Full example code (React)
1export const IframeDetails = ({ cardId }) => {2 const [error, setError] = useState(false);3 const [loaded, setLoaded] = useState(false);4 const [height, setHeight] = useState(0);56 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 }1213 useEffect(() => {14 window.addEventListener('message', handleMessage);15 return () => window.removeEventListener('message', handleMessage);16 }, []);1718 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));2930 return (31 <>32 {!loaded && <p>loading...</p>}33 {error && <p>Oops something went wrong</p>}34 <iframe35 style={{ height, display: loaded ? 'block' : 'none' }}36 frameBorder="0"37 src={`https://airwallex.com/issuing/pci/v2/${cardId}/details#${hashURI}`}38 />39 </>40 );41}