Complete Order
POST https://cc.firmly.work/api/v2/payment/domains/{domain}/complete-order
Completes the order for an existing cart on the device’s session. Firmly retrieves the cart, decrypts the supplied JWE-encrypted credit card, calls the merchant’s place-order flow, and returns the order confirmation.
Authentication
x-firmly-authorization(string, required) — Device access token from Browser Session
Path Parameters
domain(string, required) — The merchant’s domain (e.g.staging.luma.gift).
Prerequisites
Before calling this endpoint:
- The cart must contain at least one line item.
- Shipping info must be set (Set Shipping Info) — the cart’s
payment_handleis only present after that. - Any consents with
required: trueandexplicit: truemust have been signed via Set Consents. Unsigned required-explicit consents are rejected with412 RequiredConsentsNotSigned.
Request Body
encrypted_card(string, required) — JWE-encrypted credit card object. Encrypt with the public key obtained from Get Public Key. Plaintext shape:
{ "number": "4111111111111111", "name": "John Smith", "verification_value": "123", "month": "08", "year": "2026" }
-
number— full PAN -
name— cardholder name as printed on the card -
verification_value— CVV / CVC / CID -
month— expiry month,1–12(string or integer) -
year— 4-digit expiry year (must not be in the past; string or integer) -
billing_info(object, required) — Billing address. Same schema as Set Shipping Info.billing_info fields
first_name(string, required) — Buyer’s first name.last_name(string, required) — Buyer’s last name.email(string, required) — Email address.phone(string, required) — Contact phone number.address1(string, required) — Primary address line.city(string, required) — City.state_or_province(string) — State / province code. Required unless the merchant has theshipping_staterelaxation enabled (seeshop_properties.optional_fieldson Get Cart) — used for countries whose address format has no state/province.country(string, required) — Country code (ISO 3166-1 alpha-2, e.g.US).postal_code(string, required) — ZIP / postal code.address2(string) — Secondary address line.company(string) — Company name.state_name(string) — Full state / province name (when the merchant requires it in addition to the code).
-
captcha_token(string) — Captcha verification token. Only required when the merchant enforces a captcha challenge.
Response
The order confirmation with the Cart API structure. Guaranteed fields include platform_order_number (merchant-assigned order number), cart_status ("submitted" on success), cart_id, line_items, the resolved shipping_info / billing_info, and the money totals (sub_total, tax, total). Additional fields vary per merchant adapter.
{"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"},"sub_total": { "currency": "USD", "value": 22, "number": 2200, "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": 26, "number": 2600, "symbol": "$" }}
Code Example
const response = await fetch('https://cc.firmly.work/api/v2/payment/domains/staging.luma.gift/complete-order',{method: 'POST',headers: {'x-firmly-authorization': accessToken,'Content-Type': 'application/json'},body: JSON.stringify({encrypted_card: encryptedCardJWE,billing_info: {first_name: 'John',last_name: 'Smith',email: 'john@example.com',phone: '+12065551212',address1: '123 Main St',city: 'Seattle',state_or_province: 'WA',country: 'US',postal_code: '98101'}})});const order = await response.json();
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." }
400 — MissingRequiredParameters
The cart is missing data needed to complete the order (e.g. no payment_handle because Set Shipping Info hasn’t been called, or the merchant adapter signaled a missing prerequisite).
{ "code": 400, "error": "MissingRequiredParameters", "description": "Cart does not have a payment handle" }
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 — 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." }
404 — CartNotFound
No active cart exists for this device on this domain — the cart was never created, or it has expired.
{ "code": 404, "error": "CartNotFound", "description": "Cart was not found." }
409 — payment_challenge_required
Not a failure — the merchant’s PSP requires interactive step-up verification (typically 3-D Secure) before the card can be authorized. The response 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.com/challenge?payment_id=...", "display": "popup", "expires_at": "2026-06-08T18:03:44.164Z" } }
409 — NotEnoughStockError
Stock has become insufficient since the cart was built — typically a race between place-order and another customer purchasing the same items.
{ "code": 409, "error": "NotEnoughStockError", "description": "Not enough stock." }
409 — ShipmentNotAvailable
Shipping method or destination is no longer available for the cart (e.g. inventory moved to a different warehouse, address fell outside the eligible region).
{ "code": 409, "error": "ShipmentNotAvailable", "description": "Shipment is not available." }
412 — OperationNotSupported
The merchant’s platform does not support this operation.
{ "code": 412, "error": "OperationNotSupported", "description": "This operation is not supported for this store." }
412 — CheckoutError
Merchant-side checkout failure that does not map to a more specific error (e.g. promotion validation failed at place-order time, cart total mismatch).
{ "code": 412, "error": "CheckoutError", "description": "Checkout error." }
412 — RequiredConsentsNotSigned
One or more required && explicit consents from Get Consents were not signed via Set Consents before this request.
{ "code": 412, "error": "RequiredConsentsNotSigned", "description": "Required consents are not signed." }
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 fails schema validation. Confirm encrypted_card is a string and billing_info includes all required fields listed above.
{ "code": 400, "error": "InvalidInputBody", "description": "The body is not processable" }
422 — CreditCardDeclined
Card was declined by the PSP for a reason that the issuer did not categorize further.
{ "code": 422, "error": "CreditCardDeclined", "description": "Credit card was declined." }
422 — CreditCardInsufficientFunds
Issuer reported insufficient funds.
{ "code": 422, "error": "CreditCardInsufficientFunds", "description": "Credit card has insufficient funds." }
422 — CreditCardInvalidNumber
Card number failed PSP validation.
{ "code": 422, "error": "CreditCardInvalidNumber", "description": "Credit card number is invalid." }
422 — CreditCardInvalidSecurityCode
verification_value (CVV) failed PSP validation.
{ "code": 422, "error": "CreditCardInvalidSecurityCode", "description": "Credit card security code is invalid." }
422 — CreditCardInvalidExpiry
The supplied expiry is a valid month/year but the card has already expired (in the past).
{ "code": 422, "error": "CreditCardInvalidExpiry", "description": "Credit card expiry is invalid." }
422 — CreditCardInvalidExpiryDate
The expiry could not be parsed — the supplied month/year are malformed or do not form a valid date (as opposed to CreditCardInvalidExpiry, which is a well-formed but past expiry).
{ "code": 422, "error": "CreditCardInvalidExpiryDate", "description": "Credit card expiry date is invalid." }
422 — CreditCardInvalidExpiryMonth
Expiry month was rejected (not 1–12).
{ "code": 422, "error": "CreditCardInvalidExpiryMonth", "description": "Credit card expiry month is invalid." }
422 — CreditCardInvalidExpiryYear
Expiry year was rejected.
{ "code": 422, "error": "CreditCardInvalidExpiryYear", "description": "Credit card expiry year is invalid." }
422 — CreditCardInvalidPostalCode
AVS rejected the postal code supplied in billing_info against the card-on-file.
{ "code": 422, "error": "CreditCardInvalidPostalCode", "description": "Credit card postal code is invalid." }
422 — PaymentChallengeRequired
The merchant’s PSP demanded interactive verification, but your App ID has not opted in to the payment-challenge contract — so the order fails closed rather than returning a challenge your client could not present. Contact Firmly to enable it. 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." }
429 — RateLimited
The payment host has rate-limited the device for excessive place-order attempts. Wait and retry with backoff.
{ "code": 429, "error": "RateLimited", "description": "Rate limited." }
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 is unavailable." }