Docs
Firmly Agentic Commerce
Set theme to dark (⇧+D)

Cart API Implementation Guide

​​ Cart API Guide

Build a complete checkout flow with Firmly’s Cart API. This guide walks through adding items, setting shipping, and completing payment.

​​ When to Use the Cart API

The Cart API is the canonical multi-step path through checkout. Use it for:

  • Mix of standard shipping and scheduled delivery
  • Items requiring different fulfillment types
  • Addon services (warranties, protection plans)
  • In-store pickup options
  • Complex shipping scenarios
  • Any flow where you need to mutate the cart across multiple turns (multi-product agentic flows, cart review, etc.)

If your flow is a one-shot purchase with no cart mutation between steps, 1-Step Checkout is a lighter alternative.

​​ Prerequisites

Before starting, ensure you have:

App ID Your App ID from Firmly (contact support if you don’t have one)
Test Merchant Domain Work with Firmly to enable your App ID for a specific test store
API Client cURL, Postman, or your preferred HTTP client

​​ Step 1: Authenticate

Get an access token for your session:


curl -X POST https://api.firmly.work/api/v1/browser-session \
-H "x-firmly-app-id: YOUR_APP_ID"

Use the access_token from the response as x-firmly-authorization in all API calls.

​​ Step 2: Browse Products

Discover available products from the merchant:


# List products
curl -X GET "https://api.firmly.work/api/v1/domains-products/staging.luma.gift?size=100" \
-H "x-firmly-authorization: YOUR_TOKEN"
# Get product details
curl -X GET "https://api.firmly.work/api/v1/domains-products/staging.luma.gift/radiant-tee" \
-H "x-firmly-authorization: YOUR_TOKEN"

​​ Step 3: Add to Cart

Add a product variant to the cart:


curl -X POST https://api.firmly.work/api/v2/domains/staging.luma.gift/cart/line-items \
-H "x-firmly-authorization: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"add_to_cart_ref": {
"variant_id": "WS12-XS-Orange"
},
"quantity": 1
}'

​​ Step 4: Set Shipping Address

Add shipping information and get available shipping methods:


curl -X POST https://api.firmly.work/api/v2/domains/staging.luma.gift/cart/shipping-info \
-H "x-firmly-authorization: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@staging.luma.gift",
"phone": "555-123-4567",
"address1": "123 Main St",
"city": "New York",
"state_or_province": "NY",
"postal_code": "10001",
"country": "US"
}'

The response includes a shipments[] array. Each shipment carries the shipment_id you need for the next step, along with its shipping-method options:


{
"cart_status": "active",
"shipments": [
{
"shipment_id": "0fec2a3d-ef1a-905d-c54b-46caf872356f",
"shipping_method_options": [
{
"id": "standard",
"description": "Standard",
"price": { "value": 5.00, "number": 500, "symbol": "$", "currency": "USD" }
}
]
}
]
}

​​ Step 5: Select Shipping Method

Choose a shipping method for each shipment, using the shipment_id from the previous response:


curl -X POST https://api.firmly.work/api/v2/domains/staging.luma.gift/cart/shipment/methods \
-H "x-firmly-authorization: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"shipment_id": "SHIPMENT-ID-FROM-CART",
"shipping_method_id": "standard"
}'

​​ Step 6: Complete the Order

Get the public key and complete the order with encrypted payment:


# Get public key for encryption
curl -X GET https://cc.firmly.work/api/v1/payment/key \
-H "x-firmly-authorization: YOUR_TOKEN"
# Complete order with encrypted card data
curl -X POST https://cc.firmly.work/api/v2/payment/domains/staging.luma.gift/complete-order \
-H "x-firmly-authorization: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"encrypted_card": "eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoidjEifQ...",
"billing_info": {
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@staging.luma.gift",
"phone": "555-123-4567",
"address1": "123 Main St",
"city": "New York",
"state_or_province": "NY",
"postal_code": "10001",
"country": "US"
}
}'

​​ Complete Example


