Skip to main content
Gr4vy Embed renders a PCI-compliant payment form inside the checkout widget. The integration has a server-side component (embed token generation) and a client-side component (loading the payment form).
1

Generate the embed token (server-side)

Use the Gr4vy SDK to generate a signed JWT embed token:
// src/server/services/purchases.ts
import { readFileSync } from "fs";
import { Gr4vy, getEmbedToken, withToken, verifyWebhook } from "@gr4vy/sdk";

interface CheckoutItem {
  name: string;
  quantity: number;
  unitAmount: number; // in cents
}

function getPrivateKey(): string {
  const privateKeyPath = process.env.GR4VY_PRIVATE_KEY_PATH;
  if (!privateKeyPath) {
    throw new Error("Set GR4VY_PRIVATE_KEY_PATH environment variable.");
  }
  return readFileSync(privateKeyPath, "utf8");
}

function getGr4vyClient(): Gr4vy {
  const gr4vyId = process.env.GR4VY_ID;
  if (!gr4vyId) {
    throw new Error("Set GR4VY_ID environment variable.");
  }

  return new Gr4vy({
    server: (process.env.GR4VY_ENVIRONMENT || "sandbox") as "sandbox" | "production",
    id: gr4vyId,
    merchantAccountId: process.env.GR4VY_MERCHANT_ACCOUNT_ID,
    bearerAuth: withToken({ privateKey: getPrivateKey() }),
  });
}

export async function generateEmbedToken(
  amount: number,
  currency: string = "USD",
  cartItems: CheckoutItem[] = []
): Promise<string> {
  const gr4vy = getGr4vyClient();
  const privateKey = getPrivateKey();

  // 1. Create a checkout session on the Gr4vy backend
  const checkoutSession = await gr4vy.checkoutSessions.create({
    amount,
    currency,
    cartItems: cartItems.map((item) => ({
      name: item.name,
      quantity: item.quantity,
      unitAmount: item.unitAmount,
    })),
    metadata: {
      source: "plantly-chatgpt-app",
    },
  });

  // 2. Generate a signed JWT embed token
  return getEmbedToken({
    privateKey,
    checkoutSessionId: checkoutSession.id,
    embedParams: {
      amount,
      currency,
      merchantAccountId: process.env.GR4VY_MERCHANT_ACCOUNT_ID,
      cartItems: cartItems.map((item) => ({
        name: item.name,
        quantity: item.quantity,
        unitAmount: item.unitAmount,
      })),
    },
  });
}
The JWT token pins the payment amount and cart items. If the checkout widget passes mismatched values to gr4vy.setup(), Gr4vy rejects the request. The values must match exactly.
2

Load Gr4vy Embed in the checkout widget

The checkout widget follows the same widget lifecycle as other widgets. After receiving the embed configuration from start_checkout, it loads the Gr4vy Embed script and initialises the payment form. This code runs inside the widget regardless of server language.Receive the embed configuration
// src/widgets/checkout/App.tsx
const [embedConfig, setEmbedConfig] = useState<EmbedConfig | null>(null);
const [embedError, setEmbedError] = useState<string | null>(null);
const embedLoaded = useRef(false);

useEffect(() => {
  waitForToolOutput<CheckoutData>().then((data) => {
    if (data.embed) setEmbedConfig(data.embed);
    if (data.embed_error) setEmbedError(data.embed_error);
  });

  const unsub = onToolResult<CheckoutData>((data) => {
    if (data?.embed) setEmbedConfig(data.embed);
  });
  return unsub;
}, []);
Load the Gr4vy Embed scriptDynamically load the Gr4vy Embed library from your instance’s CDN:
// src/widgets/checkout/App.tsx (continued)
useEffect(() => {
  if (!embedConfig || embedLoaded.current || !embedConfig.gr4vy_id) return;

  const cdnUrl = `https://cdn.${embedConfig.gr4vy_id}.gr4vy.app/embed.latest.js`;
  const script = document.createElement("script");
  script.src = cdnUrl;
  script.onload = () => {
    embedLoaded.current = true;
    initEmbed();
  };
  script.onerror = () => {
    setEmbedError("Payment form could not be loaded.");
  };
  document.head.appendChild(script);
}, [embedConfig]);
Initialise Gr4vy EmbedCall gr4vy.setup() to render the payment form. The amount, currency, and merchantAccountId must match what was pinned in the JWT token:
// src/widgets/checkout/App.tsx (continued)
function initEmbed() {
  if (!embedConfig || typeof gr4vy === "undefined") return;

  gr4vy.setup({
    gr4vyId: embedConfig.gr4vy_id,
    token: embedConfig.token,              // Signed JWT from your server
    element: "#embed-container",           // DOM element for the payment form
    form: "#payment-form",                 // Wrapping <form> element
    amount: embedConfig.amount,            // Total in cents — must match the token
    currency: embedConfig.currency,
    country: embedConfig.country,
    environment: embedConfig.environment,  // "sandbox" or "production"
    intent: "capture",                     // "capture" or "authorize"
    merchantAccountId: embedConfig.merchant_account_id,

    onComplete: (transaction) => {
      // Fires when the payment succeeds.
      // transaction.id — the Gr4vy transaction ID
      // transaction.status — e.g., "capture_succeeded"
      // transaction.paymentMethod — card scheme, last 4 digits, etc.
      // Display your application-specific success screen here.
    },

    onEvent: (name, data) => {
      // Fires on errors and other lifecycle events.
      // "transactionFailed" — the payment was declined or errored
      // "argumentError"    — a configuration mismatch (e.g., amount vs token)
      // "apiError"         — a Gr4vy API error
      if (name === "transactionFailed") {
        // Display your application-specific error screen here.
      }
    },
  });
}
Gr4vy Embed needs a <form> wrapper and a container <div> with id attributes matching gr4vy.setup():
// src/widgets/checkout/App.tsx (JSX)
<form id="payment-form">
  <div id="embed-container" style={{ minHeight: 300 }} />
  <button type="submit">Pay Now</button>
</form>
Gr4vy Embed renders PCI-compliant input fields inside #embed-container. Your app handles the surrounding UI—order summary, result screens, and the submit button. See the reference implementation for a complete example.
3

Verify payment outcomes with webhooks

For production, verify payment outcomes via Gr4vy webhooks rather than relying on client-side callbacks:
// src/server/services/purchases.ts (continued)
export function handleWebhook(
  payload: string,
  signatureHeader: string | null | undefined,
  timestampHeader: string | null | undefined
): Record<string, unknown> {
  const secret = process.env.GR4VY_WEBHOOK_SECRET;
  if (!secret) throw new Error("GR4VY_WEBHOOK_SECRET not configured.");

  // Verify HMAC signature (throws if invalid)
  verifyWebhook(payload, secret, signatureHeader, timestampHeader, 300);

  const event = JSON.parse(payload);
  // Store or process the transaction result
  return event;
}
Configure the webhook URL in your Gr4vy dashboard (https://your-domain.com/webhooks/gr4vy).
Continue to deploy your app to ChatGPT.