Skip to main content

ERP Example Calls

This page provides copy-paste examples for the most common ERP integration calls. Use the cURL / Python selector on each example to view it in your preferred language. Replace the placeholder values with your actual endpoint and authentication token before running.

Placeholders

In every example below, replace:

  • YOUR_PICO_API_ENDPOINT — your Pico API GraphQL URL (e.g. https://yourcompany.picomes.com/graph/v2)
  • YOUR_API_TOKEN — your Pico API authentication token (see Authentication)

The Python examples use the requests library (pip install requests). Selecting cURL or Python on any example switches every example on the page to that language.


1. List Operations

Operations are Pico's representation of your products and processes — they define what can be built or performed. When your ERP creates a work order to request that something be built, Pico represents that as an operation order. Query the available operations to discover what can be ordered. Deploying a product or process in Pico will create or update an operation.

Mapping via the ERP Mapping UI

The ERP Mapping UI flow in sections 7–9 can connect your ERP's products and operations to Pico's for you: ingest them, a Pico manager maps them in the UI, and you read back the mapped picoOperationId. Listing operations and matching in your own code (this section) remains fully supported — it's how many existing integrations work.

You can receive these updates in real time three ways: (a) subscribe to operationsStream over WebSocket — see the Subscriptions Guide for client setup; (b) consume the same subscription as a Server-Sent Events (SSE) stream by POSTing the subscription query with Accept: text/event-stream (example below); or (c) register a webhook with webhookSubscribe — see section 3 for the webhook-style pattern (the same responseFragment approach works with any subscription, including operationsStream).

Use the returned operation id as the operationId when saving an operation order in section 2.

curl -X POST YOUR_PICO_API_ENDPOINT \
-H "Content-Type: application/json" \
-H "x-pico-api-org: YOUR_API_TOKEN" \
-d '{
"query": "{ operations { id externalId name updatedAt } }"
}'

SSE subscription example (real-time stream over HTTP):

curl -N -X POST YOUR_PICO_API_ENDPOINT \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-H "x-pico-api-org: YOUR_API_TOKEN" \
-d '{"query": "subscription { operationsStream(cursor: { initialValue: { updatedAt: \"2024-01-01T00:00:00Z\" } }, batchSize: 100) { id externalId name updatedAt } }"}'

Expected response:

{
"data": {
"operations": [
{
"id": "op-abc-123",
"externalId": "PROD-101",
"name": "Main Assembly",
"updatedAt": "2025-01-15T10:00:00.000Z"
}
]
}
}

2. Save an Operation Order

Use the operationOrderSave mutation to create an operation order — it tells Pico to build a specific product for a specific order (e.g. a sales order from your ERP). ERPs often call this a "work order", but in Pico a work order is the smaller unit the operation order fans out into: one process run by an operator at a station, per the operation's designed workflow.

Required fields:

  • operationId — the Pico operation ID: the mapped picoOperationId read back in section 8, or matched yourself from section 1
  • externalOrderId — your ERP's order identifier for tracking (technically optional in the schema, but strongly recommended for ERP integrations)

Optional fields:

  • stationIds / stationLineIds — limit where the order may be built: any listed station, or any station currently assigned to a listed station line (an ERP "work center" typically maps to a Pico station line). Leaving both empty keeps the order buildable at any station running the operation. IDs are validated at save; station-line assignment changes apply to existing orders immediately. Look the IDs up with the stationLines query — see section 10.
  • See OperationOrderSaveInput for all available fields.

For example, to restrict the order below to one station line, add to the input:

{ "stationLineIds": ["your-station-line-id"] }
curl -X POST YOUR_PICO_API_ENDPOINT \
-H "Content-Type: application/json" \
-H "x-pico-api-org: YOUR_API_TOKEN" \
-d '{
"query": "mutation SaveOrder($input: OperationOrderSaveInput!) { operationOrderSave(input: $input) { message } }",
"variables": {
"input": {
"operationId": "your-operation-id",
"externalOrderId": "ERP-ORDER-123"
}
}
}'

Expected response:

{
"data": {
"operationOrderSave": {
"message": "created"
}
}
}

If an operation order with the same externalOrderId and operationId already exists, the response message will be "updated" instead of "created".


3. Subscribe to Operation Order Completion Webhook

Use the webhookSubscribe mutation to register a webhook that receives a POST request every time an operation order completes.

