Use the hosted PIN-entry iframe
1
Configure your platform
Set Use the canonical HTTPS origin of the page that hosts the iframe: scheme, host, and optional port, without a path, trailing slash, credentials, query, or fragment. One origin covers all user-specific paths on that website. Omit this setting in later configuration updates to preserve it, or set it to
cardConfigs.pinTargetOrigin once through PATCH /config:null to clear it. Changes apply to newly requested URLs.2
Get a PIN-entry URL
Call
GET /cards/{id}/pin-entry-url from your backend immediately before displaying the form. Send no request body or origin parameter; Grid uses your platform’s configured origin. If the origin is unset, the request returns 409 CONFLICT.Grid returns iframeUrl, sessionToken, expiresAt, and environment. The URL already contains the temporary token and selects the appropriate environment.To customize the PIN input, pass the optional styles query parameter described below.Each request creates a fresh, temporary PIN-entry session and returns Cache-Control: no-store. Requesting a URL does not set or change the PIN.3
Load the form
Before setting the iframe’s
src, register a message listener on your page. Only accept messages whose origin equals new URL(iframeUrl).origin and whose source equals your iframe’s contentWindow; also require data.embedType to equal PIN_SETTING.Set the iframe’s src to iframeUrl exactly as returned. Wait for data.messageType to be Embed:Rendered before enabling your submit button. The cardholder enters a four-digit PIN directly into this secure form, so the plaintext PIN never reaches your servers or Grid’s.4
Submit the PIN
When the cardholder clicks your submit button, send
{ messageType: "Embed:SubmitPin", embedType: "PIN_SETTING" } to the iframe’s contentWindow using postMessage. Set the target origin to new URL(iframeUrl).origin; never use *. Disable the button while submission is in progress.5
Confirm the result
Listen for
Embed:PinSubmissionStatusChanged with the following data.status values:Remove your event listener when you remove the iframe. If you stop waiting before a result arrives, treat the outcome as unknown: do not report success or automatically resubmit. An
OK status alone cannot prove that an already configured PIN was changed.expiresAt. Request a new URL for another PIN change or after expiration. Treat both the URL and token as secrets: never persist, cache, log, or send them to analytics.
Customize the PIN input
Pass CSS declarations as a JSON string in thestyles query parameter of GET /cards/{id}/pin-entry-url. Use CSS property names such as font-size, with string values. You can include nested declarations for focus, hover, and other supported pseudo-classes.
For example, request a PIN input with your colors, font size, and border radius:
iframeUrl, so you still set your iframe’s src to that URL unchanged. Styles apply only to that request. Omit styles to use the default appearance.
Supported pseudo-classes are :first-child, :last-child, :enabled, :read-write, :read-only, :placeholder-shown, :valid, :invalid, :disabled, :hover, :focus-within, :focus, :focus-visible, and :active. Each pseudo-class contains CSS declarations with string values; arbitrary selectors and further nesting are not supported.
Styles that load external resources, including external fonts and images, are not supported. The JSON string is limited to 3072 characters. Its compact UTF-8 representation must fit within 4096 characters after base64 encoding; non-ASCII characters can consume more space. Invalid JSON, an invalid structure, or oversized styles return 400 INVALID_INPUT.
You control the surrounding layout and submit button on your page. Styling the outer iframe controls its size and border; use styles to customize the PIN input inside it.
Encrypt a PIN in your own UI
If you collect the PIN in your own UI, you must encrypt it in the customer’s browser or mobile app before sending it to your server or Grid. Download the PIN encryption public key. This PEM-encoded RSA public key is the same for sandbox and production; save it aspin-encryption-public-key.pem in your client integration.
- Collect a four-digit PIN as a string so leading zeros are preserved.
- Generate a fresh, cryptographically random integer
noncefor each request. - Serialize an object containing
nonceandpinas JSON. Encode the JSON as UTF-8. - Encrypt those bytes using the PIN encryption public key, then base64-encode the ciphertext.
- Send the base64 string as
encryptedPinBlockinPOST /cards/{id}/set-pin.
0123 produces JSON with this shape before encryption:
400 INVALID_INPUT. Check the payload, encoding, and public key, then create a new encrypted block with a fresh nonce. A 204 response means the PIN change was accepted; read pinStatus from GET /cards/{id} to check its resulting state.
Check and recover a PIN
ReadpinStatus from GET /cards/{id}:
Three consecutive incorrect attempts block an online PIN. Call
POST /cards/{id}/pin/unblock to keep the same PIN, or use either PIN-entry flow to choose a new one. Unblocking a PIN that is already OK is safe; a card with no PIN returns 409 CONFLICT.
Only request an unblock after authenticating the cardholder and receiving their explicit request. Do not automatically unblock after a declined transaction.
Each card allows one unblock attempt per rolling seven days.
If the PIN blocks again before that window ends, the endpoint returns 409 PIN_CHANGE_REQUIRED. Use either PIN-entry flow to change the PIN and recover immediately, or wait until details.pinUnblockAvailableAt. Changing the PIN does not reset the unblock allowance. You can also read pinUnblockAvailableAt from the Card resource; it is absent until an attempt consumes the allowance.
There is no PIN-reveal API. If the cardholder forgets the PIN, let them set a new one. Setting a PIN does not activate a closed or frozen card or change its spending limits.
Card.pinStatus is the last known PIN status, populated when a card with PIN management is created. An absent value means PIN management is unavailable for this card; it does not mean NOT_SET or tell you whether a payment requires a PIN.
Receive PIN status changes
HandleCARD.PIN_STATUS_CHANGE at your webhook endpoint to keep your app’s PIN status up to date. The event contains the updated Card resource in data, including pinStatus.
NOT_SET → OK: a PIN was configured.OK → BLOCKED: the PIN became blocked.BLOCKED → OK: the PIN was unblocked or replaced.
OK do not trigger it. Use the iframe submission result or PIN API response to confirm a PIN replacement.
Verify the webhook signature and deduplicate deliveries by event id. Deliveries may arrive out of order; use data.updatedAt to avoid overwriting a newer card snapshot. See Card webhooks for payloads and retry behavior.