API Reference
Every endpoint in the Pulsar Public API, grouped by resource, and the conventions they share.
Browse endpoints using the sidebar, grouped by resource (Machines, Operators, SKUs, Production logs, Reports, and so on). Each endpoint page includes parameters, request/response schemas, and a live request builder with generated code samples.
Conventions
- Path and query parameters are
snake_case, and JSON bodies arecamelCase. Exceptions: the signals endpoints also answer insnake_case, andGET /v1/shiftstakesmachineId, matching the field it returns. - Searching is a
POSTto a/filterssub-path, because the filter bodies nest. - Searches are paginated:
pagestarts at 1 andlimitdefaults to 10, and the response wraps rows in atotalCount/totalPages/currentPageenvelope. Other lists, such asGET /v1/machinesand theGET /v1/shiftsroutes, return a plain JSON array without paging. - Creating records with a
POSTto a collection, such as/v1/operatorsor/v1/production-logs, takes a JSON array, even for a single record. Actions on one existing record, such as assigning an operator to a shift or adding defects to it, take a single JSON object. - Machine and operator ids are integers. Signal, SKU, work order, production log and nominal speed ids are UUID strings. The signals endpoints take the machine's
uuid, not its integer id. - A time without a UTC offset is read as UTC.
Permissions
Each endpoint needs one scope, named section.action, such as shifts.read or production_logs.delete. Every endpoint page opens with its Required scope, and the shift write endpoints also name the related scopes an integration needs to check or undo what it sends. Keys created in the Pulsar app under Settings → Integrations carry every public scope. A key without the scope an endpoint needs gets 403 with the code missing_scope, and detail names the missing scope:
{ "code": "missing_scope", "detail": "Not enough permissions. Missing scope: shifts.read" }Errors
Every error except 422 has the same body: a stable code to branch on, and a human-readable detail that may change, so don't parse it.
{ "code": "shift_not_found", "detail": "No shift of yours matches the request." }Each endpoint's description and responses list the codes it returns, such as shift_not_found, insufficient_production or idempotency_key_reused. New codes may be added, so handle an unknown one by its HTTP status. Without a more specific code, each status has a generic one:
| Status | Code | Meaning |
|---|---|---|
400 | invalid_request | The request was rejected. |
401 | unauthenticated | The x-api-key header is missing, or the key isn't valid in this environment. See Environments. |
403 | forbidden | The key can't perform this request. A missing scope is missing_scope. |
404 | not_found | The resource doesn't exist. |
409 | conflict | The request conflicts with the current state. |
429 | rate_limited | The key's rate limit was reached. Wait the number of seconds in the Retry-After header, then send the request again. |
5XX | internal_error | The request failed on Pulsar's side, and its outcome is unknown. Read Retries before sending it again. |
A 422 means the body or a parameter failed validation. Its detail is a list, and each item's loc points to the field that failed.
Retries
Whether a failed call recorded anything depends on how it failed:
- A
4XXresponse means the request was rejected and nothing was recorded. Fix it and send it again. - A timeout, a dropped connection or a
5XXresponse leaves the outcome unknown. The request may have been recorded even though you never got the response.
Calls that record production or defects add to what is already there, so a request sent twice is recorded twice. Three endpoints accept an Idempotency-Key header that makes a retry safe: POST /v1/production-logs, POST /v1/shifts/defects and POST /v1/shifts/{shift_id}/defects. Generate a unique value for each request, such as a UUID, and send the same key and the same body on every retry of that request:
- If the original request was recorded, the retry records nothing and returns the original response, with the header
Idempotent-Replayed: true. - If the original was not recorded, the retry is processed as a new request.
- Reusing a key with a different body or URL returns
409. So does a retry while the original is still being processed: wait a moment and retry again.
Keys are shared by all of your company's API keys, and each is kept for 24 hours. After that, a request with the same key is processed as a new one.
Without an Idempotency-Key, check what was recorded before you send the request again. GET /v1/shifts/{shift_id}/defects lists a shift's defects, and POST /v1/production-logs/filters finds production logs, also by shift with "shifts": {"include": [<shift id>]}.
Covering a long period
GET /v1/shifts accepts a range of up to 31 days. To cover a longer period, split it into consecutive ranges, where each range's to is the next one's from, and add match=start. Each shift then comes back exactly once. With the default match=overlap, a shift that crosses the boundary between two ranges comes back in both, so deduplicate by id before acting on it.
Environments
Production and the staging sandbox, and when to use each.
List machines GET
Return every machine the caller's company can see. Machines are the entry point to the rest of the API. Signals take the uuid returned here; production logs, nominal speeds, realtime and reports still take the integer id. uuid is the machine's stable public identifier; it is null for machines that do not have one yet, and those cannot be queried for signals. The list is not paginated and takes no filters — it is scoped to the company resolved from the credential, so there is no company parameter to pass.