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

# Accept payments

> Wire a Cta button to Stripe Checkout via the collectPayment capability.

export const ErrorTable = ({errors}) => <div className="gv-error-table" role="table" aria-label="Errors and fixes">
    <div className="gv-error-table__head" role="row">
      <span className="gv-error-table__col-head" role="columnheader">Error</span>
      <span className="gv-error-table__col-head" role="columnheader">Cause</span>
      <span className="gv-error-table__col-head" role="columnheader">Fix</span>
    </div>
    {errors.map((err, i) => <div className="gv-error-table__row" role="row" key={i}>
        <code className="gv-error-table__code" role="cell">{err.code}</code>
        <div className="gv-error-table__cause" role="cell">{err.cause}</div>
        <div className="gv-error-table__fix" role="cell">{err.fix}</div>
      </div>)}
  </div>;

export const ExpectedOutput = ({caption = "Expected output", children}) => <div className="gv-output">
    <div className="gv-output__label">
      <svg className="gv-output__icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
        <polyline points="15 10 20 15 15 20" />
        <path d="M4 4v7a4 4 0 0 0 4 4h12" />
      </svg>
      <span>{caption}</span>
    </div>
    <div className="gv-output__body">{children}</div>
  </div>;

export const PageMeta = ({audience, readingTime, prereqs, level}) => {
  const items = [audience && ({
    key: "audience",
    label: "AUDIENCE",
    value: audience
  }), readingTime && ({
    key: "time",
    label: "READ",
    value: readingTime
  }), level && ({
    key: "level",
    label: "LEVEL",
    value: level
  }), prereqs && prereqs.length > 0 && ({
    key: "prereqs",
    label: "PREREQS",
    value: prereqs.join(" · ")
  })].filter(Boolean);
  if (items.length === 0) return null;
  return <div className="gv-page-meta" role="group" aria-label="Page metadata">
      {items.map(item => <div className="gv-page-meta__item" key={item.key}>
          <span className="gv-page-meta__label">{item.label}</span>
          <span className="gv-page-meta__value">{item.value}</span>
        </div>)}
    </div>;
};

export const BrandCard = ({eyebrow, title, href, icon, badge, cta, featured = false, horizontal = false, children}) => {
  const classes = ["gv-card", featured && "gv-card--featured", horizontal && "gv-card--horizontal"].filter(Boolean).join(" ");
  const inner = <>
      {icon && <span className="gv-card__icon" aria-hidden="true">
          <Icon icon={icon} />
        </span>}
      <div className="gv-card__main">
        {(eyebrow || badge) && <div className="gv-card__meta">
            {eyebrow && <Eyebrow label={eyebrow} tone="muted" />}
            {badge && <span className="gv-card__badge">{badge}</span>}
          </div>}
        <h3 className="gv-card__title">{title}</h3>
        {children && <div className="gv-card__body">{children}</div>}
        {cta && <span className="gv-card__cta">
            {cta}
            <svg className="gv-card__cta-arrow" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
              <line x1="7" y1="17" x2="17" y2="7" />
              <polyline points="7 7 17 7 17 17" />
            </svg>
          </span>}
      </div>
    </>;
  return href ? <a className={classes} href={href}>
      {inner}
    </a> : <div className={classes}>{inner}</div>;
};

export const NumberedStep = ({n, title, children}) => <li className="gv-step">
    <span className="gv-step__index">{n}</span>
    <div className="gv-step__body">
      <h3 className="gv-step__title">{title}</h3>
      <div className="gv-step__content">{children}</div>
    </div>
  </li>;

export const NumberedSteps = ({children}) => <ol className="gv-steps">{children}</ol>;

export const ArrowItem = ({children}) => <li className="gv-arrow-item">
    <span className="gv-arrow-item__arrow" aria-hidden="true">{"↳︎"}</span>
    <span className="gv-arrow-item__content">{children}</span>
  </li>;

export const ArrowList = ({children}) => <ul className="gv-arrow-list">{children}</ul>;

export const Eyebrow = ({label, tone = "default"}) => <div className={`gv-eyebrow gv-eyebrow--${tone}`}>
    <span className="gv-eyebrow__bar" aria-hidden="true" />
    <span className="gv-eyebrow__label">{label}</span>
  </div>;

<Eyebrow label="GUIDE" />

<PageMeta audience="Developers" readingTime="15 min" level="Intermediate" prereqs={["Stripe Connect onboarded", "Published plan", "Page in draft"]} />

By the end of this guide a `Cta` block on your page will open a Stripe Checkout session when clicked, redirect on success, and fall back cleanly on cancellation. You confirm Stripe Connect is active for the workspace, identify the plan you want to sell, bind the capability to a block, and publish.

## Prerequisites

<ArrowList>
  <ArrowItem>Stripe Connect onboarding completed for the workspace. The Builder walks through it under **Workspace → Settings → Payments**; the [Monetization API](/api-reference/resources/monetization) covers the programmatic path.</ArrowItem>
  <ArrowItem>At least one published billing plan in the workspace billing config.</ArrowItem>
  <ArrowItem>A page already in draft, or create a new one in the Builder before continuing.</ArrowItem>
</ArrowList>

## Steps

