View Categories

REST API: add-on data in the Store API checkout

19 min read

The add-ons put their own data on WooCommerce’s Store API, the public API a headless storefront or a mobile app uses for the cart and the checkout. This page lists what each add-on adds under extensions, which fields a client must send so the order is accepted, and the errors an order is refused with. It continues Adding to the cart and creating orders, which covers the rental dates on cart/add-item, the Cart-Token and placing an order; none of that is repeated here. The setup and the conventions shared by every endpoint (keys, errors, pagination) are in REST API overview and authentication.

Base URL and sign-in #

All of it lives in WooCommerce’s Store API, https://example.com/wp-json/wc/store/v1/. Each add-on owns one namespace under extensions.

NamespaceWhere it appearsNeeds
sicustomfieldsCart (GET /cart), checkout requestCustom Fields, Damage Waiver & ID Verification
sicustomfields-waiverCart, POST /cart/extensionsCustom Fields, Damage Waiver & ID Verification
sipdfsignCheckout requestRental Contract Signing & PDF Generation
sidistanceCart, POST /cart/extensionsDelivery by Distance
sibooking-deliveryCart, POST /cart/extensionsSmart Routing (rental stops also need Rentals & Bookings Regular)
sisub-store-creditCart, POST /cart/extensionsSubscriptions & Memberships
sibooking_stayEach cart item (items[].extensions)Rentals & Bookings (Lite or Regular)
  • The Store API needs no API key. A guest cart is identified by the Cart-Token header, as described in Adding to the cart and creating orders: send the token you got from the first response on every call.
  • A request made as a signed-in customer (cookie, or an Application Password) must carry the Nonce header WooCommerce returns in its responses. Without it WooCommerce answers 401 with woocommerce_rest_missing_nonce. The same header is how a page of the store itself calls the API: it is printed as wcSettings.storeApiNonce.
  • Send bodies as JSON (Content-Type: application/json). Amounts are strings in the currency’s minor unit (cents), as for every WooCommerce cart.
  • Errors use the WordPress shape {"code", "message", "data": {"status"}}.

Who may do what #

The Store API is public, so none of this needs a capability. Two rules decide what a client sees:

RuleEffect
Store credit belongs to an accountsisub-store-credit shows an amount and accepts use only for a signed-in customer with credit left. A guest always reads available: "0.00"
Questions follow the cartsicustomfields lists only the questions that apply to the signed-in customer (roles) and to the cart’s contents (products, categories, field groups, conditions)

How the extension data works #

Three calls carry add-on data:

  • Read. GET /wc/store/v1/cart returns extensions.<namespace> for the cart (and items[].extensions.<namespace> for a cart line).
  • Change the cart. POST /wc/store/v1/cart/extensions takes namespace and data. The add-on stores the choice in the customer’s session and WooCommerce works out the cart again and returns it, so a fee or a price moves at once.
  • Place the order. POST /wc/store/v1/checkout takes extensions.<namespace> beside the addresses and payment_method. The add-on checks the values and refuses the order when something is missing.

To see exactly what a store accepts, ask the API for its schema: OPTIONS /wc/store/v1/checkout lists every namespace the checkout request may carry under schema.properties.extensions.properties.

curl -X OPTIONS https://example.com/wp-json/wc/store/v1/checkout

A cart update with an unknown namespace is answered 400 with woocommerce_rest_cart_extensions_error (“There is no such namespace registered”).

curl -X POST https://example.com/wp-json/wc/store/v1/cart/extensions \
  -H "Content-Type: application/json" \
  -H "Cart-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -d '{"namespace": "sicustomfields-waiver", "data": {"accepted": false}}'

Custom fields, damage waiver and ID verification #

Needs Custom Fields, Damage Waiver & ID Verification. Three kinds of checkout question exist, and they travel differently.

