CodeGym /Courses /ChatGPT Apps /ACP / Instant Checkout: the standard and its implementati...

ACP / Instant Checkout: the standard and its implementation in ChatGPT

ChatGPT Apps
Level 14 , Lesson 3
Available

1. Why ACP exists and why it’s not “just another REST API”

If you look cynically, ACP appears to be a set of ordinary HTTP endpoints and JSON structures: some /checkout_sessions, some webhooks, some tokens. It’s easy to think: “Okay, this is yet another custom API from yet another platform.” But ACP’s idea runs deeper.

ACP is designed as an open protocol of interaction among three parties: the AI platform (for example, ChatGPT), your commerce backend, and the payment service provider. Its goal is to standardize how to describe products and prices, how the AI declares the user’s intent to buy, how a checkout session is created, how the payment is executed, and how all parties learn the final status.

The key idea: the same merchant backend implementing ACP can potentially work not only with ChatGPT but also with other LLM platforms that adopt this standard. In other words, you are not writing “a special API for ChatGPT”; you are implementing a next‑generation commerce integration protocol.

Instant Checkout in ChatGPT is the first major implementation of the ACP standard. ChatGPT adheres to this protocol, calling your ACP endpoints and showing the user a polished UI, but the rules of the game are described in the ACP specifications, not hidden as “GPT magic” inside a black box.

2. The three pillars of ACP: Product Feed, Agentic Checkout, Delegated Payment

ACP has three primary specifications that we will refer to repeatedly:

Specification Responsibility Where this shows up in GiftGenius
Product Feed Spec Format and fields of the product feed (SKU, prices, availability, links, flags). JSON/CSV feed with gifts, which OpenAI indexes.
Agentic Checkout REST contract for checkout_session: creation, update, finalize. Our ACP backend: endpoints /checkout_sessions and webhooks.
Delegated Payment How payment data is passed to the merchant as a delegated token. Working with the Stripe Shared Payment Token when finalizing the payment.

We’ve already covered the Product Feed in prior lectures. Now we care about the last two blocks: Agentic Checkout and Delegated Payment.

It’s important to distinguish three layers:

  1. Standard (SPEC). The official documents describe which fields and endpoints must exist, which statuses are legal, and which guarantees you commit to.
  2. Architectural pattern (ARCH). For example, deciding to store SKUs and orders in separate tables, adding a service wrapper around ACP, or using a queue for webhooks. These are good practices, but not part of the standard.
  3. Concrete implementation (the GiftGenius example). This is our learning project: our table structure, exact TypeScript type names, how we log orders, etc. All of this is an example, not a normative document.

We will keep highlighting where the SPEC ends and your architecture begins — so you don’t end up thinking “I saw a persona_tags field in the lecture and decided it’s part of the official spec.”

3. Checkout session from the inside: structure and statuses

The central object of the Agentic Checkout Spec is the checkout_session in your backend. Logically, it’s the state of a purchase: which items, what total, which fulfillment options, and what status the payment attempt is currently in.

The spec describes the required checkout_session fields roughly like this (wording simplified and partially shortened compared to the original):

  • id — a string identifier of the session that you generate and return. ChatGPT will use it in all subsequent calls.
  • buyer — buyer information: name, email, phone number, sometimes address. In the real spec this object is structured so that the PSP and your systems can reliably use it.
  • status — a string enum reflecting the current state of the purchase. Base statuses:
    • not_ready_for_payment — you cannot pay yet (for example, shipping option not selected or taxes not recalculated).
    • ready_for_payment — everything is ready; you can request a payment token and charge funds.
    • completed — payment succeeded; the order is created.
    • canceled — the purchase was canceled (by the user or due to an error).
  • currency — currency code in ISO 4217, lower case ("usd", "eur", etc.).
  • line_items — the list of cart line items, each with its SKU, quantity, and computed cost.
  • fulfillment_address — shipping address (if relevant).
  • fulfillment_options and fulfillment_option_id — possible fulfillment (or shipping) options and the currently selected one.
  • totals — aggregated amounts: item total, taxes, shipping, grand total.
  • order — an object describing the order that will be created after the session successfully completes.
  • messages — a list of user-facing messages that ChatGPT can show the buyer, e.g., warnings or errors.
  • links — a list of links, for example, to the return policy, Privacy Policy, and Terms of Service.