Required fields:

  • subscriptionID — a URL-safe identifier you choose for this subscription (used to unsubscribe later)
  • responseFragment — a GraphQL fragment that describes which fields to include in the webhook payload (must inherit from one of the subscription response types)
  • webhookUrl — the URL where Pico will POST completion events

Optional fields:

  • webhookHeaders — custom headers to include on every callback (e.g. for authenticating with your endpoint)
curl -X POST YOUR_PICO_API_ENDPOINT \
-H "Content-Type: application/json" \
-H "x-pico-api-org: YOUR_API_TOKEN" \
-d '{
"query": "mutation Subscribe($input: SubscribeRequestInput!) { webhookSubscribe(input: $input) { subscriptionId webhookUrl createdAt } }",
"variables": {
"input": {
"subscriptionID": "erp-order-completions",
"responseFragment": "fragment OrderComplete on OperationOrderComplete { externalOrderId operation { id externalId name } orderIndex at operationSummary { startedAt consumedSerials { resultId attrId value } } endState { producedSerial } }",
"webhookUrl": "https://your-erp-system.com/api/pico-webhooks",
"webhookHeaders": {"X-API-Key": "your-erp-api-key"}
}
}
}'

Expected response:

{
"data": {
"webhookSubscribe": {
"subscriptionId": "erp-order-completions",
"webhookUrl": "https://your-erp-system.com/api/pico-webhooks",
"createdAt": "2025-01-15T10:30:00.000Z"
}
}
}

Webhook Delivery Behavior

Successful delivery: Pico considers a webhook delivered when your endpoint returns a 2xx HTTP status code.

Failed delivery (non-2xx response): If your endpoint returns an HTTP status >= 400, Pico logs the failure. The current event delivery is skipped, but the subscription remains active and will attempt delivery for future events.

Connection retry: If the internal event stream disconnects, Pico automatically reconnects with a 5-second delay between attempts. Reconnection retries indefinitely until the subscription is explicitly unsubscribed.

Extended downtime: If your webhook endpoint is unreachable or consistently failing, events that occur during the outage will not be redelivered. The subscription continues to listen, and delivery resumes for new events once connectivity is restored.


4. Unsubscribe from a Webhook

Use the webhookUnsubscribe mutation with the subscriptionId you used when subscribing.

curl -X POST YOUR_PICO_API_ENDPOINT \
-H "Content-Type: application/json" \
-H "x-pico-api-org: YOUR_API_TOKEN" \
-d '{
"query": "mutation Unsubscribe($id: String!) { webhookUnsubscribe(subscriptionId: $id) { success subscriptionId message } }",
"variables": {
"id": "erp-order-completions"
}
}'

Expected response:

{
"data": {
"webhookUnsubscribe": {
"success": true,
"subscriptionId": "erp-order-completions",
"message": "Subscription cancelled"
}
}
}

5. List Active Webhook Subscriptions

To verify which webhooks are currently active, query webhookSubscriptions:

curl -X POST YOUR_PICO_API_ENDPOINT \
-H "Content-Type: application/json" \
-H "x-pico-api-org: YOUR_API_TOKEN" \
-d '{
"query": "{ webhookSubscriptions { subscriptionId responseFragment webhookUrl createdAt lastEventAt } }"
}'

Expected response:

{
"data": {
"webhookSubscriptions": [
{
"subscriptionId": "erp-order-completions",
"responseFragment": "fragment OrderComplete on OperationOrderComplete { externalOrderId ... }",
"webhookUrl": "https://your-erp-system.com/api/pico-webhooks",
"createdAt": "2025-01-15T10:30:00.000Z",
"lastEventAt": "2025-01-15T14:22:00.000Z"
}
]
}
}

6. Example Webhook Payload

When an operation order completes, Pico sends a POST request to your webhookUrl with the fields specified in your responseFragment. Below is an example payload based on the subscription created in section 3:

{
"externalOrderId": "ERP-ORDER-123",
"operation": {
"id": "op-abc-123",
"externalId": "PROD-101",
"name": "Main Assembly"
},
"orderIndex": 0,
"at": "2025-01-15T14:22:00.000Z",
"operationSummary": {
"startedAt": "2025-01-15T14:00:00.000Z",
"consumedSerials": [
{
"resultId": 84213,
"attrId": "PROD-101-attr-serial-1",
"value": "SN-20250115-001"
},
{
"resultId": 84217,
"attrId": "PROD-101-attr-serial-2",
"value": "SN-20250115-042"
}
]
},
"endState": {
"producedSerial": "ASSY-20250115-007"
}
}

