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

Authorize PayPal Express Checkout

POST https://api.firmly.work/api/v1/domains/{domain}/express/paypal/authorize

​​ Overview

Records the buyer’s PayPal approval against the cart. After Start returns an EC token, the buyer approves the payment in PayPal’s own window and PayPal hands back a payer ID. Pass both here, and the merchant attaches the approved PayPal payment to its checkout.

This call does not charge the buyer and does not place the order — Complete Order does both.

This step is optional. Complete Order performs the authorization itself when none has been recorded, so you can go straight there from Start. Call this endpoint when you want the approval confirmed as its own step, before committing to the order.

Availability and the host this endpoint runs on are covered in the PayPal Express Checkout overview.

​​ 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”)

​​ Prerequisites

  • Start must have been called on this cart, and you must have the paypal_token it returned.
  • The buyer must have approved the payment in PayPal, and you must have the payer ID PayPal returned.

​​ Request Body

  • attributes (object, required) — PayPal approval values. The object itself is required; omitting it returns 400 InvalidInputBody.
    Attributes
    • paypal_token (string, required) — The EC token from Start. Missing returns 400 BadRequest — “Missing PayPal EC Token”.
    • payer_id (string, required) — The payer ID PayPal returned when the buyer approved. Missing returns 400 BadRequest — “Missing Payer ID”.

​​ Response

Returns the cart with payment_method.attributes extended with the payer ID.

  • cart_id (string) — Unique identifier for the cart

  • line_items (array) — Array of items in the cart

  • payment_method (object) — PayPal details, now including the payer

    Payment Method
    • payment_type (string) — The gateway label; case varies by merchant
    • attributes (object)
    Attributes
    • paypal_token (string) — The EC token, unchanged
    • payer_id (string) — The payer ID you supplied, now stored on the cart
    • url (string) — The PayPal approval URL, where the merchant supplies one
    • sandbox (boolean) — Whether the merchant’s PayPal account is in sandbox mode

  • shop_properties (object) — Merchant-level configuration, including paypal.clientId

  • shipping_info (object) — Delivery address details

  • total / sub_total / tax / shipping_total (object) — Cart totals

​​ Code Example


// paypalToken came from the Start response;
// payerID came from the PayPal approval callback
const response = await fetch(
'https://api.firmly.work/api/v1/domains/staging.luma.gift/express/paypal/authorize',
{
method: 'POST',
headers: {
'x-firmly-authorization': authToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({
attributes: {
paypal_token: paypalToken,
payer_id: payerID
}
})
}
);
const cart = await response.json();
// A 200 confirms the approval was recorded — not that the payment will clear

import requests
response = requests.post(
'https://api.firmly.work/api/v1/domains/staging.luma.gift/express/paypal/authorize',
headers={
'x-firmly-authorization': auth_token,
'Content-Type': 'application/json'
},
json={
'attributes': {
'paypal_token': paypal_token,
'payer_id': payer_id
}
}
)
cart = response.json()

curl -X POST https://api.firmly.work/api/v1/domains/staging.luma.gift/express/paypal/authorize \
-H "x-firmly-authorization: YOUR_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"attributes": {
"paypal_token": "EC-XXXXXXXXXXXXXXXXX",
"payer_id": "XXXXXXXXXXXXX"
}
}'

​​ Response Example