We don’t need to implement every field in the demo, but understanding the idea matters: checkout_session is the “history and current state of a single purchase attempt”, and ChatGPT expects to see everything needed for a correct UX.

To make it easier, let’s introduce a simplified type in our learning code:

// Simplified checkout_session model for GiftGenius (not the full SPEC)
type GGCheckoutStatus = 'not_ready_for_payment' | 'ready_for_payment' | 'completed' | 'canceled';

type GGLineItem = { skuId: string; quantity: number; total: number };

type GGCheckoutSession = {
  id: string;
  status: GGCheckoutStatus;
  currency: 'usd';
  lineItems: GGLineItem[];
  grandTotal: number;
};

This model is intentionally simpler than the official one, but it’s great for practice: train yourself to manage statuses and transitions without drowning in a hundred fields.

4. Checkout_session lifecycle

The Agentic Checkout specification describes several operations on a checkout_session. In simplified form, the lifecycle looks like this:

  1. Create a session: POST /checkout_sessions.
  2. Update a session: POST /checkout_sessions/{id}.
  3. Complete a session: POST /checkout_sessions/{id}/complete.
  4. (Sometimes) Cancel: a separate cancel endpoint or a transition to canceled via an update.

From a state perspective, you could draw this diagram:

stateDiagram-v2
    [*] --> not_ready_for_payment
    not_ready_for_payment --> ready_for_payment: shipping/tax calculation
option selection ready_for_payment --> completed: successful POST /complete ready_for_payment --> canceled: user cancellation or error not_ready_for_payment --> canceled: error, incompatible data

Creation of a checkout_session usually starts it in not_ready_for_payment or immediately ready_for_payment if everything needed for payment is already known (for example, a digital item with no shipping or taxes). Updates are used to add data (address, promo codes, fulfillment option) and recalculate totals. Completion is when Delegated Payment kicks in and funds are actually charged.

It’s important to understand the roles here:

  • ChatGPT initiates creation, updates, and completion of the session based on the conversation with the user.
  • Your backend (the merchant) is responsible for correct business logic: validating SKUs and availability, calculating prices and taxes, changing statuses, and creating orders.
  • The PSP (Stripe and others) processes the actual payment and issues the Shared Payment Token, which the merchant uses to charge funds.

Shortly, we’ll overlay concrete HTTP requests and tiny code examples on top of this state diagram.

5. Creating a checkout_session: what exactly ChatGPT expects from us

When ChatGPT (or an agent) decides the user truly wants to buy something, it forms line items based on the Product Feed: a list of SKUs, quantities, the presumed currency, and possibly additional shipping preferences. It then calls your POST /checkout_sessions endpoint.

On the merchant side you need to:

  1. Validate the input: make sure all SKUs exist, are available for sale, and don’t violate policy (e.g., no alcohol for a minor).
  2. Calculate prices and taxes based on your own rules.
  3. Prepare fulfillment (shipping) options if the item is physical.
  4. Return a correct checkout_session with status and totals.

The simplest Express handler for GiftGenius might look like this:

// Pseudocode: creating a simplified checkout_session
app.post('/checkout_sessions', async (req, res) => {
  const items = req.body.lineItems as GGLineItem[];  // skuId + quantity
  const pricedItems = await priceItems(items);       // compute total per SKU
  const grandTotal = sum(pricedItems.map(i => i.total));

  const session: GGCheckoutSession = {
    id: generateId(),
    status: 'ready_for_payment', // for digital gifts we can go straight to ready
    currency: 'usd',
    lineItems: pricedItems,
    grandTotal,
  };

  res.status(201).json(session);
});

Here we do several things:

  • We don’t trust inbound prices from the client (ChatGPT) and recompute them using our own data — this is critical for commerce security.
  • We generate our own session id (for example, prefix gg_chk_...).
  • We return the ready_for_payment status if there are no extra steps (no shipping, automatic taxes, simple model).

