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_tokenit 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 returns400 InvalidInputBody.Attributes
paypal_token(string, required) — The EC token from Start. Missing returns400 BadRequest— “Missing PayPal EC Token”.payer_id(string, required) — The payer ID PayPal returned when the buyer approved. Missing returns400 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 payerPayment Method
payment_type(string) — The gateway label; case varies by merchantattributes(object)
Attributes
paypal_token(string) — The EC token, unchangedpayer_id(string) — The payer ID you supplied, now stored on the carturl(string) — The PayPal approval URL, where the merchant supplies onesandbox(boolean) — Whether the merchant’s PayPal account is in sandbox mode
-
shop_properties(object) — Merchant-level configuration, includingpaypal.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 callbackconst 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 requestsresponse = 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"}
Related Endpoints
- Start — Open the PayPal session
- Complete Order — Place the order
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." }