Checkout Handoff

Hand a shopper from your headless storefront to Blaze-hosted checkout, carrying the cart, the signed-in customer, and the delivery context across in a single redirect.

What you'll learn

  • When to hand checkout to Blaze instead of building it yourself
  • How to mint a short-lived handoff token for an authenticated shopper
  • How to discover the storefront checkout URL for a store
  • How to build the handoff URL and encode delivery context
  • What happens on the Blaze side when the shopper lands
  • The constraints that matter: token TTL, authentication requirement, and cart ownership

Prerequisites

  • A Store UUID (staging: e87437f2-3e35-4738-af5e-6307e368255c)
  • A signed-in shopper — you hold their JWT from POST /api/v1/auth/login. See the Authentication guide
  • A cart built against that shopper. See the Cart & Checkout guide
  • The store must have a published storefront site (a Blaze-hosted site with a resolvable domain)

When to use this

You have two ways to finish an order.

Full headless checkout Checkout handoff
Who renders checkout You Blaze
Payment provider integration You integrate each provider Handled by Blaze
ID / age verification, compliance gates You implement Handled by Blaze
Order creation You call POST /api/v4/orders Blaze calls it
Branding at checkout Yours The store's Blaze storefront theme
Effort High One redirect

Hand off when the storefront experience is the value you're adding and you'd rather not carry payment integrations, identity verification, and per-market compliance rules. Keep it headless when checkout itself is the thing you're differentiating on.

The handoff is a redirect, not an embed. The shopper leaves your domain and finishes on the store's Blaze storefront. Plan your analytics and post-order experience around that.


How it works

Your storefront                     Blaze API                    Blaze storefront
──────────────                      ─────────                    ────────────────
1. Shopper signs in    ──────────▶  POST /api/v1/auth/login
                       ◀──────────  session JWT

2. Build the cart      ──────────▶  POST /api/v5/carts
                       ◀──────────  cart_uuid

3. Get the site URL    ──────────▶  GET  /api/v1/store/site
                       ◀──────────  https://shop.example.com/

4. Mint handoff token  ──────────▶  POST /api/v1/users/me/access_token
                       ◀──────────  access_token (5 min TTL)

5. Redirect  ─────────────────────────────────────────────────▶  /checkout/{cart_uuid}/
                                                                  ?access_token=…

                                                              6. Exchanges the token
                                                                 for a session,
                                                                 loads the cart,
                                                                 strips the params

                                                              7. Shopper pays and
                                                                 the order is created

Step 1: Authenticate the shopper

The handoff carries an identity, so the shopper must be signed in on your storefront first. Use the standard login flow and keep the returned JWT — every subsequent step uses it.

const STORE_UUID = "e87437f2-3e35-4738-af5e-6307e368255c";
const BASE_URL = "https://ecom-api.staging.blaze.me";

const headers = {
  "Content-Type": "application/vnd.api+json",
  Accept: "application/vnd.api+json",
  "X-Store": STORE_UUID,
};

const loginRes = await fetch(`${BASE_URL}/api/v1/auth/login`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    data: {
      type: "users",
      attributes: { email: "john@example.com", password: "securepassword123" },
    },
  }),
});

const { data: session } = await loginRes.json();
const sessionJwt: string = session.attributes.token;

Guest carts cannot be handed off. The Blaze checkout page requires a signed-in customer when a cart UUID is present in the path — an anonymous shopper is redirected to the login screen instead of the checkout. See Constraints below.


Step 2: Build the cart

Create the cart with the shopper's JWT so it is bound to their account. Anything you set here — items, delivery specification, promo codes, rewards — travels with the cart and is what the shopper sees at checkout.

const authHeaders = { ...headers, Authorization: `Bearer ${sessionJwt}` };

const cartRes = await fetch(`${BASE_URL}/api/v5/carts`, {
  method: "POST",
  headers: authHeaders,
  body: JSON.stringify({
    data: {
      type: "carts",
      attributes: {
        delivery_specification: "pickup",
        inventory_type: "recreational",
        item: { product_id: "PRODUCT_UUID", quantity: 1 },
      },
    },
  }),
});

