Integration API (V1)
REST API for third-party systems to create, read, update and dispatch orders
Overview
The Integration API (V1) lets third-party systems (ERP, CRM, etc.) create orders, read and update them, find them by your own reference, manage tags, and dispatch orders to installers. These endpoints use API key authentication and are designed for server-to-server integrations.
See the API Reference for every endpoint's request and response schema.
Authentication
All Integration API endpoints require a Bearer token. See Authentication for how to create API keys.
Endpoints
Orders
| Endpoint | Description |
|---|---|
| Create Order | Create a new order from your external system |
| List Orders | List orders, or find one by job id, external id, idempotency key or email |
| Get Order | Get one order by its id |
| Patch Order | Update order details and custom fields |
| Add Note | Add a note to an order |
| Dispatch Order | Dispatch an order to a contractor |
| Cancel Order | Cancel an existing order |
Files
| Endpoint | Description |
|---|---|
| Upload File | Upload a file to attach to an order |
Order Tags
| Endpoint | Description |
|---|---|
| List Order Tags | Retrieve all order tags |
| Create Order Tags | Create new order tags |
| Assign Tag to Order | Assign a tag to an order |
| Remove Tag from Order | Remove a tag from an order |
Installers
| Endpoint | Description |
|---|---|
| List Installers | List available installer partners |
Finding an order by your own reference
An order has several identifiers. Use whichever one your system stores:
| Identifier | What it is |
|---|---|
id | Our order id (UUID). Create Order returns it as value. |
displayId | The job id shown in the app, for example P1-123. |
idempotencyKey | The key you sent when you created the order. Filter only; not returned. |
externalId | Your own reference, set on Create Order or Patch Order. Not unique. |
customFields | Values keyed by each field's customId. Returned on every order, not filterable yet. |
Pass one of them as a filter to List Orders, for example GET /v1/order?displayId=P1-123. The response is always a list, because externalId is not unique. Unknown or empty filters are rejected with 422.
To look orders up by your CRM or ERP id, send it as externalId when you create the order, or set it later with Patch Order.
Get Order, List Orders and Patch Order all return the same order object, except that only Get Order includes workflow. Text fields with no value are empty strings; description, externalId and state can also be null. status and source may gain new values; treat unknown values as opaque.
What you can rely on
- Order fields (ids, name, description, contact, address, status, timestamps) are stable. New fields may be added; existing ones keep their name and type.
statusandsourcekeep their meaning, but new values may be added. Treat unknown values as opaque.customFieldsare configured by the operator. A field can be renamed, removed or change type, so map on stablecustomIds that you agree on with the operator.tags: match onid. Names and colors can be edited.workflowis read-only progress. Its steps and actors come from the operator's workflow template, so map ontemplateCustomId,templateStepIdand actorslug, not on titles or names. Bookings, offers and step answers are not part of the order object.
Workflow progress
Get Order returns workflow; List Orders and Patch Order leave it out. Orders created before workflows existed have none, and the field is left out.
| Field | What it is |
|---|---|
id | The order's workflow |
templateId, templateCustomId | The template, same across versions. Send either back on create: templateId as workflowId, templateCustomId as workflowCustomId. |
templateName, templateVersion | The template's name and the published version the order runs on |
actors[] | Roles on the order: slug, name, and the organization that accepted it (null until then). organization.externalId is your own id for it: the sub-organization's external id, or your partnership external id. |
steps[] | In template order: id, templateStepId, title, type, status, completedAt, actorSlug. title can be null; use type to label the step. |
steps[].status | See workflow states. A step that has not started is todo. |
steps[].assignee | The person who answers for the step in your organization, or in the sub-organization that performs it. null otherwise. People at other organizations are never shown. |
When an installer forwards a job to another organization, the role's slug moves to the organization that now holds it. The installer that forwarded it stays in actors under a generated slug that contains __fwd__, and the steps it already handled move to that slug. Key on the role slugs from your workflow template; other slugs, such as __fwd__ ones, are generated and can change.
Paging and syncing
List Orders returns up to limit orders (default 25, max 100), newest first, and a nextCursor. Pass it as cursor with the same filters to get the next page. nextCursor is null on the last page. Orders created while you page do not shift pages.
To keep your system in sync, pass updatedAfter to fetch only what changed since your last poll. Set it to the time your previous poll started, not the latest updatedAt you received, and remove duplicates by id.
updatedAtchanges when the order's own fields change. Workflow progress does not change it yet, and List Orders does not returnworkflow. To follow progress, fetch each open order with Get Order.- Deleted orders are left out. Get Order returns
404for them. - Test orders are left out of List Orders.
statusis the order's lifecycle:PENDING(open),CANCELLEDorDONE.
Typical Integration Flow
A common ERP integration follows this pattern:
1. ERP creates order → Create Order
2. ERP assigns tag → Assign Tag to Order
3. ERP dispatches → Dispatch Order
4. Webhook notifies → step state change
5. ERP fetches result → Get OrderUse webhooks to receive real-time notifications when order status changes, rather than polling.
Error Handling
| Status | Description | Action |
|---|---|---|
200 | Success | Process the response |
400 | Bad request, such as an unknown custom field or an invalid cursor | Check request payload |
401 | Unauthorized | Verify API key |
404 | Not found, or not owned by your organization | Check resource IDs |
409 | An order with this idempotency key already exists | Use the returned orderId |
422 | Validation error in the body or query | Check fields and query parameters |
429 | Rate limit reached | Wait for Retry-After seconds, then retry |
Reading orders (List Orders and Get Order) is limited to 600 requests per minute per organization, shared by all its API keys. Retry 429 responses after the Retry-After header, with exponential backoff.