KindWhich questionsWhere the client sends the answer
NativeUnpriced questions of a simple type (text, text area, email, phone, URL, number, VAT, date, select, radio, checkbox)WooCommerce’s own additional_fields object of the checkout request, keyed sicf/<field key>
BlockPriced questions, file uploads, multiple choices (multi-select, multiple checkboxes), time, color and password questionsextensions.sicustomfields.answers
Identity documentThe ID upload, when the cart needs oneextensions.sicustomfields.id

Headings and messages are display only: they take no answer.

GET /wc/store/v1/cart: extensions.sicustomfields #

What to ask for this cart. It changes with the cart and the customer.

FieldNotes
fieldsArray of strings: the keys of the native questions that apply to this cart. WooCommerce 9.9 and newer use it to show or hide those fields
block.fieldsThe block questions for this cart, in checkout order (the table below)
block.nativeThe native questions, each with key, id (sicf/<key>), location (contact, address or order) and conditions
block.answersObject: the answers kept in the session so far, key to value. Files are not listed
block.idThe ID document settings, or null when this cart needs none (the table below)

Each entry of block.fields:

FieldNotes
keyThe question’s key: the key of answers
typetext, textarea, email, tel, url, number, password, select, multiselect, radio, checkbox, multicheckbox, date, time, color, file, heading, message, vat
label, description, placeholderText in the customer’s language. description is HTML the shop wrote
requiredBoolean
defaultString, or an array of strings for multiselect and multicheckbox
optionsArray of {value, label}. The label includes the price (“Extra driver (+$15.00)”)
conditionsThe rules that show the question: {relation, rules}
pricedTrue when the answer changes the total
readsFor a formula price: the keys of the questions the formula reads
width, min, max, maxlengthLayout and limits
accept, maxBytes, maxLabelFiles only: allowed extensions (.jpg,.pdf), size limit in bytes and as text

block.id:

FieldNotes
requiredAn ID is required to place the order
onFileThe signed-in customer already has a document on file; none is needed again
typesArray of {value, label, back}: the accepted document types, and whether this type has a back side
backThe shop asks for the back side of types that have one
expiryThe shop asks for the expiry date
minAgeMinimum age in years, 0 for none. Above 0 the date of birth is required
accept, maxBytes, maxLabelAllowed file types and the size limit
"sicustomfields": {
  "fields": [],
  "block": {
    "fields": [
      { "key": "extra_driver", "type": "select", "label": "Extra driver", "required": false, "priced": true,
        "options": [ { "value": "yes", "label": "Add a driver (+$15.00)" } ], "...": "..." }
    ],
    "native": [],
    "answers": {},
    "id": { "required": true, "onFile": false, "types": [ { "value": "drivers_license", "label": "Driver's license", "back": true } ],
            "back": true, "expiry": true, "minAge": 21, "accept": ".jpg,.jpeg,.png,.webp,.pdf", "maxBytes": 5242880, "maxLabel": "5 MB" }
  }
}

POST /wc/store/v1/cart/extensions (namespace sicustomfields) #

Keeps the block answers in the session, so a priced answer moves the cart’s fees before checkout. Optional: the checkout request carries the answers again.

Field in dataNotes
answersObject, key to answer. Only questions of block.fields are read; other keys are ignored

Answer values: a string for most types; an array of strings for multiselect and multicheckbox; for a file, the upload token (below). Answers are cleaned as the classic checkout cleans them. A file answer with an unknown or foreign token counts as no file. The response is the whole cart.

POST /wc/store/v1/checkout: extensions.sicustomfields #

FieldNotes
answersObject, key to answer: every block question the cart asks. Send it with the request even if you sent it to the cart before
idObject, the identity document (below). Leave it out when block.id is null, or when the customer has a document on file

id:

FieldNotes
typeA value from block.id.types
nameName as shown on the document. Optional
expiryYYYY-MM-DD. Required when block.id.expiry is true; an expired date is refused
dobYYYY-MM-DD. Required when block.id.minAge is above 0
token_frontUpload token of the front photo
token_backUpload token of the back photo. Required when block.id.back is true and the type has a back
curl -X POST https://example.com/wp-json/wc/store/v1/checkout \
  -H "Content-Type: application/json" \
  -H "Cart-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -d '{
    "billing_address": { "first_name": "Sarah", "last_name": "Miller", "address_1": "12 Example Street", "city": "Springfield", "state": "CA", "postcode": "90210", "country": "US", "email": "[email protected]" },
    "payment_method": "bacs",
    "additional_fields": { "sicf/driver_name": "James Cooper" },
    "extensions": {
      "sicustomfields": {
        "answers": { "extra_driver": "yes", "licence_scan": "3f1b0c9a5d7e4a2b8c6d1e0f9a8b7c6d" },
        "id": { "type": "drivers_license", "expiry": "2030-05-01", "dob": "1990-03-14",
                "token_front": "a1b2c3d4e5f60718293a4b5c6d7e8f90", "token_back": "0f9e8d7c6b5a49382716f5e4d3c2b1a0" }
      }
    }
  }'

Uploading a file or an ID photo #

A file answer and an ID photo are not sent in the checkout request. The client uploads the file first, gets a token, and sends the token as the answer. The upload is a WordPress AJAX call, not a Store API route:

POST https://example.com/wp-admin/admin-ajax.php, as multipart/form-data.

UploadactionOther fields
A checkout file questionsicf_field_upload_ajax_nonce, field (the question’s key), product = 0, file
An ID photosicf_id_upload_ajax_nonce, side (front or back), file

The nonce is not part of the Store API. The store’s Checkout block page prints it in its script data: wcSettings setting sicustomfields_data, keys uploadNonce (files) and idNonce (ID), with uploadAction and idAction naming the actions. Uploads are limited to 8 a minute per client address.

The answer is JSON. On success: {"success": true, "data": {"token": "...", "name": "licence.pdf", "size_label": "412 KB"}} (an ID upload also returns side and size). Errors: {"success": false, "data": {"message": "..."}}.

StatusWhen
403The nonce is missing or expired (“Your session has expired. Please reload the page and try again.”); or ID uploads are switched off (“ID uploads are not enabled.”)
400No file sent (“No file was uploaded.”), the cart does not ask that question (“This product does not take that file.”), or the file is refused (type, size). The body carries a code and a message
429Too many uploads in a row

A token is tied to the session that uploaded it, or to the signed-in customer, and lives two hours. A token that another session presents does not resolve. Because the upload is a browser AJAX call that shares the store’s cookie session, file and ID uploads suit a client that runs on the store’s own pages (a theme or a block), together with the Nonce header. A client that only holds a Cart-Token and has no page of the store to read the upload nonce from cannot upload files; questions without a file work for it.

To discard an upload, post action = sicf_field_discard (or sicf_id_discard), _ajax_nonce, token (and field for a question).

Errors at checkout #

The order is refused with POST /wc/store/v1/checkout and status 400, code sicustomfields_checkout_invalid. message holds every problem, joined with a space, in the classic checkout’s wording.

MessageWhen
<Label> is a required field.A required question has no answer (and its conditions show it)
Please upload a file for <Label>.A required file question has no valid token
Please choose a valid option for <Label>.The answer is not one of the options
<Label> is not a valid email address., ... URL., ... phone number.The format check of that type failed
<Label> must be a number., must be <min> or more., must be <max> or less.Number limits
<Label> must be <n> characters or fewer.Longer than maxlength
Please upload a photo of your ID to complete this order.An ID is required, none is on file and none was sent
Please choose which type of ID you are uploading., That is not one of the accepted document types.id.type missing or not in block.id.types
Please enter the expiry date printed on your ID., That expiry date is not a real date. Please use YYYY-MM-DD., That ID has expired. Please upload one that is still valid.id.expiry
Please enter your date of birth., That date of birth is not a real date. Please use YYYY-MM-DD., That date of birth is in the future., You must be at least <n> years old to place this order.id.dob and minAge
Please attach a photo of the front of your ID., Please attach a photo of the back of your ID as well.A side is missing
Your uploaded file is no longer available. Please attach it again.A token is unknown, expired or from another session
The damage waiver for your order could not be worked out. Please review your cart and try again.The waiver fee on the order differs from what the cart must charge: read the cart again and retry