const { data: cart } = await cartRes.json();
const cartUuid: string = cart.id;

See the Cart & Checkout guide for adding items, delivery specifications, promo codes, and rewards.


Step 3: Discover the checkout base URL

Each store has its own storefront domain. Read it from the store's site resource rather than hardcoding it — domains differ per store and change without notice.

GET /api/v1/store/site

cURL

curl https://ecom-api.staging.blaze.me/api/v1/store/site \
  -H "Accept: application/vnd.api+json" \
  -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c"

TypeScript

const siteRes = await fetch(`${BASE_URL}/api/v1/store/site`, { headers });
const { data: site } = await siteRes.json();

// Always returned with a trailing slash, e.g. "https://shop.example.com/"
const siteUrl: string = site.attributes.url;

Response

{
  "data": {
    "id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
    "type": "store_sites",
    "attributes": {
      "url": "https://shop.example.com/",
      "basepath": "",
      "pathname": "",
      "logo_url": "https://blaze.imgix.net/...",
      "favicon_url": "https://blaze.imgix.net/...",
      "html_title": "Example Dispensary",
      "meta_description": "Order cannabis online for pickup or delivery."
    }
  }
}

The url attribute always ends in a trailing slash. Strip it before appending the checkout path, or you will produce a double slash.

If the store has no published site, url is null — there is nowhere to hand off to, and you must complete checkout headlessly.


Step 4: Mint a handoff token

The handoff token is a short-lived credential the shopper's browser presents to the Blaze storefront to resume their session there. You mint it with the shopper's own JWT, so no partner-level privilege is involved — it is the shopper delegating their session to the next page.

POST /api/v1/users/me/access_token

cURL

curl -X POST https://ecom-api.staging.blaze.me/api/v1/users/me/access_token \
  -H "Content-Type: application/vnd.api+json" \
  -H "Accept: application/vnd.api+json" \
  -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \
  -H "Authorization: Bearer YOUR_SESSION_JWT" \
  -d '{"data": {"type": "user_access_tokens", "attributes": {}}}'

TypeScript

const tokenRes = await fetch(`${BASE_URL}/api/v1/users/me/access_token`, {
  method: "POST",
  headers: authHeaders,
  body: JSON.stringify({
    data: { type: "user_access_tokens", attributes: {} },
  }),
});

const { data: token } = await tokenRes.json();
const handoffToken: string = token.attributes.access_token;

Response

{
  "data": {
    "id": "c3a1f5d2-8b4e-4f6a-9c2d-1a3b5c7d9e0f",
    "type": "user_access_tokens",
    "attributes": {
      "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
    }
  }
}

The token expires 5 minutes after it is issued. Mint it at the moment the shopper clicks "Checkout" — immediately before you build the redirect. Do not mint it when the cart is created, cache it, or reuse it across sessions.


Step 5: Build the handoff URL

The checkout path is /checkout/{cart_uuid}/, appended to the site URL. Everything else travels as query parameters.

Query parameters

Parameter Required Values Purpose
access_token Yes Handoff token from Step 4 Signs the shopper in on the Blaze storefront
delivery_type Recommended pickup, delivery Pre-selects the fulfilment type
delivery_mode Delivery only asap, scheduled, express Pre-selects the delivery mode
delivery_address Delivery only Base64-encoded JSON Pre-fills the delivery address
utm_source Optional Any string Attribution — identify your storefront

Any additional query parameters you add are preserved on the page, so you can carry your own attribution or session correlation values across.

Encoding the delivery address

delivery_address is a base64-encoded JSON object using snake_case keys:

interface HandoffAddress {
  address: string;          // "1234 Market St"
  address_line2?: string;   // "Apt 5"
  city: string;             // "San Francisco"
  state: string;            // "CA"
  zip_code: string;         // "94103"
  country?: string;         // "US"
  building_number?: string;
  lat?: number;             // 37.7749
  lng?: number;             // -122.4194
}

