> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gr4vy.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Integrate Gr4vy Embed

> Generate embed tokens, load Gr4vy Embed in the checkout widget, and verify webhooks.

[Gr4vy Embed](/guides/payments/embed/quick-start/overview) 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).

<Steps>
  <Step title="Generate the embed token (server-side)">
    Use the [Gr4vy SDK](/guides/payments/embed/quick-start/sdks) to generate a signed JWT embed token:

    <CodeGroup>
      ```ts TypeScript theme={"system"}
      // 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,
            })),
          },
        });
      }
      ```

      ```python Python theme={"system"}
      # purchases.py
      import os
      from gr4vy import auth

      def generate_embed_token(buyer_id: str, amount: int, currency: str = "USD") -> str:
          """Generate a Gr4vy Embed token for client-side checkout.

          Args:
              buyer_id: The Gr4vy buyer ID.
              amount: Transaction amount in smallest currency unit (cents).
              currency: ISO 4217 currency code.

          Returns:
              A signed JWT token for Gr4vy Embed.
          """
          private_key = os.environ.get("GR4VY_PRIVATE_KEY")
          if not private_key:
              raise ValueError("Set GR4VY_PRIVATE_KEY environment variable.")

          return auth.get_embed_token(
              private_key,
              embed_params={
                  "amount": amount,
                  "currency": currency,
                  "buyer_id": buyer_id,
              },
          )
      ```
    </CodeGroup>

    <Warning>
      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.
    </Warning>
  </Step>

  <Step title="Load Gr4vy Embed in the checkout widget">
    The checkout widget follows the same [widget lifecycle](/guides/payments/chatgpt-app/widgets#step-3-build-the-widgets) 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**

    <CodeGroup>
      ```tsx TypeScript theme={"system"}
      // 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;
      }, []);
      ```

      ```js Python theme={"system"}
      // assets/checkout.html (inline <script>)
      var items = [];
      var embedConfig = null;
      var embedLoaded = false;

      function onToolResult(data) {
        if (!data) return;
        if (data.items) items = data.items;
        if (data.embed) embedConfig = data.embed;
        render();
      }

      // MCP Apps bridge — listen for tool result updates
      window.addEventListener("message", function(ev) {
        if (ev.source !== window.parent) return;
        var msg = ev.data;
        if (!msg || msg.jsonrpc !== "2.0") return;
        if (msg.method === "ui/notifications/tool-result") {
          onToolResult(msg.params && msg.params.structuredContent);
        }
      });

      // Poll for initial tool output
      function checkOpenAI() {
        if (window.openai && window.openai.toolOutput) {
          onToolResult(window.openai.toolOutput);
        } else {
          setTimeout(checkOpenAI, 100);
        }
      }
      checkOpenAI();
      ```
    </CodeGroup>

    **Load the Gr4vy Embed script**

    Dynamically load the Gr4vy Embed library from your instance's CDN:

    <CodeGroup>
      ```tsx TypeScript theme={"system"}
      // 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]);
      ```

      ```js Python theme={"system"}
      // assets/checkout.html (inline <script>, continued)
      function loadEmbed() {
        if (!embedConfig || !embedConfig.gr4vy_id) return;

        var cdnUrl = "https://cdn." + embedConfig.gr4vy_id + ".gr4vy.app/embed.latest.js";
        var script = document.createElement("script");
        script.src = cdnUrl;
        script.onload = function() {
          embedLoaded = true;
          initEmbed();
        };
        script.onerror = function() {
          var el = document.getElementById("embed-container");
          if (el) {
            el.innerHTML = '<div style="text-align:center;padding:20px;color:#dc2626;">' +
              'Failed to load payment form. Please try again.</div>';
          }
        };
        document.head.appendChild(script);
      }
      ```
    </CodeGroup>

    **Initialise Gr4vy Embed**

    Call `gr4vy.setup()` to render the payment form. The `amount`, `currency`, and `merchantAccountId` must match what was pinned in the JWT token:

    <CodeGroup>
      ```tsx TypeScript theme={"system"}
      // 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.
            }
          },
        });
      }
      ```

      ```js Python theme={"system"}
      // assets/checkout.html (inline <script>, continued)
      function initEmbed() {
        if (!embedConfig || typeof gr4vy === "undefined") return;

        var totalCents = embedConfig.amount || Math.round(getTotal() * 100);

        var options = {
          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: totalCents,                    // Total in cents — must match the token
          currency: embedConfig.currency || "USD",
          country: embedConfig.country || "US",
          environment: embedConfig.environment || "sandbox",
          intent: "capture",                     // "capture" or "authorize"
          onComplete: function(transaction) {
            // Fires when the payment succeeds.
            // transaction.id — the Gr4vy transaction ID
            // transaction.status — e.g., "capture_succeeded"
            // Display your application-specific success screen here.
            showResult(transaction);
          },
          onEvent: function(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)
            if (name === "transactionCreated" || name === "transactionFailed") {
              showResult(data);
            }
          }
        };

        if (embedConfig.buyer_id) options.buyerId = embedConfig.buyer_id;
        if (embedConfig.merchant_account_id) {
          options.merchantAccountId = embedConfig.merchant_account_id;
        }

        gr4vy.setup(options);
      }
      ```
    </CodeGroup>

    Gr4vy Embed needs a `<form>` wrapper and a container `<div>` with `id` attributes matching `gr4vy.setup()`:

    <CodeGroup>
      ```tsx TypeScript theme={"system"}
      // src/widgets/checkout/App.tsx (JSX)
      <form id="payment-form">
        <div id="embed-container" style={{ minHeight: 300 }} />
        <button type="submit">Pay Now</button>
      </form>
      ```

      ```html Python theme={"system"}
      <!-- assets/checkout.html (rendered by the render() function) -->
      <form id="payment-form">
        <div id="embed-container"></div>
        <button type="submit" class="pay-btn">Pay $19.99</button>
      </form>
      ```
    </CodeGroup>

    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](https://github.com/gr4vy/gr4vy-typescript-adk) for a complete example.
  </Step>

  <Step title="Verify payment outcomes with webhooks">
    For production, verify payment outcomes via Gr4vy webhooks rather than relying on client-side callbacks:

    <CodeGroup>
      ```ts TypeScript theme={"system"}
      // 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;
      }
      ```

      ```python Python theme={"system"}
      # purchases.py (continued)
      import json
      import hmac
      import hashlib

      def handle_webhook(
          payload: str,
          signature: str | None,
          timestamp: str | None,
      ) -> dict:
          """Verify and process a Gr4vy webhook."""
          secret = os.environ.get("GR4VY_WEBHOOK_SECRET")
          if not secret:
              raise ValueError("GR4VY_WEBHOOK_SECRET not configured.")

          if not signature or not timestamp:
              raise ValueError("Missing signature or timestamp header.")

          # Verify HMAC signature
          message = f"{timestamp}.{payload}"
          expected = hmac.new(
              secret.encode(), message.encode(), hashlib.sha256
          ).hexdigest()
          if not hmac.compare_digest(expected, signature):
              raise ValueError("Invalid webhook signature.")

          event = json.loads(payload)
          # Store or process the transaction result
          return event
      ```
    </CodeGroup>

    Configure the webhook URL in your Gr4vy dashboard (`https://your-domain.com/webhooks/gr4vy`).
  </Step>
</Steps>

Continue to [deploy your app to ChatGPT](/guides/payments/chatgpt-app/deployment).