A native question that is required and missing is refused by WooCommerce itself, before this check, with its own additional_fields error.

The damage waiver: extensions.sicustomfields-waiver #

A required waiver is always in the cart as a fee. An optional waiver is on by default and the customer may decline it. Read the offer from the cart:

FieldNotes
optionalTrue when the cart has a waiver the customer may decline
acceptedTrue when the waiver is in the cart. true when there is no optional waiver
name, descriptionThe waiver’s name and what it covers
amountThe waiver’s price as text (“$8.00”)
"sicustomfields-waiver": { "optional": true, "accepted": true, "name": "Damage waiver", "amount": "$8.00", "description": "Covers accidental damage to your rental." }

To decline or take it, call POST /wc/store/v1/cart/extensions with namespace = sicustomfields-waiver and data = {"accepted": true|false}. The cart answers with the fee added or removed. The choice is kept in the session and stored on the order; nothing needs to be sent again at checkout. A required waiver ignores the call.

Rental contract signing #

Needs Rental Contract Signing & PDF Generation. The signature travels in the checkout request as extensions.sipdfsign. The add-on adds no cart data: the client decides when to show a signing screen from the store’s own settings.

POST /wc/store/v1/checkout: extensions.sipdfsign #

FieldNotes
sitextThe typed signature: the signer’s full name
sisignatureThe drawn signature: a PNG data URL (data:image/png;base64,...) of at most 1 MB
consentBoolean: the signer agrees to sign electronically. Always required
methodtype or draw, a hint when both are sent; what was captured decides
strokesThe drawn signature’s stroke data (signature_pad toData()) as a JSON string
consent_at, terms_opened_at, signed_atBrowser timestamps, ISO 8601 UTC (2026-11-11T10:00:20Z). Kept in the signature’s audit trail
terms_hashSHA-256 of the contract terms as shown

Which signature counts depends on the store’s setting “Signature type”:

SettingCounts as signed
Typed (type)sitext only
Drawn (sign)sisignature only
Typed or drawn (typedsigned, the default)Either one
NoneNo signature is asked; nothing is checked

By default the signature is asked only when the cart contains rentals (“show only if the order contains rentals”); a cart of sold products needs none. The signature, the signer’s IP address and user agent, and the consent are recorded on the order, and the signed PDF is made when the order is placed.

curl -X POST https://example.com/wp-json/wc/store/v1/checkout \
  -H "Content-Type: application/json" \
  -H "Cart-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -d '{
    "billing_address": { "first_name": "Sarah", "last_name": "Miller", "address_1": "12 Example Street", "city": "Springfield", "state": "CA", "postcode": "90210", "country": "US", "email": "[email protected]" },
    "payment_method": "bacs",
    "extensions": { "sipdfsign": { "sitext": "Sarah Miller", "method": "type", "consent": true, "consent_at": "2026-11-11T10:00:00Z", "signed_at": "2026-11-11T10:00:20Z" } }
  }'

Errors #

The order is refused before it is placed, status 400. code is the first problem’s code, and message joins all of them.

CodeMessageWhen
sipdfsign_signaturePlease sign the rental contract to place your order.The cart needs a signature and the request carries none that counts for the signature type
sipdfsign_signatureThe drawn signature could not be read. Please clear the signature box and sign again.sisignature is not a PNG data URL of at most 1 MB
sipdfsign_consentPlease confirm that you agree to sign the rental contract electronically.consent is not true