const encodeAddress = (address: HandoffAddress): string =>
  Buffer.from(JSON.stringify(address), "utf-8").toString("base64");

In the browser, use btoa(JSON.stringify(address)) instead. An address that fails to decode is ignored rather than raising an error — the shopper is simply asked to enter it again, so validate the shape on your side.

Building the URL

function buildHandoffUrl(opts: {
  siteUrl: string;
  cartUuid: string;
  handoffToken: string;
  deliveryType: "pickup" | "delivery";
  deliveryMode?: "asap" | "scheduled" | "express";
  deliveryAddress?: HandoffAddress;
  utmSource?: string;
}): string {
  const base = opts.siteUrl.replace(/\/$/, "");
  const params = new URLSearchParams({
    access_token: opts.handoffToken,
    delivery_type: opts.deliveryType,
  });

  if (opts.utmSource) params.set("utm_source", opts.utmSource);

  if (opts.deliveryType === "delivery") {
    if (opts.deliveryMode) params.set("delivery_mode", opts.deliveryMode);
    if (opts.deliveryAddress) {
      params.set("delivery_address", encodeAddress(opts.deliveryAddress));
    }
  }

  return `${base}/checkout/${opts.cartUuid}/?${params.toString()}`;
}

Result

https://shop.example.com/checkout/8f14e45f-ceea-467a-9f0e-8b7c1d2a3f4b/
  ?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
  &delivery_type=delivery
  &delivery_mode=scheduled
  &delivery_address=eyJhZGRyZXNzIjoiMTIzNCBNYXJrZXQgU3QiLCJjaXR5IjoiU2FuIEZyYW5jaXNjbyJ9
  &utm_source=partner-storefront

Redirect the shopper's browser to it — a top-level navigation, not a fetch. Because the token is in the URL, use a 303 See Other server-side redirect or window.location.assign(); do not render it as a visible link the shopper might copy or share.


Step 6: What happens on the Blaze side

You do not implement any of this — it's what the storefront does with what you sent, and it's useful to know when debugging a handoff.

  1. Token exchange. The storefront reads access_token from the query string and posts it to POST /api/v1/auth/login as {"data": {"type": "users", "attributes": {"access_token": "..."}}}. The API validates the short-lived token and returns a full session JWT. This is why the 5-minute TTL is not a limit on the shopper's checkout time — only on the gap between minting the token and landing on the page.

  2. Parameter stripping. Once consumed, access_token, delivery_address, delivery_type, and delivery_mode are removed from the URL via a history replace, so the token does not survive in browser history or in a shared link.

  3. Delivery context applied. The decoded address, delivery type, and delivery mode are set as the shopper's active selections and persisted to local storage.

  4. Cart load. The storefront reads {cart_uuid} from the path, stores it as the active cart, and fetches it via GET /api/v5/carts/{uuid}.

  5. Checkout. The shopper proceeds through payment, any identity or age verification the store requires, and order creation — all on the Blaze side.

If the token has expired or is malformed, the exchange fails and the shopper is sent to the storefront's login page with a redirect_uri back to the checkout. They can sign in manually and their cart is still there, so an expired token degrades to an extra login rather than a lost cart.


Complete example

const STORE_UUID = "e87437f2-3e35-4738-af5e-6307e368255c";
const BASE_URL = "https://ecom-api.staging.blaze.me";

const jsonApi = {
  "Content-Type": "application/vnd.api+json",
  Accept: "application/vnd.api+json",
  "X-Store": STORE_UUID,
};

/**
 * Builds a checkout handoff URL for an already-authenticated shopper.
 * Call this at the moment the shopper clicks "Checkout" — the token is
 * only valid for 5 minutes.
 */
async function createCheckoutHandoff(opts: {
  sessionJwt: string;
  cartUuid: string;
  deliveryType: "pickup" | "delivery";
  deliveryMode?: "asap" | "scheduled" | "express";
  deliveryAddress?: HandoffAddress;
}): Promise<string> {
  const authed = { ...jsonApi, Authorization: `Bearer ${opts.sessionJwt}` };

  const [siteRes, tokenRes] = await Promise.all([
    fetch(`${BASE_URL}/api/v1/store/site`, { headers: jsonApi }),
    fetch(`${BASE_URL}/api/v1/users/me/access_token`, {
      method: "POST",
      headers: authed,
      body: JSON.stringify({
        data: { type: "user_access_tokens", attributes: {} },
      }),
    }),
  ]);

  if (!siteRes.ok) throw new Error(`Site lookup failed: ${siteRes.status}`);
  if (!tokenRes.ok) throw new Error(`Token mint failed: ${tokenRes.status}`);

  const { data: site } = await siteRes.json();
  const { data: token } = await tokenRes.json();

  const siteUrl: string | null = site.attributes.url;
  if (!siteUrl) {
    throw new Error("Store has no published storefront — cannot hand off.");
  }

  return buildHandoffUrl({
    siteUrl,
    cartUuid: opts.cartUuid,
    handoffToken: token.attributes.access_token,
    deliveryType: opts.deliveryType,
    deliveryMode: opts.deliveryMode,
    deliveryAddress: opts.deliveryAddress,
    utmSource: "partner-storefront",
  });
}

Wire it to a server-side redirect so the token never lands in client-side application state:

// Express example
app.post("/checkout", async (req, res) => {
  const url = await createCheckoutHandoff({
    sessionJwt: req.session.blazeJwt,
    cartUuid: req.session.cartUuid,
    deliveryType: "pickup",
  });

  res.redirect(303, url);
});

After the handoff

The shopper completes the order on the Blaze storefront and stays there — there is no automatic redirect back to your domain.

You can still follow the order from your side. The cart UUID you handed over links to the resulting order:

// Poll or check on return — uses the shopper's session JWT
const orderRes = await fetch(
  `${BASE_URL}/api/v1/carts/${cartUuid}/order`,
  { headers: authed }
);

For push-based updates instead of polling, see the Webhooks guideorder.created and order status events fire regardless of which surface created the order.


Constraints

Read these before committing to the handoff as your checkout strategy.

The shopper must be authenticated. A cart UUID in the checkout path requires a signed-in customer. If the token is missing or expired, the storefront redirects to login rather than offering guest checkout. Guest checkout exists on the Blaze storefront, but not on the cart-UUID entry path — so an anonymous cart built by your storefront cannot be handed off. Authenticate the shopper before you build the cart, not after.

The handoff token is a full user credential. For its 5-minute life it authenticates as that shopper, and it is not restricted to the cart you are handing over or to a single use. Treat it like a password:

  • Mint it immediately before the redirect, never in advance
  • Never log it, store it, or put it in an analytics payload
  • Only ever transmit it over TLS
  • Never render it in a link the shopper can copy or share

Cart UUIDs are bearer capabilities. A cart is fetched by UUID scoped to the store, with no ownership check. Anyone holding a cart UUID can read and modify that cart. Do not expose cart UUIDs in shareable URLs, referrer-visible contexts, or third-party analytics.

Delivery stores may use a different site. Stores configured with a separate delivery storefront resolve to a different domain for delivery orders. If a store uses one, use that domain when delivery_type=deliveryGET /api/v1/store/site returns the store's primary site.

Checkout branding is the store's, not yours. The shopper sees the store's Blaze storefront theme. If a seamless brand transition matters more than the integration savings, build checkout headlessly instead.


Endpoint summary

Step Endpoint Auth
Sign the shopper in POST /api/v1/auth/login None
Build the cart POST /api/v5/carts Shopper JWT
Find the storefront URL GET /api/v1/store/site None
Mint the handoff token POST /api/v1/users/me/access_token Shopper JWT
Follow the resulting order GET /api/v1/carts/{cart_uuid}/order Shopper JWT

What's Next?

For request/response format details, see the General Concepts guide. For authentication setup, see the Authentication guide.