// npm install jose
import * as jose from 'jose';
// Encrypt the card object into a JWE using Firmly's public JWK.
// The public key is safe to embed client-side; only Firmly's private
// key can decrypt. See the Get Public Key / Complete Order references.
async function encryptCardData(card, publicKeyJWK) {
const publicKey = await jose.importJWK(publicKeyJWK, 'RSA-OAEP-256');
return await new jose.CompactEncrypt(
new TextEncoder().encode(JSON.stringify(card))
)
.setProtectedHeader({
alg: 'RSA-OAEP-256',
enc: 'A256GCM',
kid: publicKeyJWK.kid
})
.encrypt(publicKey);
}
// Full checkout flow
async function checkout(domain, productVariantId) {
// 1. Authenticate
const authResponse = await fetch('https://api.firmly.work/api/v1/browser-session', {
method: 'POST',
headers: { 'x-firmly-app-id': 'YOUR_APP_ID' }
});
const { access_token } = await authResponse.json();
const headers = {
'x-firmly-authorization': access_token,
'Content-Type': 'application/json'
};
// 2. Add to cart
const cartResponse = await fetch(
`https://api.firmly.work/api/v2/domains/${domain}/cart/line-items`,
{
method: 'POST',
headers,
body: JSON.stringify({
add_to_cart_ref: { variant_id: productVariantId },
quantity: 1
})
}
);
const cart = await cartResponse.json();
// 3. Set shipping
const shippingResponse = await fetch(
`https://api.firmly.work/api/v2/domains/${domain}/cart/shipping-info`,
{
method: 'POST',
headers,
body: JSON.stringify({
first_name: 'John',
last_name: 'Doe',
email: 'john.doe@staging.luma.gift',
phone: '555-123-4567',
address1: '123 Main St',
city: 'New York',
state_or_province: 'NY',
postal_code: '10001',
country: 'US'
})
}
);
const cartWithShipping = await shippingResponse.json();
// 4. Select shipping method
const shipment = cartWithShipping.shipments[0];
const shippingMethod = shipment.shipping_method_options[0];
await fetch(
`https://api.firmly.work/api/v2/domains/${domain}/cart/shipment/methods`,
{
method: 'POST',
headers,
body: JSON.stringify({
shipment_id: shipment.shipment_id,
shipping_method_id: shippingMethod.id
})
}
);
// 5. Get public key and encrypt card.
// The /payment/key response IS a JWK (import it directly with jose).
const publicKeyJWK = await fetch(
'https://cc.firmly.work/api/v1/payment/key',
{ headers }
).then(r => r.json());
// Encrypt card data into a JWE using the public JWK. The JWE contract
// fields are: number, verification_value, month, year, name.
const encryptedCard = await encryptCardData(
{
number: '4111111111111111',
verification_value: '123',
month: '12',
year: '2028',
name: 'John Doe'
},
publicKeyJWK
);
// 6. Complete order
const orderResponse = await fetch(
`https://cc.firmly.work/api/v2/payment/domains/${domain}/complete-order`,
{
method: 'POST',
headers,
body: JSON.stringify({
encrypted_card: encryptedCard,
billing_info: {
first_name: 'John',
last_name: 'Doe',
email: 'john.doe@staging.luma.gift',
phone: '555-123-4567',
address1: '123 Main St',
city: 'New York',
state_or_province: 'NY',
postal_code: '10001',
country: 'US'
}
})
}
);
const order = await orderResponse.json();
return order;
}

import requests
import json
from joserfc.jwe import encrypt_compact
from joserfc.jwk import RSAKey
def encrypt_card_data(card, public_key_jwk):
"""Encrypt the card object into a JWE using Firmly's public JWK.
The public key is safe to use client-side; only Firmly's private key
can decrypt. joserfc's signature is encrypt_compact(protected, plaintext, key).
"""
public_key = RSAKey.import_key(public_key_jwk)
protected_header = {
'alg': 'RSA-OAEP-256',
'enc': 'A256GCM',
'kid': public_key_jwk['kid']
}
return encrypt_compact(
protected_header,
json.dumps(card).encode('utf-8'),
public_key
)
def checkout(domain, product_variant_id):
# 1. Authenticate
auth_response = requests.post(
'https://api.firmly.work/api/v1/browser-session',
headers={'x-firmly-app-id': 'YOUR_APP_ID'}
)
access_token = auth_response.json()['access_token']
headers = {
'x-firmly-authorization': access_token,
'Content-Type': 'application/json'
}
# 2. Add to cart
cart_response = requests.post(
f'https://api.firmly.work/api/v2/domains/{domain}/cart/line-items',
headers=headers,
json={
'add_to_cart_ref': {'variant_id': product_variant_id},
'quantity': 1
}
)
cart = cart_response.json()
# 3. Set shipping
shipping_response = requests.post(
f'https://api.firmly.work/api/v2/domains/{domain}/cart/shipping-info',
headers=headers,
json={
'first_name': 'John',
'last_name': 'Doe',
'email': 'john.doe@staging.luma.gift',
'phone': '555-123-4567',
'address1': '123 Main St',
'city': 'New York',
'state_or_province': 'NY',
'postal_code': '10001',
'country': 'US'
}
)
cart_with_shipping = shipping_response.json()
# 4. Select shipping method
shipment = cart_with_shipping['shipments'][0]
shipping_method = shipment['shipping_method_options'][0]
requests.post(
f'https://api.firmly.work/api/v2/domains/{domain}/cart/shipment/methods',
headers=headers,
json={
'shipment_id': shipment['shipment_id'],
'shipping_method_id': shipping_method['id']
}
)
# 5. Get public key (the /payment/key response IS a JWK)
key_response = requests.get(
'https://cc.firmly.work/api/v1/payment/key',
headers=headers
)
public_key_jwk = key_response.json()
# 6. Encrypt card and complete order.
# The JWE contract fields are: number, verification_value, month, year, name.
encrypted_card = encrypt_card_data(
{
'number': '4111111111111111',
'verification_value': '123',
'month': '12',
'year': '2028',
'name': 'John Doe'
},
public_key_jwk
)
order_response = requests.post(
f'https://cc.firmly.work/api/v2/payment/domains/{domain}/complete-order',
headers=headers,
json={
'encrypted_card': encrypted_card,
'billing_info': {
'first_name': 'John',
'last_name': 'Doe',
'email': 'john.doe@staging.luma.gift',
'phone': '555-123-4567',
'address1': '123 Main St',
'city': 'New York',
'state_or_province': 'NY',
'postal_code': '10001',
'country': 'US'
}
}
)
return order_response.json()

​​ Error Handling

The error envelope is { code, error, description }, where error is the PascalCase wire name from the error catalog. Common errors:

  • CartNotFound — Cart session expired
  • ProductNotFound — Invalid variant ID
  • NotEnoughStockError — Item out of stock
  • CountryNotSupported — Shipping country not available
  • CreditCardDeclined — Payment failed

​​ Next Steps