When both are missing, the code is sipdfsign_signature and the message holds both sentences.

Only POST /checkout is checked. A PUT or PATCH that updates the draft order is not, and neither is POST /checkout/{order_id}, which pays an order that exists: its contract is signed on the signing page the customer gets with the order, never recorded from a payment request.

Delivery by Distance #

Needs Delivery by Distance. The delivery price depends on the distance from the shop to the customer’s address, so it appears as a shipping rate once the cart has a shipping address. The add-on adds one read-only value to the cart.

GET /wc/store/v1/cart: extensions.sidistance #

FieldNotes
refusalString. Why delivery is not offered to the address now (“We deliver up to 40 km from Downtown.”), "" when it is offered or no address is known yet

When shipping_rates has no delivery rate, show refusal instead of WooCommerce’s generic “no shipping options” message. The cart reads the address saved on the cart’s customer, so set it first with POST /wc/store/v1/cart/update-customer (WooCommerce’s own call), then read the cart again.

The pin on the map #

With “Check the pin on the map” switched on, a customer can move the delivery pin up to the shop’s limit (2 km by default) from the point found for the address; the price is then measured from the pin. The pin is saved by two browser calls (?wc-ajax=sidistance_pin_start and ?wc-ajax=sidistance_pin_save, with the page’s sidistance_pin nonce), not by the Store API. The Store API only refreshes the cart afterwards: POST /wc/store/v1/cart/extensions with namespace = sidistance and any data ({}) makes WooCommerce price the cart again. A client that has no page of the store to read the nonce from prices from the address alone.

curl -X POST https://example.com/wp-json/wc/store/v1/cart/extensions \
  -H "Content-Type: application/json" \
  -H "Cart-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -d '{"namespace": "sidistance", "data": {}}'

Delivery by Distance refuses nothing in the checkout request itself; an address it cannot serve has no delivery rate, so the customer cannot choose it.

Smart Routing: delivery slots and days #

Needs Smart Routing. Rental stops (delivery, collection, pick-up, return) come from Rentals & Bookings Regular when it is active; with Smart Routing alone the cart carries no rental stops (way is null, stops is []), and only the sold-products delivery day below applies.

GET /wc/store/v1/cart: extensions.sibooking-delivery #

FieldNotes
wayHow the order gets its rentals: p pick up, d delivery, s transport. null without rentals
stopsThe order’s stops. Rentals handed over on the same day in the same slot share one
titleThe panel’s title (“Delivery plan”), "" when the plan is off
notesArray of strings: shipping options left out for these dates and why, and why the address gets no Delivery option
saleA cart of sold products only: the days it can be delivered; null otherwise

Each entry of stops:

FieldNotes
keyThe stop’s key (out|2026-12-14|08:00-10:00): send it back to change the slot
kinddelivery, collection, pickup, return, dispatch or due
label“Delivery: Mon, Dec 14, 8:00 AM – 10:00 AM”
windowThe current slot (HH:MM-HH:MM), "" for no slot
optionsArray of {id, label, full}: the slots the stop can move to. id is the value to send; full marks a slot with no room left
itemsThe names of the rentals handed over at this stop

sale (sold products delivered by the Delivery option, when the shop lets customers choose the day):

FieldNotes
title“Delivery day”
daysArray of {day, slots: [{id, label, full}]}. day is Y-m-d. When the shop allows “Any time”, the first slot of a day has id ""
day, windowThe current choice. The first open day and slot until the customer chooses
arrangeText, when the shop arranges the day itself and the customer chooses none; then days is []

POST /wc/store/v1/cart/extensions (namespace sibooking-delivery) #

Field in dataNotes
stop, windowMove the rentals of the stop stop (a key from stops) to the slot window (an id from that stop’s options)
saleDay, saleWindowChoose the delivery day (Y-m-d) and slot of a sold-products cart

