Guide

Add a cart and checkout to an Astro site

Astro owns every pixel of the storefront. Neuron Cart owns cart state, checkout, payments, tax, shipping, and orders, all server-side. Your Astro site talks to it through its own API routes, so the key never reaches the browser and the product pages stay static.

Updated Sep 28, 2026

The fast path: scaffold it

One command generates everything the rest of this guide describes as working code: the SDK client, the cart cookie, the API-route proxy, a developer-mode banner, and a full checkout.

npm create neuron-astro-store@latest my-store
cd my-store && npm install && npm run dev

The CLI prompts for an API key, or takes --key, and points you at signup if you don't have one. Product pages are prerendered, so they ship as static HTML off the CDN. Only the cart costs a request.

Read on if you'd rather wire it by hand, or to understand what the template generated.

1. Get a store and a key

Nothing to deploy. Two minutes.

  1. Sign up at cart-admin.neuroncommerce.com/signup.
  2. Copy the cs_live_… API key from the one-time reveal.

Your store starts in developer mode: a built-in sandbox validates products with the prices you send, shipping is free, and payments are mocked. A complete checkout works before you configure anything.

# .env  (server-side only — Astro exposes nothing without the PUBLIC_ prefix,
# and this key must NEVER get one)
NEURON_CART_API=https://cart-api.neuroncommerce.com/v1
NEURON_CART_KEY=cs_live_xxxxxxxxxxxxxxxxxxxxxxxx

The API key stays on the server. In Astro that means using it only in .astro frontmatter (SSR), API routes under src/pages/api/, or middleware. Never in a client:* island or a PUBLIC_-prefixed variable. Browser code talks to your API routes; your API routes talk to Neuron Cart.

Any Astro output mode works for the pages. output: 'server', or hybrid with the cart routes opted out of prerendering, is required for the API-route proxy below.

2. The client

Install the SDK, a typed wrapper over the REST API with no dependencies:

npm install @neuron-cart/sdk
// src/lib/neuron.ts
import { createNeuronCart } from '@neuron-cart/sdk';

export const neuron = createNeuronCart({
  apiUrl: import.meta.env.NEURON_CART_API,
  apiKey: import.meta.env.NEURON_CART_KEY,
});

Methods return the endpoint's payload directly and throw NeuronCartError, carrying the API's code, on failure. No envelope unwrapping.

const cart = await neuron.carts.create();
await neuron.carts.addItem(cart.id, {
  sku: 'SPACE-TEE',
  quantity: 1,
  metadata: { unitPriceCents: '2499', name: 'Space Tee' },
});

Prefer raw fetch? Everything below works the same way. The SDK is a convenience, not a requirement, and neuron.request()is the escape hatch for endpoints it doesn't wrap yet.

3. Cart session: one cookie

POST /v1/carts returns the cart with a sessionToken. Persist it in an httpOnly cookie and re-fetch the cart with GET /v1/carts/session/:sessionToken. That's the whole session model for anonymous shoppers.

// src/pages/api/cart/index.ts — GET current cart (creating on first touch)
import type { APIRoute } from 'astro';
import { neuron } from '../../../lib/neuron';

export const GET: APIRoute = async ({ cookies }) => {
  const token = cookies.get('nc_cart')?.value;
  if (token) {
    try {
      return Response.json(await neuron.carts.getBySession(token));
    } catch {
      // Expired, or belongs to another store — fall through to a new cart.
    }
  }
  const cart = await neuron.carts.create();
  cookies.set('nc_cart', cart.sessionToken, {
    path: '/',
    httpOnly: true,
    sameSite: 'lax',
    secure: true,
    maxAge: 60 * 60 * 24 * 30,
  });
  return Response.json(cart);
};

4. Add to cart: your catalog rides along

In developer mode your Astro site is the pricing authority. Pass the product inline via metadata. All values are strings, and price is integer cents. When you go live, the same call keeps working. The metadata is simply ignored in favor of your product-validate webhook's answer.

// src/pages/api/cart/items.ts
import type { APIRoute } from 'astro';
import { neuron } from '../../../lib/neuron';

export const POST: APIRoute = async ({ request, cookies }) => {
  const token = cookies.get('nc_cart')?.value;
  if (!token) return Response.json({ error: 'No cart' }, { status: 409 });

  const cart = await neuron.carts.getBySession(token);
  const { sku, quantity } = await request.json();

  // Look the price up SERVER-SIDE from your catalog — never let the browser
  // post its own price, even while the sandbox trusts inline pricing.
  const product = findProduct(sku);
  if (!product) return Response.json({ error: 'Unknown SKU' }, { status: 400 });

  const updated = await neuron.carts.addItem(cart.id, {
    sku: product.sku,
    quantity,
    metadata: { unitPriceCents: String(product.priceCents), name: product.name },
  });
  return Response.json(updated);
};

