Documentation / Widget API

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'} and events.onCalculated, the widget reports the product and its price on every change, and putProduct sets 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_step from 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:

  1. picks the order's status (see Status of new orders);
  2. finds or creates the customer's contact (see Contact matching);
  3. finds or creates a product for each item, de-duplicated like the products API;
  4. 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:

  1. the status marked is_default_widget;
  2. the default status (is_default) of the default funnel;
  3. the first status of the default funnel;
  4. 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 3000 does not match 15550103000.
  • 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.