The cart is priced again afterwards (a slot may carry a surcharge) and returned. A stop that does not exist, a full slot or a closed day is ignored without an error: the cart comes back unchanged, so compare window or day with what you sent.

curl -X POST https://example.com/wp-json/wc/store/v1/cart/extensions \
  -H "Content-Type: application/json" \
  -H "Cart-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -d '{"namespace": "sibooking-delivery", "data": {"stop": "out|2026-12-14|08:00-10:00", "window": "10:00-12:00"}}'

Errors at checkout #

POST /wc/store/v1/checkout is refused with status 400. Slots held in the cart are claimed again at this moment, so a slot filled by someone else since the cart was built is refused here.

CodeWhen
sibooking_sale_day“Choose a delivery day.” A sold-products cart delivered by the Delivery option has no open day, or a day with slots has no slot and “Any time” is off
sibooking_capacityA stop’s slot or day is full. The message names it
sibooking_handoverRentals & Bookings Regular’s delivery rules refuse the cart (a start that is too soon, no Delivery option for the address). The message is the first problem

After a refusal the held slots are released. Read the cart again, let the customer pick another slot or day, and send the checkout again.

Store credit #

Needs Subscriptions & Memberships. A customer with an active credit membership may pay part of an order with the credit of the current billing period. Credit never pays for a membership or subscription product itself.

GET /wc/store/v1/cart: extensions.sisub-store-credit #

FieldNotes
availableString: credit left this billing period, as a decimal ("25.00"). "0.00" for a guest
labelThe checkbox text (“Use my store credit ($25.00 available)”), "" when there is no credit
useTrue when the credit is applied to this cart
appliedString: the credit taken off the cart, as a decimal. The credit shows as a negative fee named “Store Credit”

POST /wc/store/v1/cart/extensions (namespace sisub-store-credit) #

Field in dataNotes
useBoolean. true applies the credit, up to what the cart’s items cost; false removes it

The call works only for a signed-in customer who has credit left. Otherwise use stays false and no error is returned, so check use in the cart that comes back. Nothing needs to be sent at checkout: the credit fee is on the order and the usage is recorded when the order is placed.

curl -X POST https://example.com/wp-json/wc/store/v1/cart/extensions \
  -H "Content-Type: application/json" \
  -H "Nonce: NONCE_FROM_THE_HEADER" \
  -u "username:YOUR_APP_PASSWORD" \
  -d '{"namespace": "sisub-store-credit", "data": {"use": true}}'

The checkout has no store credit error of its own. WooCommerce refuses an order whose total changed between the cart and the checkout.

Nights on a cart line #

Needs Rentals & Bookings (Lite 1.0.9 or newer, or Regular). When a date-only product is sold by the night (“stays”), each cart line carries the stay in items[].extensions.sibooking_stay. It is read only. The dates are sent with cart/add-item as described in Adding to the cart and creating orders.

FieldNotes
nightlyTrue when the line is a stay of nights
check_inCheck-in date (Y-m-d), null when nightly is false
check_outCheck-out date (Y-m-d): the morning the stay ends
nightsNumber of nights, an integer
"extensions": { "sibooking_stay": { "nightly": true, "check_in": "2026-12-14", "check_out": "2026-12-17", "nights": 3 } }

A line that is not a stay answers {"nightly": false, "check_in": null, "check_out": null, "nights": null}.

A checkout with several add-ons #

  1. GET /wc/store/v1/cart: keep the Cart-Token.
  2. POST /cart/add-item for each rental, with its dates.
  3. GET /cart: read extensions to see which questions, waiver, delivery slots and credit apply.
  4. POST /cart/extensions for each choice that moves the price: the waiver, a priced answer, a slot, the credit.
  5. POST /checkout with the addresses, payment_method, additional_fields, and every extensions namespace the cart asked for (sicustomfields, sipdfsign).

If a checkout is refused, fix what the error names and send the same request again: a refused request leaves the cart as it was, and uploaded files stay valid for two hours.