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:
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 productscurl -X GET "https://api.firmly.work/api/v1/domains-products/staging.luma.gift?size=100" \-H "x-firmly-authorization: YOUR_TOKEN"# Get product detailscurl -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 encryptioncurl -X GET https://cc.firmly.work/api/v1/payment/key \-H "x-firmly-authorization: YOUR_TOKEN"# Complete order with encrypted card datacurl -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 joseimport * 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 flowasync function checkout(domain, productVariantId) {// 1. Authenticateconst 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 cartconst 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 shippingconst 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 methodconst 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 orderconst 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 requestsimport jsonfrom joserfc.jwe import encrypt_compactfrom joserfc.jwk import RSAKeydef 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 keycan 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. Authenticateauth_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 cartcart_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 shippingshipping_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 methodshipment = 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 expiredProductNotFound— Invalid variant IDNotEnoughStockError— Item out of stockCountryNotSupported— Shipping country not availableCreditCardDeclined— Payment failed
Next Steps
- Review Error Codes for comprehensive error handling
- Explore Addon Management for warranties and services
- Check Session Management for managing multiple carts