In a real ACP‑compliant backend you would also return messages, links, and a composite totals object, and also fill out order (at least as a draft), as described in the spec.

6. Updating a checkout_session and idempotency

After the session is created, ChatGPT can ask the user for additional details: shipping address, coupon application, fulfillment option change. When this data appears, the platform calls POST /checkout_sessions/{id} so you can update the calculations.

Code‑wise this is very similar to creation, but instead of generating a new session you:

  • find the existing one by id;
  • apply the changes (for example, change fulfillment_option_id or add a discount);
  • recalculate totals;
  • return the updated checkout_session.

Importantly, the spec allows repeated calls (due to network glitches or retries by ChatGPT). So, just like in earlier modules where we talked about idempotency for tools and webhooks, it’s recommended to use an Idempotency-Key in request headers and handle repeats carefully.

A possible update handler could look like this:

app.post('/checkout_sessions/:id', async (req, res) => {
  const id = req.params.id;
  const key = req.header('Idempotency-Key'); // same key => same effect
  const existing = await loadSessionWithIdempotency(id, key, req.body);

  // applyUpdates inside can recalculate prices, shipping, etc.
  const updated = await applyUpdates(existing, req.body);
  await saveSession(updated, key);

  res.json(updated);
});

We’re not strictly following the SPEC’s exact structure here; we’re illustrating the idea: input — changes and an idempotency key; output — a consistent checkout_session state. If the same request comes with the same key, you must return the same result without creating extra orders or duplicates in logs.

7. Completing a checkout_session and Delegated Payment: how the Shared Payment Token works

The most interesting and nerve‑wracking moment is completing the checkout_session when funds are actually charged. This is where the second specification comes into play: Delegated Payment.

The idea of Delegated Payment

The user enters or selects payment details in the ChatGPT interface (card, wallet, saved payment method). The platform doesn’t send this data to you directly — instead, it requests a special token from the PSP (e.g., Stripe), the Shared Payment Token (SPT), which:

  • is uniquely tied to the merchant and a specific session;
  • is limited in amount and lifetime;
  • does not reveal the real card number to you.

This results in the following picture:

Actor Sees card details Sees Shared Payment Token Sees order details (SKU, amounts)
User Yes (enters them in the UI) No (not needed) Partially (what they buy and for how much)
ChatGPT/OpenAI Yes (during payment) Yes Yes
PSP (Stripe) Yes Yes Within the payment
Merchant No Yes Yes

This design lets the merchant avoid storing payment credentials and focus on order business logic, leaving compliance issues to the PSP and the platform.

Insight

The point of the Shared Payment Token is to hide card data from your backend, while you still perform the charge. But you can also look at it slightly differently.

You’ve likely encountered a situation where a store or hotel first places an authorization hold on your card and later captures the funds. Think of the Shared Payment Token as a hold token. ChatGPT placed an authorization hold on the user’s account, but did not capture the funds. It passed you this hold token, and now you can forward it to Stripe and charge the funds.

There are two important nuances here:

  • the hold and capture amounts shouldn’t differ much; ideally they should match exactly.
  • you can sell the first month of a subscription via ChatGPT for $1, and then charge $49.99 every month after.

Request POST /checkout_sessions/{id}/complete

When the user presses the payment confirmation button in Instant Checkout, ChatGPT:

  1. Requests an SPT from the PSP (for example, via the Stripe ACP API).
  2. Sends this token to your backend via POST /checkout_sessions/{id}/complete along with buyer data.

The spec describes the request body roughly like this (below is an adapted and shortened example from the official docs):

POST /checkout_sessions/checkout_session_123/complete

{
  "buyer": {
    "first_name": "John",
    "last_name": "Smith",
    "email": "johnsmith@mail.com"
  },
  "payment_data": {
    "token": "spt_123",
    "provider": "stripe"
  }
}

Your backend should then:

  1. Find the checkout_session with id checkout_session_123.
  2. Verify that the status allows completion (usually ready_for_payment).
  3. Create a payment with the PSP using token spt_123 (the method depends on the PSP; for Stripe — a specific endpoint and payment method type).
  4. Wait for the payment operation confirmation.
  5. Update the checkout_session to completed, create and persist the order, and populate the order field in the session structure.
  6. Return the current checkout_session in the response.

