---
title: Build your first SkyPlanner integration with API V4
description: "**Developer guide · API V4** This tutorial covers the **V4 API**. Send customers and production orders from your ERP to SkyPlanner, let your team plan and execute the work, and bring progress and recorded time back into your ERP. This…"
url: "https://skyplanner.ai/docs/api-v4-basic-integration/"
updated: "2026-10-28"
category: Integrations
---

# Build your first SkyPlanner integration with API V4

**Developer guide · API V4**

This tutorial covers the **V4 API**. Send customers and production orders from your ERP to SkyPlanner, let your team plan and execute the work, and bring progress and recorded time back into your ERP. This guide walks through that round trip with one small manufacturing order.

You will create an order for ten steel brackets with cutting and welding steps, update it to twelve pieces, and retrieve its production results. All names, IDs, and dates below are fictional. Replace them with values from your account. The examples use only the public V4 API.

All ten request bodies are included below. Save each JSON example with the filename shown, in the directory where you run the cURL commands. Use a plain-text editor and UTF-8 encoding. You can also paste the JSON directly into the matching Swagger request body.

## 1. Understand the integration flow

[![ERP sends customer and order data to SkyPlanner. SkyPlanner plans work on mapped workstations and records production. The integration reads progress and timelogs back into the ERP.](https://skyplanner.ai/wp-content/uploads/2026/10/integration-flow.png)](https://skyplanner.ai/wp-content/uploads/2026/10/integration-flow.png)
Start with an agreed direction for each kind of data. Keep the SkyPlanner IDs returned by the API.

| Business concept | V4 resource | How it connects |
| --- | --- | --- |
| Customer | `/customers` | An order references its customer through `customer_id`. |
| Order / work order | `/orders` | Contains an `order_items` array. |
| Order row / line | `/order-items` | Belongs to an order; contains `process_steps`. |
| Job / operation on a row | `/process-steps` | Belongs to an order-item. Its public `id` is the `process_step_id` used by timelogs. |
| Machine / work center | `/workstations` | A step's `workstations` array lists eligible workstation IDs. |
| Recorded work session | `/timelogs` | Connects a job to a person, start/end times, and recorded quantities. |

Business terms and their V4 resources

Agree with the customer which system owns each field. A common starting point is that the ERP owns customer references, order quantities, due dates, and operation estimates; SkyPlanner owns the schedule and execution progress. Update only fields your integration owns.

For this basic integration, the customer configures workstation capacity, working hours, and scheduling in SkyPlanner. Your integration imports the work and reads the results. Creating an order does not guarantee that it already has scheduled start and end times.

## 2. Connect to your account

Ask your SkyPlanner administrator for an API key and the account's tenant identifier. The key must belong to that tenant. For `example.skyplanner.app`, the tenant identifier is `example`. Use a test account while developing.

| Setting | Value |
| --- | --- |
| Production base URL | `https://api.skyplanner.app/production-planning/api/v4` |
| `x-api-key` header | Your account's API key |
| `X-Tenant` header | Your tenant identifier |
| JSON request header | `Content-Type: application/json` |
| Response header preference | `Accept: application/json` |

Use the API base URL shown above for all requests. Direct requests to `https://YOUR-TENANT.skyplanner.app/production-planning/api/v4/...` are rejected with HTTP 403.

The examples below use a POSIX shell with cURL, such as Bash or Git Bash. In Windows, run these commands in Git Bash; the line continuations and environment-variable syntax are not PowerShell syntax. Set these environment variables locally; keep the real key in your integration's secret store and out of source control and logs.

```
export SP_BASE='https://api.skyplanner.app/production-planning/api/v4'
export SP_TENANT='example'
export SP_API_KEY='REPLACE_WITH_YOUR_API_KEY'

curl --get "$SP_BASE/workstations" 
  --header "x-api-key: $SP_API_KEY" 
  --header "X-Tenant: $SP_TENANT" 
  --header 'Accept: application/json' 
  --data-urlencode 'page=1' 
  --data-urlencode 'per_page=100'
```

Sign in to your SkyPlanner tenant and open **API Documentation** from the tenant's **front page** to access the API V4 Swagger reference. Check that the reference is for **V4**. Click **Authorize** and enter your API key in `apiToken`. The `tenantHeader` value is filled automatically from your tenant context; verify that it matches your account and leave it unchanged. Then try **GET /workstations**. Swagger's server should be the API URL above. Use **Try it out** to edit a request; **Execute** sends it to the selected account. The screenshots show prepared requests with fictional IDs, not successful executions.

For reference, a V4 documentation URL with tenant context looks like `https://integration.skyplanner.app/api/v4/swagger?instance=example`, where `example` is the tenant identifier. Use the front-page link as your normal entry point. If that link opens V3 or the tenant is not filled, ask your SkyPlanner administrator to check the documentation link before continuing.

[![V4 Swagger Authorize dialog with x-api-key and X-Tenant inputs.](https://skyplanner.ai/wp-content/uploads/2026/10/swagger-authorize.png)](https://skyplanner.ai/wp-content/uploads/2026/10/swagger-authorize.png)
The Swagger authorization dialog. Enter your API key in apiToken; tenantHeader is filled automatically when you arrive through your tenant's front-page link. This guide's captures contain no working credentials.

Successful JSON responses have a `data` property. Lists also contain `pagination`. HTTP 200 means a read or update succeeded; creation normally returns 201. Always check the HTTP status before using the body.

```
{
  "data": [{"id": 7, "name": "Cutting station", "external_id": "ERP-WC-CUT"}],
  "pagination": {"page": 1, "per_page": 100, "total": 1, "total_pages": 1}
}
```

*Illustrative response excerpt: other resource fields are omitted for readability.* Read every page, not just page 1. Resource endpoints document their accepted filters and sorting fields; an unknown query parameter returns 400.

## 3. Prepare customers and workstations

### Find or create the customer

First check whether your ERP customer is already mapped:

```
curl --get "$SP_BASE/customers" 
  --header "x-api-key: $SP_API_KEY" 
  --header "X-Tenant: $SP_TENANT" 
  --data-urlencode 'external_identifier=ERP-CUST-1001' 
  --data-urlencode 'page=1' --data-urlencode 'per_page=100'
```

If exactly one customer matches, keep its `id`. If none matches and your integration owns customer creation, create it with **POST /customers**. If several match, resolve the duplicate records rather than selecting the first one.

```
{
  "name": "Example Manufacturing",
  "external_identifier": "ERP-CUST-1001"
}
```

Save this as `customer-create.json` and send it:

```
curl --request POST "$SP_BASE/customers" 
  --header "x-api-key: $SP_API_KEY" 
  --header "X-Tenant: $SP_TENANT" 
  --header 'Content-Type: application/json' 
  --data-binary @customer-create.json
```

In this walkthrough, assume the response contains `data.id = 7`. Use your actual returned ID as the order's `customer_id`. To change this customer later, send only the fields you own to `PUT /customers/7`.

[![POST /customers expanded in V4 Swagger with the fictional customer request body.](https://skyplanner.ai/wp-content/uploads/2026/10/swagger-customer-create.png)](https://skyplanner.ai/wp-content/uploads/2026/10/swagger-customer-create.png)
A customer creation request prepared in Swagger. The screenshot shows a draft request, not a production execution.

### Map the workstations

Prefer mapping existing SkyPlanner workstations. For example, request `GET /workstations?external_id=ERP-WC-CUT` and `GET /workstations?external_id=ERP-WC-WELD`. If external IDs have not been populated, list workstations and agree the mapping with the customer.

If the integration is responsible for creating a missing workstation, `POST /workstations` accepts the following body. In SaaS accounts, creation is subject to the account’s subscription workstation limit. All workstations count toward this limit. When the limit is reached, the API returns HTTP 403 with `error.code: "workstation_limit_reached"`, even when the credentials are valid. Reuse an existing workstation where appropriate, or ask the account administrator to review the subscription limit. Creating a workstation record does not configure a usable shift calendar or complete scheduling setup.

```
{
  "name": "Cutting station",
  "external_id": "ERP-WC-CUT"
}
```

Save the body as `workstation-create.json`. For welding, use `Welding station` and `ERP-WC-WELD`. Assume the mapped workstation IDs are 7 and 8. A customer ID of 7 and workstation ID of 7 are unrelated: IDs belong to their respective resources.

```
curl --request POST "$SP_BASE/workstations" 
  --header "x-api-key: $SP_API_KEY" 
  --header "X-Tenant: $SP_TENANT" 
  --header 'Content-Type: application/json' 
  --data-binary @workstation-create.json
```

## 4. Create an order, rows, and jobs together

Use **POST /orders** to create the complete order tree in one request. This keeps the parent and child records together and avoids separate calls for every row and job.

This example has no product-catalog dependency. An order-item can have a description, quantity, and explicitly supplied process steps without a product reference. If you later use `product_id` or `product_identifier`, the referenced product must already exist.

Save the following body as `order-create.json` and replace `customer_id` and the workstation IDs with the mappings from the previous section.

```
{
  "number": "SO-1042",
  "external_order_number": "ERP-SO-1042",
  "customer_id": 7,
  "description": "Ten steel brackets",
  "delivery_date": "2026-11-05T14:00:00Z",
  "order_items": [
    {
      "external_id": "ERP-SO-1042-L10",
      "description": "Steel bracket, revision B",
      "amount": 10,
      "process_steps": [
        {
          "external_id": "ERP-SO-1042-L10-CUT",
          "name": "Cutting",
          "time_variation": "time_per_pcs",
          "duration": 120,
          "setting_time": 600,
          "workstations": [7],
          "instructions": "Use drawing BRACKET-B. Deburr cut edges."
        },
        {
          "external_id": "ERP-SO-1042-L10-WELD",
          "name": "Welding",
          "time_variation": "time_per_pcs",
          "duration": 240,
          "setting_time": 900,
          "workstations": [8]
        }
      ]
    }
  ]
}
```

```
curl --request POST "$SP_BASE/orders" 
  --header "x-api-key: $SP_API_KEY" 
  --header "X-Tenant: $SP_TENANT" 
  --header 'Content-Type: application/json' 
  --data-binary @order-create.json
```

| Field | Meaning in this example |
| --- | --- |
| `number` | The order's business number. `external_order_number` stores the ERP reference used for reconciliation. |
| `external_id` | Your reference for a specific row or step. Retain the server ID alongside it; sending an external reference is not an automatic upsert. |
| `amount` | Quantity to produce: ten pieces initially. |
| `time_variation` | Set to `time_per_pcs` explicitly so run time scales with the order-item quantity. When omitted on a new step, SkyPlanner uses the selected process-step template’s duration mode, falling back to `time_per_pcs` if no valid template default exists. |
| `duration` | Run time in seconds. With time_variation: `time_per_pcs`, this is seconds per piece: cutting takes 120 seconds per piece. With fixed_time, this is the total run time for the item, independent of quantity. Setup time is added once in either case. |
| `setting_time` | One-off setup time in seconds. Cutting's base estimate is 10 × 120 + 600 = 1,800 seconds, or 30 minutes. |
| `settle_time` | Optional settling time after the work, in whole seconds; omitted here and defaults to 0 on creation. This differs from setup time. |
| `min_degree` | Minimum degree of manufacture, a percentage from 0 to 100 with at most two decimal places. It defaults to 100 for these new steps. A positive `settle_time` requires `min_degree: 100`; incompatible values return 422. |
| `workstations` | Eligible workstation IDs, not a promise that the step is scheduled there at a particular time. |
| `name` | Creates or reuses an active process-step template. Multiple matching templates cause 422; use a known `process_step_template_id` to disambiguate. |

What the important fields mean

Creating or resolving a template by name also appends the supplied workstations to that template’s eligible stations. Agree operation names with the customer. This example explicitly sets `time_variation: "time_per_pcs"` on both steps, so its estimates remain valid even when a matching template uses fixed time. Cutting therefore takes 10 × 120 + 600 = 1,800 seconds initially, and 12 × 120 + 600 = 2,040 seconds after the quantity update. Omitting `time_variation` when updating an existing step preserves its stored duration mode, even if you change its template. Working hours and scheduling settings also affect the calendar span of the final schedule.

![](https://skyplanner.ai/wp-content/uploads/2026/10/image-1024x1024.png)

### Keep the IDs returned by SkyPlanner

A successful creation returns the order under `data`, including its nested rows and steps. The following is an **illustrative excerpt**, not a second request to send:

```
{
  "data": {
    "id": 1042,
    "number": "SO-1042",
    "external_order_number": "ERP-SO-1042",
    "order_items": [{
      "id": 301,
      "external_id": "ERP-SO-1042-L10",
      "process_steps": [
        {"id": 512, "external_id": "ERP-SO-1042-L10-CUT", "name": "Cutting"},
        {"id": 513, "external_id": "ERP-SO-1042-L10-WELD", "name": "Welding"}
      ]
    }]
  }
}
```

| ERP reference | SkyPlanner resource | Illustrative ID |
| --- | --- | --- |
| ERP-CUST-1001 | Customer | 7 |
| ERP-WC-CUT / ERP-WC-WELD | Workstations | 7 / 8 |
| ERP-SO-1042 | Order | 1042 |
| ERP-SO-1042-L10 | Order-item | 301 |
| ERP-SO-1042-L10-CUT / ERP-SO-1042-L10-WELD | Process-steps | 512 / 513 |

Example mapping stored by the integration, scoped to the tenant

## 5. Make safe updates

Use the returned IDs and send only the fields you intend to change. V4 uses PUT for these partial updates: omitted fields are preserved.

### Move the due date

Save as `order-update.json`. Send to `PUT /orders/1042`.

```
{
  "delivery_date": "2026-11-06T14:00:00Z"
}
```

```
curl --request PUT "$SP_BASE/orders/1042" 
  --header "x-api-key: $SP_API_KEY" 
  --header "X-Tenant: $SP_TENANT" 
  --header 'Content-Type: application/json' 
  --data-binary @order-update.json
```

### Increase the quantity

Save as `order-item-update.json`. Send to `PUT /order-items/301`.

```
{
  "amount": 12
}
```

```
curl --request PUT "$SP_BASE/order-items/301" 
  --header "x-api-key: $SP_API_KEY" 
  --header "X-Tenant: $SP_TENANT" 
  --header 'Content-Type: application/json' 
  --data-binary @order-item-update.json
```

### Revise cutting instructions

Save as `process-step-update.json`. Send to `PUT /process-steps/512`.

```
{
  "instructions": "Use drawing BRACKET-C. Deburr cut edges."
}
```

```
curl --request PUT "$SP_BASE/process-steps/512" 
  --header "x-api-key: $SP_API_KEY" 
  --header "X-Tenant: $SP_TENANT" 
  --header 'Content-Type: application/json' 
  --data-binary @process-step-update.json
```

### Take care with nested arrays

- Omitting `order_items` leaves the existing rows untouched.
- Sending `order_items` declares the desired row set. Existing rows omitted from that array are archived along with their steps.
- Within a row, omitting `process_steps` preserves its steps. Sending the array replaces the desired step set; sending `[]` archives all steps on that row.
- Nested children are matched by their SkyPlanner `id`. A child without an ID is created, even if you send a familiar external reference.
- A present `meta_data` object or `materials` array also replaces that collection. Avoid sending a complete GET response back as your update.

For a small row or job edit, the individual endpoints above are usually easier to control. To add another row or job later, use `POST /order-items` with `order_id`, or `POST /process-steps` with `order_item_id`. Their Swagger request schemas describe the remaining fields.

## 6. Read planning and production progress

After the customer has scheduled the imported work, read the order and its steps:

```
GET /orders/1042
GET /order-items?order_id=1042
GET /process-steps?order_item_id=301
GET /process-steps/512
```

All paths use the V4 base URL and headers from section 2. Use the individual step response to read `planned_start_time`, `planned_end_time`, and `production_status`. Planned times can be null when no applicable scheduled work exists. They summarize a step's schedule; do not assume a single uninterrupted reservation.

```
{
  "data": {
    "id": 512,
    "order_item_id": 301,
    "external_id": "ERP-SO-1042-L10-CUT",
    "name": "Cutting",
    "production_status": "not_started",
    "planned_start_time": "2026-11-03T08:00:00Z",
    "planned_end_time": "2026-11-03T08:34:00Z"
  }
}
```

*Illustrative scheduled response excerpt, not a guaranteed outcome from the creation request.* The 34-minute base estimate reflects the updated twelve-piece quantity. Actual scheduling depends on capacity and account configuration.

**Read `production_status` for execution progress.** The ordinary `status` field belongs to the order/row/step's business state and has a different vocabulary. Order and order-item production statuses have their own vocabularies; do not apply the step status list to them. A step's production status can be `not_started`, `in_progress`, `paused`, `completed`, `unknown`, or null. Do not equate null with completed, and do not write a status merely to mirror a value you just read.

[![V4 Swagger GET /process-steps/{id} with the step ID parameter ready to submit.](https://skyplanner.ai/wp-content/uploads/2026/10/swagger-process-step.png)](https://skyplanner.ai/wp-content/uploads/2026/10/swagger-process-step.png)
Use the process-step ID returned by the order request when inspecting an individual job.

## 7. Import recorded time and quantities

If operators use SkyPlanner to record work, your integration only needs to read timelogs. Request the sessions for each mapped job:

```
curl --get "$SP_BASE/timelogs" 
  --header "x-api-key: $SP_API_KEY" 
  --header "X-Tenant: $SP_TENANT" 
  --data-urlencode 'log_type=jobs' 
  --data-urlencode 'process_step_id=512' 
  --data-urlencode 'sort=id' 
  --data-urlencode 'page=1' 
  --data-urlencode 'per_page=100'
```

`process_step_id=512` refers to the cutting process-step. The timelog's own `id` identifies one work session, not the job. Pausing and resuming work can produce several sessions for the same job and person.

```
{
  "data": [{
    "id": 4822,
    "process_step_id": 512,
    "person_id": 33,
    "person_name": "Alex Example",
    "status": "completed",
    "start_time": "2026-11-03T08:15:00Z",
    "end_time": "2026-11-03T08:46:00Z",
    "duration": 1860,
    "quantity": 12,
    "faulty_quantity": 0,
    "workstation": "Cutting station",
    "note": "Cutting complete; ready for welding."
  }],
  "pagination": {"page": 1, "per_page": 100, "total": 1, "total_pages": 1}
}
```

*Illustrative single-session excerpt.* A full read after the optional pause/resume walkthrough includes both sessions. `duration` is seconds. The full response also includes fields such as `work_type` and `group_log_key`, omitted from this excerpt. An open session has `end_time: null` and `duration: null`; null is not a zero-length completed session. For job sessions, `status` reflects the job's current production state, so use `end_time` to determine whether that particular session has ended.

[![GET /timelogs in V4 Swagger with process_step_id and pagination filters for reading recorded work.](https://skyplanner.ai/wp-content/uploads/2026/10/swagger-timelogs.png)](https://skyplanner.ai/wp-content/uploads/2026/10/swagger-timelogs.png)
Filter by the mapped job and consume every page. /timelogs accepts up to 200 entries per page; this guide requests 100. Extra work without a job is excluded by log_type=jobs.

### A polling strategy that catches later changes

1. Keep a set of mapped jobs whose results need synchronization.
2. For each job, fetch every page of `/timelogs?log_type=jobs&process_step_id=...` in a stable sort order.
3. Upsert each session into the ERP using tenant + timelog `id` as the external key. Re-reading a session must update it, not add its time and quantity again.
4. Revisit open and recently completed jobs. Agree a historical reconciliation schedule for older corrections; a daily full reread of the integration's mapped jobs is a safe starting point when volume permits.
5. If ERP totals are derived from sessions, replace each session's contribution or recalculate totals. Agree how recorded and faulty quantities map to the ERP's production reporting rules.

**There is no `modified` filter on `/timelogs`.** A filter such as `start_time[gte]` finds sessions by when they began; it will miss an older session that ends or is corrected later. A start-time watermark alone is insufficient. Page-based reads are also not a snapshot of a changing dataset, so make polling repeatable and run reconciliation.

## 8. Optional: control the timer through the API

Use this section only if your own application will record work. It is optional for an ERP integration that reads work already recorded in SkyPlanner.

Resolve the worker through `GET /people?external_person_id=ERP-EMP-33` and keep their returned person ID. The example uses 33. A person ID is not a login user ID. The job must be ready for time tracking, and the selected workstation must exist. Completing a job can be rejected if another person is still working on it. If `workstation_id` is omitted, the API must be able to derive exactly one workstation from the job's work; supplying it explicitly avoids ambiguity.

| Action | Endpoint | Example |
| --- | --- | --- |
| Start | `POST /timelogs/start` | `timelog-start.json` |
| Pause | `POST /timelogs/stop`, omit `complete` | `timelog-pause.json` |
| Resume | `POST /timelogs/start` again | `timelog-resume.json` |
| Finish the job | `POST /timelogs/stop`, set `complete: true` | `timelog-complete.json` |

### Start cutting at 08:00 UTC

Save as `timelog-start.json`. Send to `POST /timelogs/start`.

```
{
  "process_step_id": 512,
  "person_id": 33,
  "workstation_id": 7,
  "datetime": "2026-11-03T08:00:00Z"
}
```

```
curl --request POST "$SP_BASE/timelogs/start" 
  --header "x-api-key: $SP_API_KEY" 
  --header "X-Tenant: $SP_TENANT" 
  --header 'Content-Type: application/json' 
  --data-binary @timelog-start.json
```

### Pause at 08:10 UTC

Save as `timelog-pause.json`. Send to `POST /timelogs/stop`.

```
{
  "process_step_id": 512,
  "person_id": 33,
  "datetime": "2026-11-03T08:10:00Z"
}
```

```
curl --request POST "$SP_BASE/timelogs/stop" 
  --header "x-api-key: $SP_API_KEY" 
  --header "X-Tenant: $SP_TENANT" 
  --header 'Content-Type: application/json' 
  --data-binary @timelog-pause.json
```

### Resume at 08:15 UTC

Save as `timelog-resume.json`. Send to `POST /timelogs/start`.

```
{
  "process_step_id": 512,
  "person_id": 33,
  "workstation_id": 7,
  "datetime": "2026-11-03T08:15:00Z"
}
```

```
curl --request POST "$SP_BASE/timelogs/start" 
  --header "x-api-key: $SP_API_KEY" 
  --header "X-Tenant: $SP_TENANT" 
  --header 'Content-Type: application/json' 
  --data-binary @timelog-resume.json
```

### Complete cutting at 08:45 UTC

Save as `timelog-complete.json`. Send to `POST /timelogs/stop`.

```
{
  "process_step_id": 512,
  "person_id": 33,
  "complete": true,
  "datetime": "2026-11-03T08:45:00Z",
  "quantity": 12,
  "faulty_quantity": 0,
  "note": "Cutting complete; ready for welding."
}
```

```
curl --request POST "$SP_BASE/timelogs/stop" 
  --header "x-api-key: $SP_API_KEY" 
  --header "X-Tenant: $SP_TENANT" 
  --header 'Content-Type: application/json' 
  --data-binary @timelog-complete.json
```

**Use the returned time values.** In the current V4 job-completion behavior, `complete: true` records an end time one minute after the supplied datetime. This example returns 08:46 and a 1,860-second resumed session. The earlier paused session lasts 600 seconds. Read both from `/timelogs`; do not assume the submitted stop time is the persisted end time.

Completion finishes the cutting job, not automatically every other step in the order. Re-read production status for welding and the overall order. If a timer request fails or times out, inspect current sessions before retrying; the lifecycle calls are not a general-purpose idempotent event-import interface.

## 9. Make the integration reliable

### Use stable mappings and reconcile uncertain creates

Persist tenant + ERP reference + SkyPlanner resource ID. External references help you find records; they do not provide an idempotency guarantee. Serialize creation for a given ERP record so two workers do not create it concurrently.

After a timeout on order creation, query `GET /orders?external_order_number=ERP-SO-1042` before sending another POST. Inspect all matches and their children. If the outcome is still uncertain, reconcile it instead of blindly retrying. Apply the same principle to customers and workstations.

### Send unambiguous dates and times

Use full RFC3339 timestamps with `Z` or an explicit offset, such as `2026-11-05T14:00:00Z`. Responses may use UTC or the account’s local offset; parse either as an instant. Offset-free inputs use the account timezone. A date-only business deadline means local midnight at the beginning of that date, not the end of the working day. Send the intended deadline explicitly. Invalid dates and nonexistent local times are rejected. Repeated local times during a daylight-saving change require an explicit offset. Accounts configured to store business timestamps in local time cannot preserve both occurrences of a repeated clock time: an instant that cannot be stored and read back unambiguously is rejected even when you supply an offset or `Z`. Check the returned error details rather than removing the offset or changing the intended time to bypass validation.

Do not confuse seconds in duration fields with timestamps. Account calendars, efficiencies, and existing step settings can make actual scheduled elapsed time differ from your base estimate.

### Handle failures by category

| HTTP status | Integration action |
| --- | --- |
| 400 | Check JSON syntax, query names, filter operators, and filter dates. Correct the request before retrying. Invalid dates inside request bodies normally produce 422 validation errors. |
| 401 | Check the API key and tenant configuration. Correct the credentials before retrying. |
| 403 | Inspect the error code and message. Check API access and feature restrictions. For `workstation_limit_reached`, reuse an existing workstation or ask the account administrator to review the subscription limit. Repeated retries will not resolve these conditions. |
| 404 | Check the tenant and ID mapping, and whether the resource is still available. |
| 422 | Read `error.details` for field-specific validation messages; correct the payload or prerequisites. |
| 429 | Honor `Retry-After` when present and use backoff. Quota exhaustion may need a later retry or an account limit change. |
| 5xx / timeout | Retry reads with bounded backoff. Reconcile writes before retrying because the operation may already have succeeded. |

```
{
  "error": {
    "code": "validation_failed",
    "message": "Validation failed",
    "details": {"customer_id": ["The referenced customer does not exist"]}
  }
}
```

*Illustrative validation envelope; exact messages vary.* The `details` field is optional. Some failures may return a different error body, so start with the HTTP status and parse defensively. Log the operation, external reference, HTTP status, and request ID when available; redact credentials.

### Choose polling based on volume

As an initial integration policy, poll active jobs every five minutes and reconcile historical results daily, then tune this with the customer and their API limits. These are suggested intervals, not an API guarantee. Paginate every list and respect throttling. Do not rely only on an order's `modified` timestamp to detect all child, scheduling, or timelog changes.

## 10. Check your first integration

- Authentication works for the intended tenant, and all calls use the V4 base URL.
- Repeated customer/workstation synchronization reuses the saved mappings.
- The sample order creates exactly one row and two steps, with the expected references and quantities.
- The cutting estimate uses seconds per piece plus one-off setup, and workstation capacity is configured.
- Updating quantity or instructions preserves the other rows and steps.
- After scheduling, the integration can read planned times and distinguish business status from production status.
- Timelog imports handle running sessions, multiple sessions, pagination, and later corrections without double counting.
- A timed-out POST is reconciled before retrying, and a 429 triggers backoff.
- The customer and integrator agree who owns each field and how cancellations, corrections, and older records are reconciled.

Keep your account's V4 Swagger reference beside this guide for full field definitions and endpoint-specific requirements. This walkthrough deliberately keeps products, inventory, purchasing, group time tracking, and advanced production resources out of the initial integration.