Payload Field Reference

FieldDescription
externalOrderIdThe ERP order identifier you provided when creating the operation order
operation.idPico's internal operation identifier
operation.externalIdThe part number configured in Pico
operation.nameHuman-readable operation name
orderIndex0-based index when multiple of the same operation are on the same order
atISO 8601 timestamp of when the order was completed
operationSummary.startedAtWhen the first process in the operation began
operationSummary.consumedSerialsMaterials consumed during the build (resolve attrId via upfront operation mapping)
endState.producedSerialSerial number of the produced unit (if captured)

7. Ingest ERP Operations for Mapping

Sections 7–9 cover the ERP Mapping UI flow: your ERP pushes its own products and operations (and the serialized materials each consumes) into Pico, a Pico manager maps them to the matching Pico operations in the ERP Mapping UI, and you read the result back to place orders. (The operations-query matching approach in section 1 remains fully supported as a self-managed alternative.) Use the erpOperationsSave mutation to create or update these ERP operations in batch.

Fields per operation:

  • id — your ERP's immutable identifier for the operation (primary key; re-sending the same id updates it)
  • name — display name shown to the mapping manager
  • externalId — your public-facing code / part number (the match key)
  • externalRevision — version/revision string (optional)
  • subs — child sub-operations, each { id, quantity }, conveying your operation hierarchy with an instance count per child (optional)
  • hasPlannedOrder — whether your ERP places orders against this operation. Only operations with a planned order get a mapping decision (see section 8). An operation without a planned order carries no decision in isolation — it is mapped only in the context of an operation with a planned order, as a sub-operation or a group of work, and then produces operationOrderProgress events through that operation's orders.
  • consumedSerials — the serialized materials this operation consumes, each with its own id, name, externalId, and quantity (optional)

The batch below sends two operations: ERP-OP-1001 with two children conveyed via subs, and ERP-OP-1020 — one of those children — also in the batch as its own operation with its own planned order (a subassembly that is both a sub-operation of ERP-OP-1001 and independently ordered). The other child, ERP-OP-1010, would be ingested the same way with hasPlannedOrder: false.

curl -X POST YOUR_PICO_API_ENDPOINT \
-H "Content-Type: application/json" \
-H "x-pico-api-org: YOUR_API_TOKEN" \
-d '{
"query": "mutation IngestERPOperations($input: ERPOperationsSaveInput!) { erpOperationsSave(input: $input) { message } }",
"variables": {
"input": {
"operations": [
{
"id": "ERP-OP-1001",
"name": "Frame Weld Assembly",
"externalId": "FRAME-WELD",
"externalRevision": "B",
"hasPlannedOrder": true,
"subs": [{ "id": "ERP-OP-1010", "quantity": 1 }, { "id": "ERP-OP-1020", "quantity": 1 }],
"consumedSerials": [
{ "id": "ERP-CS-1", "name": "Down Tube", "externalId": "TUBE-DOWN", "quantity": 1 },
{ "id": "ERP-CS-2", "name": "Head Tube", "externalId": "TUBE-HEAD", "quantity": 1 }
]
},
{
"id": "ERP-OP-1020",
"name": "Headset Press-Fit",
"externalId": "HEADSET-PRESS",
"externalRevision": "B",
"hasPlannedOrder": true,
"subs": [],
"consumedSerials": [
{ "id": "ERP-CS-3", "name": "Headset Bearing Set", "externalId": "BEARING-SET", "quantity": 2 }
]
}
]
}
}
}'

Expected response:

{
"data": {
"erpOperationsSave": {
"message": "created"
}
}
}

Re-sending an operation with an id that already exists updates it in place. Send your operations in batches rather than one call per operation.


8. Read Back the Operation Mapping