In very simplified TypeScript‑like pseudocode it could look like this:

app.post('/checkout_sessions/:id/complete', async (req, res) => {
  const { id } = req.params;
  const { buyer, payment_data } = req.body;
  const session = await loadSession(id);

  await chargeWithSharedToken(payment_data.token, session.grandTotal);
  const completed = await markSessionCompleted(session, buyer);

  res.json(completed);
});

In the real world, these lines would hide error handling, retries, logging, and integration with your order model.

If something goes wrong (for example, the payment is declined), you should return a checkout_session with status not_ready_for_payment or canceled and fill messages so ChatGPT can correctly explain to the user what happened.

8. Instant Checkout in ChatGPT: how everything comes together into one flow

Now let’s assemble these pieces into an end‑to‑end “intent to payment” scenario in ChatGPT. You can think of this lecture as “decoding” what hides behind a single “Buy” button in the widget.

Simplified scenario:

  1. The user writes: “Find a digital gift for a friend up to $50 and check out right away.”
  2. The agent (or the ChatGPT App itself) uses the Product Feed to find suitable SKUs within the budget.
  3. ChatGPT shows several gift cards in the chat (through your GiftGenius widget) and suggests picking one.
  4. After the selection, ChatGPT forms line items and calls POST /checkout_sessions on your ACP backend, receiving a checkout_session with totals and status.
  5. In the Instant Checkout UI, the user sees the final amount, the item name, return policy, and a confirmation button.
  6. On confirmation, ChatGPT obtains a Shared Payment Token from the PSP and calls POST /checkout_sessions/{id}/complete, as discussed above.
  7. Your backend processes the payment, creates the order, and returns a checkout_session with status completed.
  8. ChatGPT shows the user a confirmation, and your backend (via webhooks per the Agentic Checkout Spec) can send an event back to OpenAI so the platform knows the order’s outcome.

As a sequence diagram, it looks like this:

sequenceDiagram
    actor U as User
    participant GPT as ChatGPT
    participant GG as GiftGenius ACP backend
    participant PSP as Stripe (PSP)

    U->>GPT: I want a gift up to $50 and to buy it right here
    GPT->>GG: POST /checkout_sessions (line_items)
    GG-->>GPT: checkout_session (ready_for_payment)
    GPT->>U: Displays Instant Checkout (item, price, ToS)
    U->>GPT: Clicks “Confirm payment”
    GPT->>PSP: Request SPT for amount and merchant
    PSP-->>GPT: Shared Payment Token (spt_xxx)
    GPT->>GG: POST /checkout_sessions/{id}/complete (token + buyer)
    GG->>PSP: Payment with SPT
    PSP-->>GG: Payment successful
    GG-->>GPT: checkout_session (completed + order)
    GPT-->>U: Displays purchase confirmation

In this scenario, there is no “arbitrary” call to your database or to strange internal endpoints. Everything fits into the strictly described ACP contract where each participant knows its role.

9. Mini‑practice: a simplified ACP backend for GiftGenius

To keep this lecture practical, it’s important to mentally walk through implementing an ACP layer for our learning project.

Assume GiftGenius already has:

  • A SKU and price database used to form the Product Feed (we modeled this in previous lectures).
  • A simple order model: an orders table with fields id, userId, skuId, amount, currency, status, createdAt.
  • A ChatGPT App UI and an MCP layer that can recommend gifts (we built this in previous course modules).

Now your task is to add one more small service, gg-acp:

  • Endpoint POST /checkout_sessions:
    • Accepts a list of SKUs and quantities.
    • Recalculates totals based on your DB.
    • Creates a draft order (for example, with status pending) and a checkout_session with status ready_for_payment.
    • Returns the checkout_session.
  • Endpoint POST /checkout_sessions/{id}:
    • Finds the session and the order.
    • Applies changes (for example, promo code support that reduces the final amount).
    • Returns the updated checkout_session.
  • Endpoint POST /checkout_sessions/{id}/complete:
    • Receives the SPT, amount, and buyer data.
    • In the demo version, it can simply mark the order as “paid” without a real PSP integration call (or you can simulate Stripe).
    • Updates the checkout_session to completed and attaches an order_id to it.