<NumberedSteps>
  <NumberedStep title="Verify Stripe Connect is live">
    Confirm the workspace's Stripe Connect account is fully onboarded. A `state` of `"connected"` means payments can flow.

    ```ts theme={null}
    import { Gavai } from "@gavai/sdk";

    const gv = new Gavai({ apiKey: process.env.GAVAI_API_KEY });

    const monetization = await gv.monetization.get({ workspace: "acme" });
    console.log("Stripe state:", monetization.state);
    ```

    <ExpectedOutput>
      ```text theme={null}
      Stripe state: connected
      ```
    </ExpectedOutput>

    If the state is anything other than `connected`, complete Stripe Connect onboarding in the Builder (**Workspace → Settings → Payments**) before continuing. Every `collectPayment` call returns `stripe_not_connected` until onboarding is finished.
  </NumberedStep>

  <NumberedStep title="Pick the plan to sell">
    You need the `id` of a published plan. The canonical place to browse plans is the Builder UI (**Workspace → Billing → Plans**). The `listPlans` capability covers programmatic listing.

    ```ts theme={null}
    const result = await gv.capabilities.invoke("listPlans", {});

    if (result.ok) {
      result.plans.forEach((p) =>
        console.log(p.id, p.name, p.price_cents / 100, p.currency, p.interval)
      );
    }
    ```

    <ExpectedOutput>
      ```text theme={null}
      plan_pro_monthly Pro 29 usd month
      plan_pro_yearly Pro (annual) 290 usd year
      ```
    </ExpectedOutput>

    Copy the `id` of the plan you want — you will pass it as `plan_id` in the next step.

    <Note>
      Programmatic capability invocation from server-side scripts is still being documented. The shape above matches the runtime SDK; verify against the SDK reference when you ship.
    </Note>
  </NumberedStep>

  <NumberedStep title="Bind a Cta block to collectPayment">
    Patch your page to add (or update) a `Cta` block that invokes `collectPayment` when clicked. Use absolute URLs for `success_url` and `cancel_url`.

    ```ts theme={null}
    await gv.pages.patch({
      workspace: "acme",
      id: "page_01H8X4N5P6Q7R8S9T0U1V2W3X4",
      operations: [
        {
          op: "add",
          path: "/tree/children/-",
          value: {
            block: "Cta",
            props: {
              headline: "Start your Pro plan",
              cta_label: "Subscribe now",
              on_click: {
                capability: "collectPayment",
                args: {
                  plan_id: "plan_pro_monthly",
                  success_url: "https://app.acme.com/welcome",
                  cancel_url: "https://app.acme.com/pricing",
                  customer_email: "{{authUser.email}}",
                },
              },
            },
          },
        },
      ],
    });
    ```

    `customer_email` accepts a runtime interpolation that resolves to the signed-in user's email and pre-fills the Stripe Checkout form. If your page does not require sign-in, omit `customer_email`.

    <Note>
      The Cta block uses `on_click` for capability bindings; Form blocks use `on_submit`. If you are not sure which a custom block uses, run the Builder MCP's `get_block_schema` against the block before shipping.
    </Note>

    <Note>
      The `args` object is validated against the `collectPayment` input schema at call time. Missing required fields return `code: "invalid_input"` with field-level details.
    </Note>
  </NumberedStep>

  <NumberedStep title="Publish the page">
    Once the patch is applied, publish to make the button live.

    ```ts theme={null}
    await gv.pages.publish({ workspace: "acme" });
    console.log("Published.");
    ```
  </NumberedStep>

  <NumberedStep title="Test with a Stripe test card">
    Open the published page and click the CTA. The expected flow:

    <ArrowList>
      <ArrowItem>The runtime calls `collectPayment`, creates a Stripe Checkout session, and returns a `checkout_url`.</ArrowItem>
      <ArrowItem>The user is redirected to Stripe's hosted checkout page.</ArrowItem>
      <ArrowItem>On successful payment, Stripe redirects to your `success_url`.</ArrowItem>
      <ArrowItem>On cancellation, Stripe redirects to your `cancel_url`.</ArrowItem>
    </ArrowList>

    While the API key is `gak_test_*`, use Stripe's test card `4242 4242 4242 4242` (any future expiry, any CVC) to simulate payments without real charges.

    <Note>
      Refunds, disputes, and subscription renewals are managed in your workspace's Stripe Dashboard. gavAI does not proxy those flows; they happen directly between Stripe and your Connect account.
    </Note>
  </NumberedStep>
</NumberedSteps>

## Troubleshooting

<ErrorTable
  errors={[
{
  code: "stripe_not_connected",
  cause: "The workspace's Connect account is not fully onboarded.",
  fix: "Finish onboarding in the Builder under Workspace → Settings → Payments.",
},
{
  code: "invalid_input",
  cause: "The collectPayment args are missing a required field or have a bad value.",
  fix: "Read the field-level details in the error response, fix, and retry.",
},
{
  code: "Stripe: 'No such plan'",
  cause: "The plan_id does not exist or is not published in this workspace.",
  fix: "Re-list plans and copy the exact id.",
},
{
  code: "Redirect to cancel_url (no error)",
  cause: "The user dismissed the Checkout page.",
  fix: "Expected behavior — no action needed.",
},
]}
/>

## What you built

You confirmed Stripe Connect is active, picked a plan, added a `Cta` block with a `collectPayment` binding to your page tree, and tested the flow with a Stripe test card. End-users now move from your page to Stripe Checkout and back, with no payment infrastructure to run yourself.

## Next steps

<Columns cols={2}>
  <BrandCard title="collectPayment reference" href="/capabilities/collect-payment" cta="Read reference">
    Full input/output schema and error codes for the collectPayment capability.
  </BrandCard>

  <BrandCard title="listPlans reference" href="/capabilities/list-plans" cta="Read reference">
    Return your workspace's published billing plans and pricing.
  </BrandCard>

  <BrandCard title="Monetization API" href="/api-reference/resources/monetization" cta="Browse endpoints">
    Programmatic endpoints for Stripe Connect onboarding and plan management.
  </BrandCard>
</Columns>