An add-to-cart button is then a plain form or a few lines of client JS. No framework island required.

<button data-add sku="SPACE-TEE">Add to cart</button>
<script>
  document.querySelectorAll('[data-add]').forEach((btn) =>
    btn.addEventListener('click', async () => {
      await fetch('/api/cart/items', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ sku: btn.getAttribute('sku'), quantity: 1 }),
      });
      document.dispatchEvent(new CustomEvent('cart:changed'));
    })
  );
</script>

5. The developer-mode banner

Do this early. Every cart response from a sandboxed store carries developerMode: true. Render a persistent banner from it. It's the signal that prices are client-supplied and payments are mocked, and it disappears by itself once the store leaves the sandbox.

{cart.developerMode && (
  <div class="dev-banner" role="status">
    Developer mode — sandbox pricing, nothing is charged.
  </div>
)}

6. Checkout: six calls

All against your server-side proxy, in order. Addresses are flat field sets, not nested objects. Money is integer cents.

1. POST /v1/checkout/sessions                        { "cartId": "<id>" }
2. PUT  /v1/checkout/sessions/:cartId/shipping-address
        { "firstName","lastName","line1","city","state","postalCode",
          "country":"US", "email" }                  // flat — NOT nested
3. GET  /v1/checkout/sessions/:cartId/shipping-rates → { rates: [{id,name,amount}] }
4. PUT  /v1/checkout/sessions/:cartId/shipping-rate  { "rateId","rateName","amount" }
5. POST /v1/checkout/sessions/:cartId/payment-intent {}
        → data.paymentIntent.id + data.clientConfig
6. POST /v1/checkout/sessions/:cartId/confirm
        { "paymentIntentId", "billingAddress": {…flat…}, "email" }
        → data.order with orderNumber "ORD-YYYY-NNNNN" // email REQUIRED for guests

With the SDK, the same six steps read like this:

await neuron.checkout.initiate(cart.id);
await neuron.checkout.setShippingAddress(cart.id, {
  firstName: 'Ada', lastName: 'Lovelace', line1: '1 Fremont St',
  city: 'Las Vegas', state: 'NV', postalCode: '89101', email: 'ada@example.com',
});

const { rates } = await neuron.checkout.getShippingRates(cart.id);
await neuron.checkout.setShippingRate(cart.id, {
  rateId: rates[0].id, rateName: rates[0].name, amount: rates[0].amount,
});

const { paymentIntent } = await neuron.checkout.createPaymentIntent(cart.id);
const order = await neuron.checkout.confirm(cart.id, {
  paymentIntentId: paymentIntent.id,
  billingAddress: { /* … */ },
  email: 'ada@example.com',            // required for guest checkout
});

order.orderNumber; // ORD-2026-00001

In developer mode step 5 needs no card UI at all. Confirm immediately and you'll get a real order row in your admin. When you configure Stripe or Authorize.net later, the clientConfig from step 5 tells you which payment form to mount. The six-call sequence itself never changes.

7. Going live

Four sandbox defaults, and what replaces each one.

Sandbox defaultReplace with
Built-in validate (client prices)Your product.validate webhook. Your server becomes the pricing authority.
Mock payment providerStripe, Authorize.net, or PayPal in Admin → Payments.
Free shippingReal shipping mode, flat or carrier rates, in Admin → Shipping.
developerMode: true on cartsCleared when the sandbox config is replaced. Your banner vanishes on its own.

Customer accounts and text-login (a phone number plus a texted code, no password) are the same headless calls under /v1/auth/*, proxied the same way. The scaffolded store opens checkout by asking for a phone number for exactly that reason: one code identifies the shopper and satisfies the fraud gate in one motion.

Common questions

Does this work with a fully static Astro site?

The product pages, yes. Keep output: 'static'for everything you can, and put the handful of cart routes on a server, either Astro's hybrid mode or any small function host. The cart runs as a service, not a build step, so a static catalog and a live cart are the normal shape.

Do I need a database?

No. Cart state, checkout sessions, customers, and orders live in Neuron Cart. Your Astro site needs a product source, which can be a content collection, a CMS, or a JSON file.

What does it cost?

Nothing while you build. Every new store starts on the developer plan with the whole feature surface and no card. General-availability pricing hasn't been announced.