{
"cart_id": "a8e61139-3045-470f-b211-90820e862335",
"line_items": [
{
"line_item_id": "d2d196b2-733c-457a-a7c9-001b68517807",
"sku": "MT12",
"base_sku": "MT12",
"quantity": 1,
"requires_shipping": true,
"description": "Cassius Sparring Tank",
"price": { "currency": "USD", "value": 18, "number": 1800, "symbol": "$" },
"line_price": { "currency": "USD", "value": 18, "number": 1800, "symbol": "$" },
"msrp": { "currency": "USD", "value": 21.6, "number": 2160, "symbol": "$" },
"image": {
"url": "https://staging.luma.gift/cassius-sparring-tank.jpg",
"alt": "Cassius Sparring Tank",
"type": "default"
}
}
],
"payment_method": {
"payment_type": "paypal",
"attributes": {
"paypal_token": "EC-XXXXXXXXXXXXXXXXX",
"payer_id": "XXXXXXXXXXXXX",
"url": "https://www.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=EC-XXXXXXXXXXXXXXXXX",
"sandbox": false
}
},
"payment_method_options": [
{ "type": "CreditCard", "wallet": "user" },
{ "type": "PayPal", "wallet": "paypal" }
],
"shop_properties": {
"paypal": {
"clientId": "YOUR_MERCHANT_PAYPAL_CLIENT_ID",
"sandbox": false,
"intent": "order",
"integration_version": "v2"
},
"order_status_supported": true
},
"shipping_info": {
"first_name": "John",
"last_name": "Smith",
"email": "john@example.com",
"phone": "2065551212",
"address1": "123 Main St",
"address2": "",
"city": "Seattle",
"state_or_province": "WA",
"state_name": "Washington",
"country": "US",
"postal_code": "98101"
},
"sub_total": { "currency": "USD", "value": 18, "number": 1800, "symbol": "$" },
"shipping_total": { "currency": "USD", "value": 10, "number": 1000, "symbol": "$" },
"tax": { "currency": "USD", "value": 2, "number": 200, "symbol": "$" },
"total": { "currency": "USD", "value": 30, "number": 3000, "symbol": "$" },
"cart_status": "active",
"display_name": "Luma (Staging)",
"shop_id": "staging.luma.gift"
}

​​ 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 — InvalidInputBody

The attributes object is missing from the request body.


{ "code": 400, "error": "InvalidInputBody", "description": "At path: attributes -- Expected an object, but received: undefined" }
400 — BadRequest (missing EC token)

attributes was supplied but contains no paypal_token.


{ "code": 400, "error": "BadRequest", "description": "Missing PayPal EC Token" }
400 — BadRequest (missing payer ID)

attributes.paypal_token was supplied but payer_id was not.


{ "code": 400, "error": "BadRequest", "description": "Missing Payer ID" }
400 — ErrorSubmitExpressCheckout

The merchant rejected the authorization, or the adapter failed to submit it.


{ "code": 400, "error": "ErrorSubmitExpressCheckout", "description": "Could not submit checkout." }
409 — PaymentMethodNotAvailable

PayPal is no longer selectable on this cart — commonly because the cart changed after the EC token was created.


{ "code": 409, "error": "PaymentMethodNotAvailable", "description": "The payment method is not available. Please, choose a diferent one or try again later." }
412 — PreconditionError

A required step has not been completed on this cart.


{ "code": 412, "error": "PreconditionError", "description": "A precondition was not completed." }
400 — InvalidToken

The x-firmly-authorization header is missing, empty, or not a well-formed JWT. Device-auth can also surface as MissingAuthHeader or PartnerNotFound; see Authentication errors for the full set.


{ "code": 400, "error": "InvalidToken", "description": "Jwt token is invalid." }
401 — InvalidJWTToken

The device JWT is well-formed but its signature does not verify, or required claims are missing.


{ "code": 401, "error": "InvalidJWTToken", "description": "Jwt token is invalid." }
404 — CartNotFound

No active cart exists for this device on this domain.


{ "code": 404, "error": "CartNotFound", "description": "Cart was 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 — GatewayNotFound

PayPal is not enabled as a payment gateway for this merchant. Check payment_method_options before offering it.


{ "code": 404, "error": "GatewayNotFound", "description": "Payment gateway 'paypal' not found" }
412 — OperationNotSupported

The merchant adapter does not implement PayPal express checkout.


{ "code": 412, "error": "OperationNotSupported", "description": "This operation is not supported for this store." }
503 — StoreUnavailable

The merchant’s own API was unavailable or timed out. Retry with backoff.


{ "code": 503, "error": "StoreUnavailable", "description": "Store temporarily unavailable. Please try again later." }
429 — RateLimited

Too many requests from this device. Back off and retry after the Retry-After window — see Rate Limits.


{ "code": 429, "error": "RateLimited", "description": "Too many requests. Please try again later." }