Skip to main content
The check frame is a lightweight, headless page hosted on a MoonPay domain. Use it to check whether the customer already has an active connection. If the customer is connected, the frame returns encrypted client credentials. If the customer is not connected (or their connection has expired), the frame returns a status that tells you to render the full connect flow.

URL

https://blocks.moonpay.com/platform/v1/check-connection

Requirements

Key exchange

Credentials returned from the frame are encrypted to protect their content since they are sent over postMessage. You need to generate an X25519 keypair and pass the public key into the frame. The frame uses your public key to encrypt the payload, ensuring only you can read it with your private key.
Never persist the private key to disk or storage. Hold it in memory only for the duration of the session.
Frame credential verification lifecycle
The frame uses the @noble/curves library internally. On web and React Native, you can use this same library to generate your keypair and handle decryption. For native platforms, use a compatible utility like CryptoKit on iOS or KeyPairGenerator on Android.
The following example shows how to generate a keypair and decrypt credentials using @noble/curves. You’ll want to add your own error handling and input validation for production use.
pnpm i @noble/curves @noble/hashes @noble/ciphers
crypto.ts
// An example module for generating keypairs and decrypting client credentials.
// This should not be used as-is in production!

import { gcm } from "@noble/ciphers/aes.js";
import { x25519 } from "@noble/curves/ed25519.js";
import { hkdf } from "@noble/hashes/hkdf.js";
import { sha256 } from "@noble/hashes/sha2.js";

/** The credentials returned from the connect flow. */
export type ClientCredentials = {
  /** A JWT used to authenticate client requests to the Moonpay API. */
  accessToken: string;
  /** A JWT used to initialize authenticated frames such as Apple Pay. */
  clientToken: string;
  /** An [ISO 8601 timestamp](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) representing the expiration time of the tokens. */
  expiresAt: string;
};

/** X25519 `privateKey` and `publicKey`as hex strings. */
export type KeyPair = Record<"privateKey" | "publicKey", string>;

export type DecryptClientCredentialsResult =
  | { ok: true; value: ClientCredentials }
  | { ok: false; error: string };

const hexToBytes = (hex: string): Uint8Array => {
  if (hex.length % 2 !== 0) {
    throw new Error("Invalid hex string");
  }

  const bytes = new Uint8Array(hex.length / 2);
  for (let i = 0; i < bytes.length; i++) {
    bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
  }
  return bytes;
};

const bytesToHex = (bytes: Uint8Array): string => {
  return Array.from(bytes)
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
};

/** Decrypts an encrypted `ClientCredentials`. */
export const decryptClientCredentials = (
  /** A base64-encoded string representing the encrypted JSON payload. **/
  encryptedCredentials: string,
  /** The recipient's X25519 private key as a hex string. **/
  privateKeyHex: string,
): DecryptClientCredentialsResult => {
  // Base64 decode the encrypted credentials
  const payload = atob(encryptedCredentials);

  // Guard and validate this deserialization
  const parsedPayload = JSON.parse(payload);
  // Convert the private key from a hex string to a `Uint8Array`
  const privateKey = hexToBytes(privateKeyHex);
  // Convert the ephemeral public key from a hex string to a `Uint8Array`
  const publicKey = hexToBytes(parsedPayload.ephemeralPublicKey);
  const ivBytes = hexToBytes(parsedPayload.iv);
  const ciphertextBytes = hexToBytes(parsedPayload.ciphertext);
  const sharedSecret = x25519.getSharedSecret(privateKey, publicKey);

  const encryptionKey = hkdf(sha256, sharedSecret, undefined, undefined, 32);
  const cipher = gcm(encryptionKey, ivBytes);
  const plainTextBytes = cipher.decrypt(ciphertextBytes);
  const plaintext = new TextDecoder().decode(plainTextBytes);

  let parsed: unknown;
  try {
    parsed = JSON.parse(plaintext);
  } catch {
    return { ok: false, error: "Failed to parse decrypted payload as JSON" };
  }

  // Validate the decrypted payload
  if (
    typeof parsed !== "object" ||
    parsed === null ||
    typeof (parsed as Record<string, unknown>).accessToken !== "string" ||
    typeof (parsed as Record<string, unknown>).clientToken !== "string" ||
    typeof (parsed as Record<string, unknown>).expiresAt !== "string"
  ) {
    return { ok: false, error: "Decrypted payload missing required fields" };
  }

  return { ok: true, value: parsed as ClientCredentials };
};

/** Generates a new X25519 key pair encryption. */
export const generateKeyPair = (): KeyPair => {
  const { secretKey: privateKey, publicKey } = x25519.keygen();

  return {
    privateKey: bytesToHex(privateKey),
    publicKey: bytesToHex(publicKey),
  };
};
The following example shows how to generate a keypair and decrypt credentials using @noble/curves. You’ll want to add your own error handling and input validation for production use.In React Native, yuo will need a polyfill for getRandomValues (MDN) which is only available in browsers.
pnpm i react-native-get-random-values @noble/curves @noble/hashes @noble/ciphers
crypto.ts
// An example module for generating keypairs and decrypting client credentials.
// This should not be used as-is in production!

import { gcm } from "@noble/ciphers/aes.js";
import { x25519 } from "@noble/curves/ed25519.js";
import { hkdf } from "@noble/hashes/hkdf.js";
import { sha256 } from "@noble/hashes/sha2.js";
// React Native polyfill for getRandomValues
import "react-native-get-random-values";

/** The credentials returned from the connect flow. */
export type ClientCredentials = {
  /** A JWT used to authenticate client requests to the Moonpay API. */
  accessToken: string;
  /** A JWT used to initialize authenticated frames such as Apple Pay. */
  clientToken: string;
  /** An [ISO 8601 timestamp](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) representing the expiration time of the tokens. */
  expiresAt: string;
};

/** X25519 `privateKey` and `publicKey`as hex strings. */
export type KeyPair = Record<"privateKey" | "publicKey", string>;

export type DecryptClientCredentialsResult =
  | { ok: true; value: ClientCredentials }
  | { ok: false; error: string };

const hexToBytes = (hex: string): Uint8Array => {
  if (hex.length % 2 !== 0) {
    throw new Error("Invalid hex string");
  }

  const bytes = new Uint8Array(hex.length / 2);
  for (let i = 0; i < bytes.length; i++) {
    bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
  }
  return bytes;
};

const bytesToHex = (bytes: Uint8Array): string => {
  return Array.from(bytes)
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
};

/** Decrypts an encrypted `ClientCredentials`. */
export const decryptClientCredentials = (
  /** A base64-encoded string representing the encrypted JSON payload. **/
  encryptedCredentials: string,
  /** The recipient's X25519 private key as a hex string. **/
  privateKeyHex: string,
): DecryptClientCredentialsResult => {
  // Base64 decode the encrypted credentials
  const payload = atob(encryptedCredentials);

  // Guard and validate this deserialization
  const parsedPayload = JSON.parse(payload);
  // Convert the private key from a hex string to a `Uint8Array`
  const privateKey = hexToBytes(privateKeyHex);
  // Convert the ephemeral public key from a hex string to a `Uint8Array`
  const publicKey = hexToBytes(parsedPayload.ephemeralPublicKey);
  const ivBytes = hexToBytes(parsedPayload.iv);
  const ciphertextBytes = hexToBytes(parsedPayload.ciphertext);
  const sharedSecret = x25519.getSharedSecret(privateKey, publicKey);

  const encryptionKey = hkdf(sha256, sharedSecret, undefined, undefined, 32);
  const cipher = gcm(encryptionKey, ivBytes);
  const plainTextBytes = cipher.decrypt(ciphertextBytes);
  const plaintext = new TextDecoder().decode(plainTextBytes);

  let parsed: unknown;
  try {
    parsed = JSON.parse(plaintext);
  } catch {
    return { ok: false, error: "Failed to parse decrypted payload as JSON" };
  }

  // Validate the decrypted payload
  if (
    typeof parsed !== "object" ||
    parsed === null ||
    typeof (parsed as Record<string, unknown>).accessToken !== "string" ||
    typeof (parsed as Record<string, unknown>).clientToken !== "string" ||
    typeof (parsed as Record<string, unknown>).expiresAt !== "string"
  ) {
    return { ok: false, error: "Decrypted payload missing required fields" };
  }

  return { ok: true, value: parsed as ClientCredentials };
};

/** Generates a new X25519 key pair encryption. */
export const generateKeyPair = (): KeyPair => {
  const { secretKey: privateKey, publicKey } = x25519.keygen();

  return {
    privateKey: bytesToHex(privateKey),
    publicKey: bytesToHex(publicKey),
  };
};

Initialization parameters

PropertyTypeRequiredDescription
sessionTokenstring✅The session token obtained from your server when creating a session.
publicKeystring✅An ephemeral public key generated on the client. See requirements for details.

The frame uses this key to encrypt the client credentials returned from the connect flow.
channelIdstring✅A unique identifier for the frame generated on your client. This value is attached to each postMessage payload to help identify messages.

The format of this string is up to you.

Events

All events are dispatched using the message pattern described in the frames protocol. Below are the event payloads specific to the check frame.

Outbound events

frame->parent These events are sent from this frame to the parent window.

handshake

The frame requests that you open a message channel.
{
  "version": 2,
  "meta": { "channelId": "ch_1" },
  "kind": "handshake"
}

complete

The frame finished checking the customer’s connection status.
// Active
{
  "version": 2,
  "meta": { "channelId": "ch_1" },
  "kind": "complete",
  "payload": {
    "status": "active",
    "credentials": "<encrypted_value>",
  }
}

// Pending
{
  "version": 2,
  "meta": { "channelId": "ch_1" },
  "kind": "complete",
  "payload": {
    "status": "pending"
  }
}

// Unavailable
{
  "version": 2,
  "meta": { "channelId": "ch_1" },
  "kind": "complete",
  "payload": {
    "status": "unavailable"
  }
}

// Failed
{
  "version": 2,
  "meta": { "channelId": "ch_1" },
  "kind": "complete",
  "payload": {
    "status": "failed",
    "reason": "Unable to create MoonPay account."
  }
}

// Connection required
{
  "version": 2,
  "meta": { "channelId": "ch_1" },
  "kind": "complete",
  "payload": {
    "status": "connectionRequired"
  }
}

error

This event dispatches errors that occur in the flow and, if available, provides steps for recovery.
{
  "version": 2,
  "meta": { "channelId": "ch_1" },
  "kind": "error",
  "payload": {
    "code": "ipAddressMismatch",
    "message": "The client IP address does not match the session."
  }
}

Inbound events

parent->frame These events are sent from the parent window to this frame.

ack

Acknowledge the handshake.
{
  "version": 2,
  "meta": { "channelId": "ch_1" },
  "kind": "ack"
}