Widget API
The public API behind the embeddable configurator — widget tokens, permissions, CORS, reading configurators, placing orders from a website, contact matching and rate limits.
The widget API serves the public configurator that a manufacturer embeds on their own website. Customers are not Configo users, so these endpoints are not authorized with OAuth. A widget token identifies the project and says what the widget may do.
Use it to build your own storefront on top of Configo: list configurators, price a configuration in the browser, and send the order to the project's CRM. To embed the ready-made widget instead, see Widget embedding & iframe API.
Field types follow the field notation.
Authentication
Send the token in the X-Widget-Token header on every request:
GET /api/v1/widget/configurators HTTP/1.1
Host: configo.org
X-Widget-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
- Tokens are generated in the app: Project settings → Widget, by a member with
can_edit_project. The token carries the project and the widget permissions chosen at that moment. - A token is valid for one year. It cannot be revoked or changed: to change permissions, generate a new token and replace it on the site.
- A widget token is meant to be public. Anyone who has it can read what it allows and place orders, so grant only what the website needs.
| Status | Body | When |
|---|---|---|
| 401 | {"message": "Unauthorized"} |
No X-Widget-Token header |
| 403 | {"message": "Invalid widget token"} |
The token is malformed, expired, or not a widget token |
| 403 | {"message": "Project unavailable"} |
The project owner's subscription is not active; the widget stops working until it is renewed |
CORS
Every widget endpoint can be called from any origin, without credentials (no cookies). Responses carry
Access-Control-Allow-Origin: *, methods GET, POST, OPTIONS, headers Content-Type, X-Widget-Token. Preflight
requests are answered with 204 and cached for a day.
Widget permissions
| Permission | Value | Effect |
|---|---|---|
show_cart |
2 | The embedded widget shows a cart with checkout. Not enforced by the API |
show_public_only |
4 | Only configurators with is_publicly_accessible: true are listed and readable |
can_view_purchase_price |
2048 | Material purchase_price is returned; otherwise 0 |
can_view_sale_price |
4096 | Material sale_price is returned; otherwise 0 |
can_view_dealer_price |
8192 | Material dealer_price is returned; otherwise 0 |
can_override_price |
2097152 | The user may set their own price: the widget shows a price field, the API accepts sale_price |
can_adjust_price |
4194304 | The user may apply a markup or discount: in the widget and in markup_percent/discount_percent |
can_print_docs |
8388608 | Document templates marked for the widget can be printed |
Error format
Widget endpoints return errors as {"message": "..."}, not the API error objects of the OAuth API.
Get Project
GET /api/v1/widget
{
"name": "Acme Windows",
"price_step": 0.01
}
price_step is the project's rounding step: round totals you show to it.
List Configurators
GET /api/v1/widget/configurators
Returns the project's active configurators, sorted by name. With show_public_only, only public ones.
[
{
"uuid": "0199ab16-3d4e-7f50-8162-738495061728",
"name": "Roller blind",
"image_uuid": "0199ae51-0000-7000-8000-000000000002",
"image_url": "/uploads/0199ae51-0000-7000-8000-000000000002"
}
]
Get Configurator
GET /api/v1/widget/configurators/{configurator.uuid}
Returns everything needed to price the configurator in the browser: its structure and its materials.
| Field | Type | Description |
|---|---|---|
| uuid, name | — | As in the configurator |
| product_name | string | Product name template |
| product_work_cost | decimal | Flat amount added to sale and dealer totals |
| product_amortization | decimal | Flat amount added to sale and dealer totals |
| price_multiplier | decimal | Multiplies every material price |
| controls | array | The controls, each with a ready options list for choice controls |
| calculations, parts, presets | array | The structure |
| is_calculations_displayed | boolean | Whether to show the bill of materials to the customer |
| is_publicly_accessible | boolean | Whether the configurator is public |
| status | string | Always active here |
| materials | array | {uuid, name, group, unit, purchase_price, dealer_price, sale_price, percent_waste, properties, image_uuid, description}; prices the token may not see are 0 |
Control options
Controls of type select, select_image, select_depend and materials carry options, already resolved from the
JSON list, the material group or the material properties the control points at. Other controls have no options.
| Field | Type | Description |
|---|---|---|
| value | string | The value to send for this control |
| image_url? | ?string | The material's picture, when the option is a material |
| description? | ?string | The material's description, when the option is a material and it has one |
| when? | object | select_depend only: {"<control id>": [values]} — the option is available when every listed control has one of these values |
| uuid, group, unit | — | materials only: the material to put in values as {"<uuid>": {"quantity": n}} |
{
"id": "$cloth",
"type": "select_image",
"label": "Fabric",
"default": "Blackout 605",
"options": [
{
"value": "Blackout 605",
"image_url": "/uploads/0199ae51-0000-7000-8000-000000000003",
"description": "Dims 100% of light"
},
{
"value": "Linen 220",
"image_url": null,
"description": null
}
]
}
Pricing on the website
There is no server-side calculate endpoint for the widget: prices are calculated in the browser, and the server recalculates every item only once, when the order is created. To show a live price on your own page:
- Use the embedded widget as the calculator. With
config: {mode: 'calculator'}andevents.onCalculated, the widget reports the product and its price on every change, andputProductsets values from your page — the numbers are exactly the ones the order will get. See Widget embedding & iframe API. - Or calculate yourself from this endpoint's data, following the formulas and
the price formula, and round totals to the project's
price_stepfrom Get Project. Material prices are returned only to a token that may view them.
404 {"message": "Not found"} if the configurator does not exist, is suspended, or is hidden by show_public_only.
Create Order
POST /api/v1/widget/orders
Places an order from the website. First every item is calculated on the server from its configurator, the same way the widget calculates it in the browser (see Server-side pricing). Then, in one transaction, it:
- picks the order's status (see Status of new orders);
- finds or creates the customer's contact (see Contact matching);
- finds or creates a product for each item, de-duplicated like the products API;
- creates the order with those products.
The order's journal gets a created entry with no author. A non-empty message is added as a note on the order. After
the order is saved, the project's order_created automations run.
JSON Params
| Field | Type | Description |
|---|---|---|
| contact_name? | string | Up to 255 characters. Default "" |
| contact_email? | string | Up to 64 characters. Default "" |
| contact_phone? | string | Up to 32 characters. Default "" |
| contact_address? | string | Up to 512 characters. Default "" |
| message? | string | Customer's comment, up to 4096 characters |
| products? | array | Up to 200 order items. Default [] |
Order Item Input
| Field | Type | Description |
|---|---|---|
| configurator_uuid | uuid | An active configurator of this project (with show_public_only — a public one) |
| values? | object | Control values by control id, e.g. {"$width": 1200}. A materials control takes {"<material uuid>": {"quantity": 2}} |
| configuration? | object | Used when values is absent — the display values the widget reports. Cannot price a non-empty materials control |
| price_type? | string | sale, dealer or purchase — which price level becomes the line price. Only a level the token can view is accepted |
| sale_price? | decimal | Your own unit price, 0–99 999 999.99. Used only with can_override_price |
| markup_percent? | decimal | 0–99 999. Used only with can_adjust_price, otherwise 0 |
| discount_percent? | integer | 0–100. Used only with can_adjust_price, otherwise 0 |
| quantity? | decimal | At least 1; default 1 |
Controls missing from values take their default. Other fields — name, materials, parts, purchase_price — are
ignored: the product sent by the widget's onAddToCart can be posted as it is.
Numeric fields that are invalid or out of range are replaced with 0 (quantity with 1) instead of failing.
Identical items are merged into one line with the quantities added up.
Server-side pricing
The product name, configuration, bill of materials, cut-list and prices are calculated from the configurator and the
current material prices — whatever the browser calculated is not trusted. The line price is the level chosen by
price_type; without it, the same level the widget uses: dealer if the token can view only dealer prices, else
sale, else purchase. A price level is calculated from real material prices even when the token cannot view it.
Example Request
{
"contact_name": "John Smith",
"contact_email": "[email protected]",
"contact_phone": "+1 555 010 3000",
"message": "Please call before delivery.",
"products": [
{
"configurator_uuid": "0199ab16-3d4e-7f50-8162-738495061728",
"values": {
"$width": 1200,
"$height": 1600,
"$cloth": "Blackout 605"
},
"quantity": 2
}
]
}
Response
201 Created
{
"uuid": "0199a4e2-7b1c-7c3e-9d2a-5f4e8b1c0a11",
"serial": "26274003",
"total_amount": 44.9,
"products": [
{
"name": "Roller blind Blackout 605, 1200×1600 mm",
"quantity": 2,
"sale_price": 22.45,
"markup_percent": 0,
"discount_percent": 0,
"amount": 44.9
}
]
}
total_amount and the line amounts are what the server calculated — show them to the customer as the confirmed
price. amount is sale_price × quantity × (1 + markup_percent/100) × (1 − discount_percent/100); totals are not
rounded to price_step. Lines are sorted by name; identical items are already merged.
Status of new orders
The order gets the first status found in this order of preference:
- the status marked
is_default_widget; - the default status (
is_default) of the default funnel; - the first status of the default funnel;
- the first status of any funnel.
Contact matching
The widget never creates a duplicate of a contact it can match:
- An existing contact of the project is matched by email OR phone. The values are trimmed and must be exactly equal
to the stored ones, including case and formatting:
+1 555 010 3000does not match15550103000. - If several contacts match, the oldest is used. Archived contacts are matched too.
- On a match, the contact's empty fields (name, email, phone, address) are filled in from the request. Fields that already have a value are never overwritten.
- With no match, or when neither email nor phone is given, a new contact is created as a
lead.
Rate limit
At most 60 orders per minute per project, counted across all callers of all of the project's tokens. Over the limit,
the response is 429 {"message": "Too many requests"}. Wait and retry; the window is a fixed 60 seconds.
Errors
| Status | Body | When |
|---|---|---|
| 400 | {"message": "Invalid request body"} |
The body does not match the schema |
| 400 | {"message": "Configurator not found: <uuid>"} |
An item's configurator is not in this project |
| 400 | {"message": "<uuid>: <reason>"} |
An item cannot be calculated: an unknown control id in values, or a materials control sent as display text |
| 400 | {"message": "No default status configured"} |
The project has no statuses |
| 429 | {"message": "Too many requests"} |
The rate limit is exceeded |
Get Order
GET /api/v1/widget/orders/{order.uuid}
Returns an order placed through the widget together with its contact and products: the data a
document template is rendered with. Intended for printing documents, so it requires can_print_docs.
{
"order": {
"uuid": "0199a4e2-7b1c-7c3e-9d2a-5f4e8b1c0a11",
"serial": "26274003",
"status_uuid": "0198f1c0-2d4a-7e11-8b3f-1a2b3c4d5e6f",
"user_uuid": null,
"user_name": "",
"contact_uuid": "0199b5ee-0000-7000-8000-000000000001",
"paid_amount": 0,
"product_count": 1,
"subtotal_amount": 44.9,
"discount_amount": 0,
"total_amount": 44.9,
"created_at": "2026-10-01T09:30:00.000Z",
"updated_at": "2026-10-01T09:30:00.000Z"
},
"contact": {
"uuid": "0199b5ee-0000-7000-8000-000000000001",
"name": "John Smith",
"email": "[email protected]",
"phone": "+1 555 010 3000",
"address": "",
"note": "",
"meta": {}
},
"products": [
{
"uuid": "0199a3f1-5c2e-7d40-b1a2-3c4d5e6f7a8b",
"name": "Roller blind Blackout 605, 1200×1600 mm",
"configuration": {
"$width": 1200,
"$height": 1600,
"$cloth": "Blackout 605"
},
"materials": [],
"parts": [],
"unit": "pcs",
"purchase_price": 0,
"quantity": 2,
"controls": {
"$width": "Width, mm",
"$height": "Height, mm",
"$cloth": "Fabric"
}
}
]
}
order.total_amount is the amount still to be paid (subtotal_amount − paid_amount). products[].purchase_price is 0 unless the token has can_view_purchase_price. products[].controls maps
control ids to their labels, for printing the configuration.
404 {"message": "Not found"} without can_print_docs, 404 {"message": "Order not found"} if there is no such order.
Documents
Document templates marked is_shown_widget, for customers to print. Both endpoints require can_print_docs, and
return 403 {"message": "Not allowed"} without it.
List Documents
GET /api/v1/widget/documents
[
{
"uuid": "0199ae5a-7182-7394-a5b6-c7d8e9f0a1b2",
"name": "Quote"
}
]
Get Document
GET /api/v1/widget/documents/{document.uuid}
Returns the template, to be rendered on the client with the data from Get Order:
{
"content": "<h1>Quote {{order.serial}}</h1>...",
"logo_url": "/uploads/0199ae50-0000-7000-8000-000000000001"
}
404 {"message": "Document not found"} if it does not exist or is not marked for the widget.