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

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, default false) — 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 name
    • last_name (string): Buyer’s last name
    • email (string): Email address
    • phone (string): Phone number
    • address1 (string): Primary address line
    • city (string): City name
    • state_or_province (string): State/province code
    • country (string): Country code (ISO 3166-1 alpha-2, e.g. US)
    • postal_code (string): ZIP or postal code Optional fields:
    • address2 (string): Secondary address line
    • company (string): Company name
  • items (array, required) — Array of items to add to cart Item structure:

    • add_to_cart_ref (object, required): Product reference
    • variant_id (string, required): Variant identifier
    • product_id (string, optional): Product identifier
    • variant_handles (array[string], optional): Variant handle path
    • quantity (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 started
  • item-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, the data: payload wraps the error envelope under an error key — { "error": { code, error, description } } (e.g. { "error": { "code": 422, "error": "CreditCardDeclined", "description": "Credit card declined" } }). Program against data.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 references
const 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 references
response = 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);
}
}

​​ 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." }