Once a Pico manager has mapped your ingested operations in the UI, read the result with the erpOperations query. Fetch a single operation by your ERP-assigned id with where (as below, or filter by externalId); without arguments the query returns all of your ERP operations. Typical timing: call it just before creating an operation order (to resolve the current mapping) or when processing a completion event.

  • hasPlannedOrder — echoed back exactly as you ingested it (see section 7). Important for reading the result: only operations with a planned order carry a mapping decision and the fields below.
  • mappings — the Pico operation mappings, one entry per mapped operation. The entry whose operationId equals the operation's own id is its self mapping; any other entries map sub-operations in the context of this operation (optional operations, or sub-operation completion/progress events). Each entry's picoOperationId names the target Pico operation.
  • neverMappedReason / neverMappedAt — set when a manager marked the operation never-to-be-mapped instead of mapping it. neverMappedReason is a stable key (e.g. notBuiltInPico), not display text.
  • orderedViaSubOps — true when the operation is ordered via its sub-operations rather than mapped directly (a specialized case — see below).
  • consumedSerials — echoed back as ingested (slim: id, externalId, name, quantity). Their Pico allocations are not nested here — they're reported in consumedSerialMappings below.
  • consumedSerialMappings — the consumed-serial allocations, each keyed back to a serial by consumedSerialId and allocating it to a Pico attribute marked as a consumed serial (picoAttrId + picoProcessId) with a quantity.

Each operation with a planned order is in one of three states: mapped (mappings has a self entry — pass its picoOperationId to operationOrderSave), never-mapped (neverMappedReason / neverMappedAt set), or pending (neither yet).

Allocations can absorb the subtree: a consumed serial may physically belong to any operation in the subtree, and because allocations are keyed by consumedSerialId on the operation (not nested on the serial), a parent can map everything for its subtree (see ERP-CS-3 below). Drilling in and mapping a sub-operation with its own planned order independently is also valid — parent-level mapping is simply sufficient.

curl -X POST YOUR_PICO_API_ENDPOINT \
-H "Content-Type: application/json" \
-H "x-pico-api-org: YOUR_API_TOKEN" \
-d '{
"query": "{ erpOperations(where: { id: { _eq: \"ERP-OP-1001\" } }) { id name externalId externalRevision subs { id quantity } hasPlannedOrder orderedViaSubOps mappings { operationId picoOperationId mappedAt } neverMappedReason neverMappedAt consumedSerials { id externalId name quantity } consumedSerialMappings { consumedSerialId picoAttrId picoProcessId quantity mappedAt } } }"
}'

Expected response:

{
"data": {
"erpOperations": [
{
"id": "ERP-OP-1001",
"name": "Frame Weld Assembly",
"externalId": "FRAME-WELD",
"externalRevision": "B",
"subs": [{ "id": "ERP-OP-1010", "quantity": 1 }, { "id": "ERP-OP-1020", "quantity": 1 }],
"hasPlannedOrder": true,
"orderedViaSubOps": false,
"mappings": [
{
"operationId": "ERP-OP-1001",
"picoOperationId": "op-frame-weld",
"mappedAt": "2025-01-15T10:00:00.000Z"
},
{
"operationId": "ERP-OP-1010",
"picoOperationId": "op-frame-inspect",
"mappedAt": "2025-01-15T10:02:00.000Z"
}
],
"neverMappedReason": null,
"neverMappedAt": null,
"consumedSerials": [
{ "id": "ERP-CS-1", "externalId": "TUBE-DOWN", "name": "Down Tube", "quantity": 1 },
{ "id": "ERP-CS-2", "externalId": "TUBE-HEAD", "name": "Head Tube", "quantity": 1 },
{ "id": "ERP-CS-3", "externalId": "BEARING-SET", "name": "Headset Bearing Set", "quantity": 2 }
],
"consumedSerialMappings": [
{
"consumedSerialId": "ERP-CS-1",
"picoAttrId": "attr-downtube",
"picoProcessId": "proc-frame-weld",
"quantity": 1,
"mappedAt": "2025-01-15T10:05:00.000Z"
},
{
"consumedSerialId": "ERP-CS-2",
"picoAttrId": "attr-headtube",
"picoProcessId": "proc-frame-weld",
"quantity": 1,
"mappedAt": "2025-01-15T10:05:00.000Z"
},
{
"consumedSerialId": "ERP-CS-3",
"picoAttrId": "attr-bearing",
"picoProcessId": "proc-frame-weld",
"quantity": 2,
"mappedAt": "2025-01-15T10:06:00.000Z"
}
]
}
]
}
}

Walking the example

  • The self mapping — the mappings entry with operationId: "ERP-OP-1001" — points at op-frame-weld. That picoOperationId is what you pass to operationOrderSave in section 9.
  • The in-context entry maps the sub-operation ERP-OP-1010 (which has no planned order of its own) to op-frame-inspect — use it to correlate that sub-operation's operationOrderProgress events.
  • ERP-CS-3 is absorbed from the subtree: it physically belongs to the sub-operation ERP-OP-1020, but its allocation is carried here at the parent. ERP-OP-1020 has its own planned order — fetch it by its own id when you order it independently.

