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

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_handle is only present after that.
  • Any consents with required: true and explicit: true must have been signed via Set Consents. Unsigned required-explicit consents are rejected with 412 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 the shipping_state relaxation enabled (see shop_properties.optional_fields on 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." }