Place Order
POST https://cc.firmly.work/api/v2/payment/domains/{domain}/place-order
Overview
Creates a new cart and places an order in a single atomic operation. Supports complex product references including variant handles and product IDs, and the full cart feature set (shipments, addons, multi-fulfillment).
Authentication
x-firmly-authorization(string, required) — Device access token from Browser Session
Path Parameters
domain(string, required) — The merchant domain (e.g., “staging.luma.gift”)
Query Parameters
sse(string, defaultfalse) — Enable Server-Sent Events for real-time progress updates. Set to “true” to receive events.
Request Body
-
encrypted_card(string, required) — JWE-encrypted credit card data using the public key from Get Public Key endpoint -
billing_info(object, required) — Billing address information (same structure as shipping_info) -
shipping_info(object, required) — Shipping address information Required fields:first_name(string): Buyer’s first namelast_name(string): Buyer’s last nameemail(string): Email addressphone(string): Phone numberaddress1(string): Primary address linecity(string): City namestate_or_province(string): State/province codecountry(string): Country code (ISO 3166-1 alpha-2, e.g.US)postal_code(string): ZIP or postal code Optional fields:address2(string): Secondary address linecompany(string): Company name
-
items(array, required) — Array of items to add to cart Item structure:add_to_cart_ref(object, required): Product referencevariant_id(string, required): Variant identifierproduct_id(string, optional): Product identifiervariant_handles(array[string], optional): Variant handle pathquantity(number, required): Quantity to purchase (minimum: 1)
-
captcha_token(string) — Captcha verification token when required by merchant
Response
Returns an order confirmation with the Cart API structure — cart_status: "submitted", a platform_order_number, line items, addresses, and money totals.
{"cart_id": "9cf76530-1344-420f-9ca6-c7a96fb6db45","cart_status": "submitted","platform_order_number": "29185","display_name": "Luma (Staging)","platform_id": "example_commerce","shop_id": "staging.luma.gift","submitted_at": "2026-04-29T07:15:35.367Z","line_items": [{"line_item_id": "d290c5fa-0e91-462f-b53f-08d5b71ae532","sku": "WS12-XS-Orange","quantity": 1,"description": "Radiant Tee","price": { "currency": "USD", "value": 22, "number": 2200, "symbol": "$" },"line_price": { "currency": "USD", "value": 22, "number": 2200, "symbol": "$" },"msrp": { "currency": "USD", "value": 26, "number": 2600, "symbol": "$" },"image": { "url": "https://staging.luma.gift/radiant-tee.jpg", "alt": "Radiant Tee", "type": "default" }}],"shipping_info": {"first_name": "John","last_name": "Smith","email": "john@example.com","phone": "2065551212","address1": "123 Main St","city": "Seattle","state_or_province": "WA","country": "US","postal_code": "98101"},"billing_info": {"first_name": "John","last_name": "Smith","address1": "123 Main St","city": "Seattle","state_or_province": "WA","country": "US","postal_code": "98101"},"shipping_method": {"id": "standard_shipping","description": "Standard Shipping","price": { "currency": "USD", "value": 10, "number": 1000, "symbol": "$" }},"sub_total": { "currency": "USD", "value": 22, "number": 2200, "symbol": "$" },"shipping_total": { "currency": "USD", "value": 10, "number": 1000, "symbol": "$" },"tax": { "currency": "USD", "value": 2, "number": 200, "symbol": "$" },"fees": [{ "description": "Recycle fee", "currency": "USD", "value": 2, "number": 200, "symbol": "$" }],"fee_total": { "currency": "USD", "value": 2, "number": 200, "symbol": "$" },"total": { "currency": "USD", "value": 36, "number": 3600, "symbol": "$" }}
Server-Sent Events
When sse=true, the endpoint streams progress events. Each event has an event name and a data: line carrying a JSON payload:
creating-cart— Cart creation starteditem-added— Item added to cart (includes cart data)shipping-updated— Shipping info set (includes cart data)order-placed— Order successfully placed (includes order data)error— Error occurred. Unlike the non-SSE error responses, thedata:payload wraps the error envelope under anerrorkey —{ "error": { code, error, description } }(e.g.{ "error": { "code": 422, "error": "CreditCardDeclined", "description": "Credit card declined" } }). Program againstdata.error.error.
Code Example
curl --request POST \--url 'https://cc.firmly.work/api/v2/payment/domains/staging.luma.gift/place-order' \--header 'x-firmly-authorization: YOUR_ACCESS_TOKEN' \--header 'Content-Type: application/json' \--data '{"encrypted_card": "JWE_ENCRYPTED_CARD_DATA","billing_info": {"first_name": "John","last_name": "Smith","email": "john@staging.luma.gift","phone": "2065551212","address1": "123 Main St","city": "Seattle","state_or_province": "WA","country": "US","postal_code": "98101"},"shipping_info": {"first_name": "John","last_name": "Smith","email": "john@staging.luma.gift","phone": "2065551212","address1": "123 Main St","city": "Seattle","state_or_province": "WA","country": "US","postal_code": "98101"},"items": [{"add_to_cart_ref": {"variant_id": "WS12-XS-Orange","product_id": "WS12","variant_handles": ["radiant-tee", "xs-orange"]},"quantity": 1}]}'
// Place order with complex product referencesconst response = await fetch('https://cc.firmly.work/api/v2/payment/domains/staging.luma.gift/place-order', {method: 'POST',headers: {'x-firmly-authorization': authToken,'Content-Type': 'application/json'},body: JSON.stringify({encrypted_card: encryptedCard,billing_info: billingInfo,shipping_info: {first_name: 'John',last_name: 'Smith',email: 'john@staging.luma.gift',phone: '2065551212',address1: '123 Main St',city: 'Seattle',state_or_province: 'WA',country: 'US',postal_code: '98101'},items: [{add_to_cart_ref: {variant_id: 'WS12-XS-Orange',product_id: 'WS12',variant_handles: ['radiant-tee', 'xs-orange']},quantity: 1}]})});const order = await response.json();
import requests# Place order with complex product referencesresponse = requests.post('https://cc.firmly.work/api/v2/payment/domains/staging.luma.gift/place-order',headers={'x-firmly-authorization': auth_token,'Content-Type': 'application/json',},json={'encrypted_card': encrypted_card,'billing_info': billing_info,'shipping_info': {'first_name': 'John','last_name': 'Smith','email': 'john@staging.luma.gift','phone': '2065551212','address1': '123 Main St','city': 'Seattle','state_or_province': 'WA','country': 'US','postal_code': '98101',},'items': [{'add_to_cart_ref': {'variant_id': 'WS12-XS-Orange','product_id': 'WS12','variant_handles': ['radiant-tee', 'xs-orange'],},'quantity': 1,}],},)order = response.json()
SSE Example
The browser EventSource API only issues GET requests and cannot send custom headers or a request body, so it cannot be used with this endpoint. Consume the stream with fetch and read the response body directly, or use a library such as
@microsoft/fetch-event-source that adds POST/header/body support to the EventSource protocol.
// Consume the SSE stream with fetch + a streamed body reader.const response = await fetch('https://cc.firmly.work/api/v2/payment/domains/staging.luma.gift/place-order?sse=true',{method: 'POST',headers: {'x-firmly-authorization': authToken,'Content-Type': 'application/json',Accept: 'text/event-stream'},body: JSON.stringify({encrypted_card: encryptedCard,billing_info: billingInfo,shipping_info: shippingInfo,items: items})});const reader = response.body.getReader();const decoder = new TextDecoder();let buffer = '';// Parse one SSE message block (event: ... / data: ... separated by a blank line).function handleEvent(block) {let eventName = 'message';const dataLines = [];for (const line of block.split('\n')) {if (line.startsWith('event:')) eventName = line.slice(6).trim();else if (line.startsWith('data:')) dataLines.push(line.slice(5).trim());}const data = dataLines.length ? JSON.parse(dataLines.join('\n')) : null;if (eventName === 'item-added') {console.log('Item added to cart:', data.cart);} else if (eventName === 'order-placed') {console.log('Order completed:', data.order.platform_order_number);} else if (eventName === 'error') {// SSE error payload wraps the envelope: { error: { code, error, description } }console.error('Place order failed:', data.error.error, data.error.description);}}while (true) {const { value, done } = await reader.read();if (done) break;buffer += decoder.decode(value, { stream: true });// SSE messages are separated by a blank line.let sep;while ((sep = buffer.indexOf('\n\n')) !== -1) {handleEvent(buffer.slice(0, sep));buffer = buffer.slice(sep + 2);}}
Related Endpoints
- Get Public Key — Get encryption key
- Complete Order — Alternative flow with existing cart
- Add Line Item — Add items individually
Error Responses
Errors return a JSON body with code, error, and description. Program against the error value — descriptions are human-readable and may change.
400 — MissingAuthHeader
The x-firmly-authorization header is missing or empty.
{ "code": 400, "error": "MissingAuthHeader", "description": "x-firmly-authorization header is missing or invalid." }
400 — InvalidToken
The authorization token is not a valid JWT structure.
{ "code": 400, "error": "InvalidToken", "description": "Jwt token is invalid." }
401 — InvalidJWTToken
The device JWT signature does not verify, or required claims are missing.
{ "code": 401, "error": "InvalidJWTToken", "description": "Jwt token is invalid." }
404 — PartnerNotFound
The appid claim on the device JWT does not map to a known partner / tenant.
{ "code": 404, "error": "PartnerNotFound", "description": "Partner not found." }
404 — ProductNotFound
One or more add_to_cart_ref references no longer resolve at the merchant.
{ "code": 404, "error": "ProductNotFound", "description": "Product not found." }
404 — DomainNotFound
The {domain} path parameter does not match any merchant configured with Firmly, or the merchant has been disabled.
{ "code": 404, "error": "DomainNotFound", "description": "This domain was not found in firmly servers." }
409 — payment_challenge_required
Not a failure — the merchant’s PSP requires interactive step-up verification (typically 3-D Secure). The body is a challenge envelope, not the standard error shape. Get the shopper through challenge.url, then call Resume After Challenge once.
Only App IDs opted in to the challenge contract receive this; others get 422 PaymentChallengeRequired instead.
{ "type": "payment_challenge_required", "challenge": { "challenge_id": "pi_3RXkQ2...", "method": "redirect", "url": "https://merchant.example/challenge?payment_id=...", "display": "popup", "expires_at": "2026-06-08T18:03:44.164Z" } }
409 — NotEnoughStockError
Stock for one or more items dropped below the requested quantity between the catalog read and the order placement.
{ "code": 409, "error": "NotEnoughStockError", "description": "The amount of the required item is not available in stock." }
412 — OperationNotSupported
The merchant’s platform does not support cartless place-order.
{ "code": 412, "error": "OperationNotSupported", "description": "This operation is not supported for this store." }
412 — CountryNotSupported
The billing_info.country is outside the supported set (currently US and AU). Firmly rejects the order before tokenization.
{ "code": 412, "error": "CountryNotSupported", "description": "We don't currently support this country/region. Enter a new address and try again." }
400 — InvalidInputBody
Request body failed schema validation. Ensure add_to_cart_ref, encrypted_card, shipping_info, and billing_info are all present and well-formed.
{ "code": 400, "error": "InvalidInputBody", "description": "The body is not processable" }
422 — PaymentChallengeRequired
The PSP demanded interactive verification but the App ID has not opted in to the payment-challenge contract, so the order fails closed. See Resume After Challenge.
{ "code": 422, "error": "PaymentChallengeRequired", "description": "This payment requires additional bank verification (3D Secure) that is not supported in this checkout. Please use a different card." }
422 — CreditCardDeclined
PSP declined the card. See Complete Order for the full set of CreditCard* variants (insufficient funds, invalid number / CVV / expiry, AVS postal-code mismatch).
{ "code": 422, "error": "CreditCardDeclined", "description": "Credit card declined" }
503 — StoreUnavailable
The merchant’s API returned an unexpected response and the order could not be placed. Retry with backoff.
{ "code": 503, "error": "StoreUnavailable", "description": "Store temporarily unavailable. Please try again later." }