Reading the mapping states

StateHow to detect it
MappedhasPlannedOrder: true; mappings contains a self entry (operationId == the operation's id). May also carry in-context sub-op entries plus consumedSerials / consumedSerialMappings.
Never-mappedhasPlannedOrder: true; mappings empty; neverMappedReason set (e.g. notBuiltInPico); neverMappedAt set.
Ordered via sub-operationshasPlannedOrder: true; orderedViaSubOps: true; mappings empty — expect mappings and completions at its sub-operations instead (see below).
Pending (undecided)hasPlannedOrder: true; mappings empty; neverMapped* null; orderedViaSubOps: false.
No planned orderhasPlannedOrder: false, as ingested — carries no mapping decision or serials of its own; it may still appear in a parent's in-context mappings.

Ordered via sub-operations

A specialized case worth knowing once the basics are in place: orderedViaSubOps: true marks an operation with a planned order that is ordered via its sub-operations instead of being mapped directly: Pico builds its work through the operation's own sub-operations with planned orders — each of those maps on its own and carries its own consumed serials. When you see the flag, expect mappings — and completions — at the sub-operations rather than at the top-level operation.

Each consumedSerialMappings entry is one Pico target for one ERP serial, identified by consumedSerialId (which may belong to a sub-operation in the planned-order operation's subtree). picoAttrId is the target Pico attribute (marked as a consumed serial) and picoProcessId disambiguates the process variant; quantity is how much of the serial's total was allocated to that target. A serial can be split across several targets (several entries with the same consumedSerialId) or be left unallocated (no entry).


9. Create the Work Order from the Mapped Operation

Take the operation's self mapping from section 8 — the mappings entry whose operationId equals the ordered operation's own id — and pass its picoOperationId as the operationId on operationOrderSave to order the build. This closes the loop: your ERP ingests its operations, a manager maps them, and your ERP orders builds against the mapped Pico operation without hard-coding Pico ids.

curl -X POST YOUR_PICO_API_ENDPOINT \
-H "Content-Type: application/json" \
-H "x-pico-api-org: YOUR_API_TOKEN" \
-d '{
"query": "mutation SaveOrder($input: OperationOrderSaveInput!) { operationOrderSave(input: $input) { message } }",
"variables": {
"input": {
"operationId": "op-abc-123",
"externalOrderId": "ERP-ORDER-123"
}
}
}'

Only map-complete operations should be ordered: skip any operation with a planned order but no self mapping (still pending, ordered via sub-operations, or never-mapped). Operations without a planned order (hasPlannedOrder: false) have no mapping to order against in the first place — they're structural, not something your ERP orders directly. For an operation ordered via sub-operations, order its mapped sub-operations (the ones with their own planned orders) instead. See section 2 for the full set of operationOrderSave options.


10. List Station Lines and Their Stations

Use the stationLines query to discover the IDs accepted by the stationIds and stationLineIds restrictions in section 2. Each station is returned with its name, nested under the line it is assigned to — an ERP "work center" typically maps to a Pico station line.

curl -X POST YOUR_PICO_API_ENDPOINT \
-H "Content-Type: application/json" \
-H "x-pico-api-org: YOUR_API_TOKEN" \
-d '{
"query": "query { stationLines { lines { id name stations { id name } } unassignedStations { id name } } }"
}'

Example response:

{
"data": {
"stationLines": {
"lines": [
{
"id": "128d1d99-2211-4179-b1d2-0be106474deb",
"name": "Line 1",
"stations": [
{ "id": "cudpi3d7u6gc6o0e5un0", "name": "Assembly 1" }
]
},
{
"id": "a26d27cc-3a1a-407d-94e9-4336b677b39f",
"name": "Line 2",
"stations": null
}
],
"unassignedStations": [
{ "id": "d03s5c57u6gc1agkasa0", "name": "Rework" }
]
}
}
}

Notes:

  • Every station appears exactly once — either under the line it is assigned to, or in unassignedStations.
  • A line with no stations assigned yet returns null for stations, as Line 2 does above.
  • Station line assignments are read live, so a station moved to a different line in the Pico manage UI is reflected on the next call and immediately changes which orders that station can build.

See Also