You can implement this entire service in a small Node/Express application or as endpoints in the Next.js App Router. The main thing is to honor the contract for the format and statuses even if you emulate the payment.

A possible order model in TypeScript could look like this:

// Simplified GiftGenius order model
type GGOrderStatus = 'pending' | 'paid' | 'canceled';

type GGOrder = {
  id: string;
  userId: string;
  skuId: string;
  amount: number;
  currency: 'usd';
  status: GGOrderStatus;
};

In production you’ll add integration with your Auth/Identity (to know which user the chat represents), webhooks to OpenAI, and more complex refund scenarios. But as a learning step within this lecture, it’s enough to get comfortable with the loop: create a session → update → complete — without losing money or your sanity.

10. Common mistakes when designing ACP / Instant Checkout

Mistake #1: mixing roles (“ChatGPT is my store”).
Sometimes developers mentally appoint ChatGPT as the “system of record” and try to store business order state on the platform’s side: “well, there’s a checkout_session there, so I’ll read my order history from OpenAI.” That leads nowhere. checkout_session is a protocol object, not the source of truth for orders. The source of truth is your commerce backend: orders, statuses, refunds, and reports must live there. In this setup, ChatGPT is just a trusted “chat frontend.”

Mistake #2: trusting inbound prices from ChatGPT.
It’s easy to think: “the agent already picked SKUs and even tallied the amount; let’s just accept that amount and charge.” Don’t do this. Input from ChatGPT (line items, presumed prices) should be treated as a proposal, not as a command. Your backend must validate SKUs, prices, availability, discount applicability, etc., against the Product Feed and your DB. Otherwise you’ll get a nasty class of bugs like “the user bought an item for $0.01 because the model decided to round.”

Mistake #3: ignoring statuses and the state machine.
Early prototypes often implement a “leaky” version: the session status is always completed or simply ok, while discrepancies with the actual payment state get buried inside. As a result, ChatGPT can’t correctly show the user what’s happening: whether the payment is in progress, already completed, or was canceled. It’s much more reliable to honestly implement the state machine not_ready_for_paymentready_for_paymentcompleted/canceled and return the real status from your backend, instead of inventing ad‑hoc fields.

Mistake #4: using the Shared Payment Token as a “reusable card.”
By design, the SPT is a one‑time or strictly constrained token: it’s tied to a specific operation, amount, and merchant. Trying to cache it “just in case” or reuse it for another purchase is a bad idea. At best, the PSP will reject the second attempt; at worst, you’ll confuse payment accounting and orders. Each checkout_session.complete must have its own fresh token, and if the payment fails, a new token should be requested.

Mistake #5: no idempotency in /checkout_sessions and webhooks.
In real networks, requests can be duplicated: ChatGPT can retry POST /checkout_sessions after a timeout; a PSP can resend a webhook after a temporary error. If your implementation creates a new order and a new DB record each time, you’ll quickly get chaos: double charges, duplicate orders, and confusing discrepancies across systems. Using an Idempotency-Key, checking for repeats, and storing previous call results is not an “optional optimization” — it’s a necessary element of a reliable ACP integration.

Mistake #6: forgetting the link to the Product Feed.
Sometimes the ACP layer is designed “in a vacuum”: SKUs and prices are taken from internal tables that don’t match what goes into the Product Feed. As a result, ChatGPT shows one thing to the user (per the feed), while the ACP checkout runs with something else. To avoid such surprises, your SKU and pricing model must be unified: the feed, the ACP backend, and the internal database should reference the same source of truth, even if there are different projections and caches on top.

1
Task
ChatGPT Apps, level 14, lesson 3
Locked
A button that opens Instant Checkout (requestCheckout)
A button that opens Instant Checkout (requestCheckout)
1
Task
ChatGPT Apps, level 14, lesson 3
Locked
ACP create-session — the server calculates prices and supports Idempotency-Key
ACP create-session — the server calculates prices and supports Idempotency-Key
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION