Skip to content
Experimental API

Experimental API

The Experimental API is a Beta-only GraphQL endpoint for customers who are testing new ShipHero API capabilities before they are added to the standard Public API.

Access is not enabled by default. If you want to use the Experimental API, request access through your Customer Success Manager (CSM). Your CSM will confirm whether your account is eligible and enable access for your account.

Once access is enabled, use the standard GraphQL endpoint:

https://public-api.shiphero.com/graphql/experimental

Because the Experimental API can change frequently, fetch or inspect the latest GraphQL schema from this endpoint before building or updating an integration.

The endpoint is called Experimental because the operations exposed here are still under testing and active development. Available queries, mutations, fields, inputs, and response shapes may change.
Experimental operations are not available through the ShipHero MCP. The MCP only exposes the standard Public API. To use Experimental operations from an AI agent, install the Public API Skill and ask the agent to run its queries against the Experimental endpoint above. See the ShipHero AI Toolkit page for setup instructions.

If you have access, please share feedback quickly with your ShipHero contact. Feedback from Beta testers helps shape the final API before it becomes generally available.

Currently Available

Today, the Experimental API can be used to:

  • Query available carriers and shipping methods with carriers and shipping_methods queries
  • Request label printing with the label_print mutation
  • Get shipping rates without creating a label with the label_quote mutation
  • Create an inbound shipment from an Advance Shipment Notice with the inbound_shipment_create_from_asn mutation
  • Read and manage Workforce Hero labor activities with the labor_worker_activities query and the labor_activity_start, labor_activity_finish, labor_activity_modify, and labor_activity_remove mutations

Connected Carriers Query

Use the carriers query to retrieve the carriers and shipping methods currently available to your account.

query {
  carriers {
    request_id
    complexity
    data {
      carrier
      carrier_name
      shipping_methods {
        carrier
        carrier_name
        method
        method_name
      }
    }
  }
}

Example response:

{
  "data": {
    "carriers": {
      "request_id": "...",
      "complexity": 2,
      "data": [
        {
          "carrier": "amazon",
          "carrier_name": "Amazon",
          "shipping_methods": [
            {
              "carrier": "amazon",
              "carrier_name": "Amazon",
              "method": "UPS_PTP_2ND_DAY_AIR",
              "method_name": "UPS 2nd Day Air"
            },
            {
              "carrier": "amazon",
              "carrier_name": "Amazon",
              "method": "UPS_PTP_GND",
              "method_name": "UPS Ground"
            }
          ]
        },
        {
          "carrier": "genericlabel",
          "carrier_name": "Generic",
          "shipping_methods": [
            {
              "carrier": "genericlabel",
              "carrier_name": "Generic",
              "method": "genericlabel",
              "method_name": "genericlabel"
            }
          ]
        },
        {
          "carrier": "ups",
          "carrier_name": "UPS",
          "shipping_methods": [
            {
              "carrier": "ups",
              "carrier_name": "UPS",
              "method": "UPS Ground",
              "method_name": "UPS Ground"
            },
            {
              "carrier": "ups",
              "carrier_name": "UPS",
              "method": "UPS Next Day Air",
              "method_name": "UPS Next Day Air"
            }
          ]
        }
      ]
    }
  },
  "extensions": {
    "throttling": {
      "estimated_complexity": 2,
      "cost": 1,
      "cost_detail": {
        "carriers": {
          "items_count": 0,
          "cost": 1,
          "total_cost": 1,
          "fields": {}
        }
      },
      "user_quota": {
        "credits_remaining": 4003,
        "max_available": 4004,
        "increment_rate": 60
      }
    }
  }
}

Note

The carriers and methods returned by this query can vary by account configuration and by the current Experimental API release.

Label Print Mutation

Use label_print to request label printing for an order. The payload identifies the order, carrier, method, whether an invoice should print, and the package contents.

The mutation returns labels directly under data.label_print.labels. It does not return a top-level shipment object, but each label can include shipment data such as shipment_id and shipment_line_items when those fields are selected.

Required OAuth Scope

The label_print mutation requires the granted OAuth scope:

change:shipments

If the issued token does not include this scope, the mutation returns a permission error:

{
  "errors": [
    {
      "message": "Missing required scope(s): change:shipments",
      "operation": "label_print",
      "field": "label_print",
      "request_id": "...",
      "code": 7
    }
  ],
  "data": {
    "label_print": null
  }
}

Note

Requesting change:shipments in the OAuth flow is not enough. The issued token must actually include the granted change:shipments scope. Some OAuth clients or accounts may not be allowed to receive write scopes.

Mutation

mutation Label_print($data: PrintLabelInput!) {
  label_print(data: $data) {
    request_id
    complexity
    labels {
      id
      legacy_id
      account_id
      shipment_id
      order_id
      box_id
      box_name
      status
      tracking_number
      alternate_tracking_id
      order_number
      order_account_id
      carrier
      shipping_name
      shipping_method
      cost
      box_code
      device_id
      delivered
      picked_up
      refunded
      needs_refund
      profile
      partner_fulfillment_id
      full_size_to_print
      packing_slip
      warehouse
      warehouse_id
      insurance_amount
      carrier_account_id
      source
      created_date
      tracking_url
      package_number
      parcelview_url
      tracking_status
      in_shipping_container
      shipping_container_id
      label {
        pdf_location
        paper_pdf_location
        thermal_pdf_location
        image_location
      }
      shipment_line_items {
        edges {
          cursor
          node {
            id
            legacy_id
            line_item_id
            shipment_id
            shipping_label_id
            quantity
            line_item {
              id
              legacy_id
              sku
              partner_line_item_id
              product_id
              quantity
              price
              product_name
              option_title
              fulfillment_status
              quantity_pending_fulfillment
              quantity_shipped
              warehouse
              quantity_allocated
              backorder_quantity
              custom_options
              custom_barcode
              eligible_for_return
              customs_value
              warehouse_id
              locked_to_warehouse_id
              subtotal
              barcode
              created_at
              updated_at
              order_id
              promotion_discount
            }
          }
        }
      }
    }
  }
}

Variables

This example uses genericlabel and explicit package dimensions.

{
  "data": {
    "order_id": "{{order_id}}",
    "shipping_carrier": "genericlabel",
    "shipping_method": "genericlabel",
    "print_invoice": false,
    "packages": [
      {
        "dimensions": {
          "weight": 0.0625,
          "height": 1,
          "width": 1,
          "length": 1
        },
        "line_items": [
          {
            "line_item_id": "{{line_item_id}}",
            "quantity": 1
          }
        ]
      }
    ]
  }
}

Before calling label_print, use the carriers query to retrieve the carrier and method values available for the account. For genericlabel, the carrier result looks like this:

{
  "carrier": "genericlabel",
  "carrier_name": "Generic",
  "shipping_methods": [
    {
      "carrier": "genericlabel",
      "carrier_name": "Generic",
      "method": "genericlabel",
      "method_name": "genericlabel"
    }
  ]
}

Package Dimensions Input

PrintLabelPackageInput supports dimensions as an alternative to carrier_box_code or custom_box_id. You can also send custom_box_id instead of carrier_box_code when you want to print the label with a custom box.

The PrintLabelDimensionsInput fields are:

weight: Float!
height: Float!
width: Float!
length: Float!

Example:

{
  "dimensions": {
    "weight": 0.0625,
    "height": 1,
    "width": 1,
    "length": 1
  }
}

Response

Example response:

{
  "data": {
    "label_print": {
      "request_id": "...",
      "complexity": 15,
      "labels": [
        {
          "id": "...",
          "legacy_id": 12345,
          "shipment_id": "...",
          "order_id": "{{order_id}}",
          "status": "valid",
          "tracking_number": "",
          "order_number": "EXAMPLE-1001",
          "carrier": "genericlabel",
          "shipping_method": "genericlabel",
          "cost": "0.00",
          "warehouse": "Primary",
          "warehouse_id": "12345",
          "created_date": "2026-06-22T19:58:51",
          "tracking_url": "",
          "package_number": 1,
          "tracking_status": "UNKNOWN",
          "label": {
            "pdf_location": "https://example.com/shipping-labels/example-1001/label.pdf",
            "paper_pdf_location": "https://example.com/shipping-labels/example-1001/paper-label.pdf",
            "thermal_pdf_location": "https://example.com/shipping-labels/example-1001/thermal-label.pdf",
            "image_location": "[\"https://example.com/shipping-labels/example-1001/label.pdf\", \"https://example.com/shipping-labels/example-1001/label.png\"]"
          },
          "shipment_line_items": {
            "edges": [
              {
                "cursor": "...",
                "node": {
                  "id": "...",
                  "legacy_id": 12345,
                  "line_item_id": "{{line_item_id}}",
                  "shipment_id": "...",
                  "shipping_label_id": "...",
                  "quantity": 1,
                  "line_item": {
                    "id": "{{line_item_id}}",
                    "legacy_id": 12345,
                    "sku": "EXAMPLE-SKU",
                    "product_name": "Example Product",
                    "quantity": 1,
                    "quantity_shipped": 1,
                    "order_id": "{{order_id}}"
                  }
                }
              }
            ]
          }
        }
      ]
    }
  }
}

Note

For generic labels, tracking_number and tracking_url may be empty strings.

Note

The image_location field is returned as a JSON-encoded string containing label asset URLs.

Label Quote Mutation

Use label_quote to get shipping rates for an order and a set of packages without creating a label. Use it to preview costs or pick a carrier and method before calling label_print.

The quote is run under the label-printer user of the warehouse assigned to the order.

Required OAuth Scope

The label_quote mutation requires the granted OAuth scope:

view:shipments

The mutation has a complexity cost of 15.

Input

LabelQuoteInput fields:

FieldTypeRequiredDescription
order_idStringYesPublic API order ID.
shipping_carrierStringNoCarrier to quote. Omit to use the order’s shipping carrier. Use "cheapest" to quote against your cheapest-carrier profiles.
shipping_methodStringNoCarrier method to quote. Omit to use the order’s shipping method.
include_partial_ratesBooleanNoDefaults to false. When true, carrier errors are returned in carrier_errors together with any successful rates instead of failing the mutation.
max_delivery_daysIntNoMaximum transit days allowed for returned rates.
packages[PrintLabelPackageInput!]!YesPackages to quote. Same shape as label_print: each package has line_items (line_item_id, quantity) plus either custom_box_id, carrier_box_code, or explicit dimensions.

custom_box_id and carrier_box_code are mutually exclusive: sending both is rejected.

Mutation

mutation {
  label_quote(
    data: {
      order_id: "T3JkZXI6MTIzNA=="
      shipping_carrier: "ups"
      include_partial_rates: true
      max_delivery_days: 5
      packages: [
        {
          line_items: [{ line_item_id: "TGluZUl0ZW06MTIzNA==", quantity: 2 }]
          dimensions: { weight: 24.0, length: 12.5, width: 8.0, height: 4.0 }
        }
      ]
    }
  ) {
    request_id
    complexity
    rates {
      name
      value
      cost
      carrier
      box_to_use
    }
    cheapest {
      name
      value
      cost
      carrier
    }
    errors
    warnings
    carrier_errors {
      carrier
      errors
    }
  }
}

Response

FieldTypeDescription
rates[LabelQuoteRate]Rates returned for the requested carrier and packages.
cheapestLabelQuoteRateLowest-cost rate among rates.
errors[String]Top-level quote errors (only present when include_partial_rates: true; otherwise the mutation fails).
warnings[String]Non-blocking warnings.
carrier_errors[LabelQuoteCarrierError]Per-carrier errors from partial results (carrier, errors).

Each LabelQuoteRate contains name (carrier display name for the method), value (carrier method code — pass it back as shipping_method to label_print), cost (quoted cost as returned by the carrier), carrier, and box_to_use (carrier-specific packaging hint).

Example response:

{
  "data": {
    "label_quote": {
      "request_id": "...",
      "complexity": 15,
      "rates": [
        { "name": "UPS Ground", "value": "ups_ground", "cost": "12.34", "carrier": "ups", "box_to_use": null },
        { "name": "UPS 2nd Day Air", "value": "ups_2nd_day_air", "cost": "28.90", "carrier": "ups", "box_to_use": null }
      ],
      "cheapest": { "name": "UPS Ground", "value": "ups_ground", "cost": "12.34", "carrier": "ups" },
      "errors": [],
      "warnings": [],
      "carrier_errors": []
    }
  }
}

Note

If the carrier returns errors and include_partial_rates is false (default), the mutation fails with those errors in the GraphQL errors array. With include_partial_rates: true the same messages are returned in errors / carrier_errors and any successful rates are still returned.

Note

Line item quantities are validated against the order; quoting more units than remain unshipped fails.

Create Inbound Shipment from ASN

Use inbound_shipment_create_from_asn to create an inbound shipment from an Advance Shipment Notice (ASN): the inbound counterpart to a shipping label. Where a supplier would otherwise send an EDI 856 or EDI 943 document, this mutation accepts a normalized JSON payload describing what is arriving, and creates the corresponding ShipHero records so the shipment can be received at the dock.

From a single submission the mutation:

  1. Creates a Purchase Order and its line items (one per SKU).
  2. Creates an Inbound Shipment linked to that Purchase Order, with status PENDING_ARRIVAL.
  3. Creates the expected inbound line items (quantity per SKU).
  4. Stores the full ASN payload — including the declared quantity, UOM, lot, and expiration date for each line item — so warehouse workers can look it up by scanning any container barcode (SSCC) when the truck arrives.

The mutation always returns a 997-style acknowledgment envelope describing whether the ASN was Accepted or Rejected, echoing the original document metadata back to the sender.

Note

The ASN declares what you expect to receive — it does not itself update inventory. The actual received quantity, UOM, lot, and expiration date for each item are determined by the warehouse team during physical receiving, and that data is ShipHero’s source of truth for inventory.

If a pallet or box is received exactly as declared in the ASN, the declared quantity, UOM, lot, and expiration date are copied to inventory automatically. If the warehouse team receives it differently than declared (a different quantity, a different lot, and so on), they enter the actual values manually during receiving, and those manually entered values are what gets recorded — not what was declared in the ASN.

Required OAuth Scopes

The inbound_shipment_create_from_asn mutation requires both granted OAuth scopes:

change:inbound_shipments
change:purchase_orders

If the issued token does not include both scopes, the mutation returns a permission error:

{
  "errors": [
    {
      "message": "Missing required scope(s): change:inbound_shipments, change:purchase_orders",
      "operation": "inbound_shipment_create_from_asn",
      "field": "inbound_shipment_create_from_asn",
      "request_id": "...",
      "code": 7
    }
  ],
  "data": {
    "inbound_shipment_create_from_asn": null
  }
}

Note

Requesting these scopes in the OAuth flow is not enough. The issued token must actually include both granted scopes. Some OAuth clients or accounts may not be allowed to receive write scopes.

Packing Structure

The payload describes packaging with a recursive packing_units tree, which lets a single input shape represent both flat (943-style) and hierarchical (856-style) shipments:

  • A packing unit with nested packing_units is treated as a pallet.
  • A packing unit without nested packing_units is treated as a box, and carries the line items directly.
Hierarchical (856-style):                 Flat (943-style):

packing_units:                            packing_units:
  - sscc: PALLET-AAA                        - sscc: BOX-BBB
    packing_units:                            line_items:
      - sscc: BOX-BBB                           - sku: WIDGET-A, qty: 50
        line_items:
          - sku: WIDGET-A, qty: 50

Each unit optionally carries an sscc (the shipper’s GS1 container barcode) and a po_number. A po_number set on an outer unit (for example, a pallet) is automatically inherited by its nested units that don’t set their own — you don’t need to repeat it on every box. Purchase Orders are grouped by the distinct po_number values found across the tree, after inheritance is applied. If no unit specifies a po_number, ShipHero auto-generates one for the shipment.

Mutation

mutation InboundShipmentCreateFromAsn($data: CreateInboundShipmentFromAsnInput!) {
  inbound_shipment_create_from_asn(data: $data) {
    request_id
    complexity
    document_type
    document_id
    created_at
    interchange {
      sender_id
      sender_qualifier
      receiver_id
      receiver_qualifier
      control_number
    }
    functional_group {
      functional_id_code
      version
      control_number
      original_transaction_set_id
    }
    acknowledgment {
      original_transaction_set_id
      original_external_shipment_id
      original_vendor_name
      original_ship_date
      accepted_count
      rejected_count
      status
      status_description
    }
    result {
      inbound_shipment {
        id
        legacy_id
        bol_reference
        warehouse_id
        status
        carrier
        pro_number
        container_number
        scheduled_at
        estimated_truck_arrival
        pallets_quantity
        number_carton
        booking_contact_name
        booking_contact_email
        booking_contact_number
      }
      purchase_orders {
        id
        legacy_id
        po_number
        line_items {
          edges {
            node {
              id
              sku
              quantity
            }
          }
        }
      }
      asn_document_id
    }
    errors {
      code
      segment
      element
      qualifier
      value
      message
    }
  }
}

Note

result.inbound_shipment and result.purchase_orders are the standard InboundShipment and PurchaseOrder types, so you can select any field those types expose (including the full line_items and purchase_orders connections). Object ids are the encoded global id; use legacy_id when you need the numeric id for legacy endpoints. The booking_contact_* fields require the view:pii scope.

Variables

This example is a single-box (943-style) shipment for one SKU.

{
  "data": {
    "external_shipment_id": "BOL-001",
    "warehouse_id": "{{warehouse_id}}",
    "vendor_name": "Acme Co",
    "edi_type": "943",
    "packing_units": [
      {
        "sscc": "00012345600000000018",
        "po_number": "PO-001",
        "line_items": [
          {
            "product_ids": [{ "qualifier": "SK", "value": "{{sku}}" }],
            "quantity": 10,
            "uom": "EA",
            "lot": {
              "number": "LOT-2026-0715",
              "expiration_date": "2027-07-15"
            }
          }
        ]
      }
    ]
  }
}

Note

lot declares the expected lot/expiration for this item, as described above — it’s stored with the ASN for reference but is only copied to inventory if the warehouse team receives the item exactly as declared. Otherwise, the worker enters the actual lot/expiration manually at receiving time.

Required Input Fields

FieldDescription
external_shipment_idBOL, shipment ID, or any shipper reference. Must be unique per account.
warehouse_idThe receiving warehouse (encoded Public API id).
vendor_nameThe shipping vendor / trading partner name.
edi_typeThe source document type: "856" or "943".
packing_unitsRecursive list of pallets and boxes carrying the line items.

Each line item requires product_ids, quantity, and uom. Supported uom codes are EA, CA, PL, CT, and BX. Line items can also optionally declare a lot (with a required number and optional expiration_date) to state the expected lot for that item; only a single declared lot per line item is supported today (see AMBIGUOUS_LOT_NOT_SUPPORTED below).

Optional shipment fields include edi_version, trading_partner_id, carrier, pro_number, container_number, ship_date, estimated_arrival, the vendor_* address fields, and the booking_contact_* fields.

Note

In the current release, products can only be identified by ShipHero SKU. Send qualifier: "SK" and set value to the exact SKU. Other identifiers (UPC, GTIN, vendor part number) are not yet resolved.

Response

When the ASN is accepted, acknowledgment.status is "A" and result contains the created records.

{
  "data": {
    "inbound_shipment_create_from_asn": {
      "request_id": "...",
      "complexity": 15,
      "document_type": "997",
      "document_id": "997-20260715-1a2b3c4d",
      "created_at": "2026-07-15T12:00:00",
      "interchange": {
        "sender_id": "SHIPHERO",
        "sender_qualifier": "ZZ",
        "receiver_id": "ACME-EDI",
        "receiver_qualifier": "ZZ",
        "control_number": "a1b2c3d4e"
      },
      "functional_group": {
        "functional_id_code": "FA",
        "version": "004010",
        "control_number": "f5e6d7c8b",
        "original_transaction_set_id": "943"
      },
      "acknowledgment": {
        "original_transaction_set_id": "943",
        "original_external_shipment_id": "BOL-001",
        "original_vendor_name": "Acme Co",
        "original_ship_date": null,
        "accepted_count": 1,
        "rejected_count": 0,
        "status": "A",
        "status_description": "Accepted"
      },
      "result": {
        "inbound_shipment": {
          "id": "SW5ib3VuZFNoaXBtZW50Ojc4OQ==",
          "legacy_id": 789,
          "bol_reference": "BOL-001",
          "warehouse_id": "V2FyZWhvdXNlOjQ1Ng==",
          "status": "PENDING_ARRIVAL",
          "carrier": null,
          "pro_number": null,
          "container_number": null,
          "scheduled_at": null,
          "estimated_truck_arrival": null,
          "pallets_quantity": 0,
          "number_carton": 1,
          "booking_contact_name": null,
          "booking_contact_email": null,
          "booking_contact_number": null
        },
        "purchase_orders": [
          {
            "id": "UHVyY2hhc2VPcmRlcjoxMDE=",
            "legacy_id": 101,
            "po_number": "PO-001",
            "line_items": {
              "edges": [
                { "node": { "id": "...", "sku": "WIDGET-A", "quantity": 10 } }
              ]
            }
          }
        ],
        "asn_document_id": "..."
      },
      "errors": []
    }
  }
}

Rejection Envelope

Validation failures do not raise a GraphQL error. Instead the mutation returns a rejection envelope: acknowledgment.status is "R", result is null, and errors lists what went wrong. Each error maps back to the originating EDI segment where applicable.

{
  "data": {
    "inbound_shipment_create_from_asn": {
      "document_type": "997",
      "acknowledgment": {
        "status": "R",
        "status_description": "Rejected",
        "accepted_count": 0,
        "rejected_count": 1
      },
      "result": null,
      "errors": [
        {
          "code": "PRODUCT_NOT_FOUND",
          "segment": "W04",
          "element": "W0406",
          "qualifier": "SK",
          "value": "SKU-DOES-NOT-EXIST",
          "message": "Could not resolve product identifiers: SKU:SKU-DOES-NOT-EXIST"
        }
      ]
    }
  }
}

The error code values are:

CodeMeaning
WAREHOUSE_NOT_FOUNDwarehouse_id does not belong to the account.
DUPLICATE_SHIPMENTAn inbound shipment already exists for this external_shipment_id.
NO_LINE_ITEMSThe shipment contains no line items.
PRODUCT_NOT_FOUNDA SKU could not be resolved for the account.
INVALID_SSCC_LENGTHAn SSCC exceeds the 22-character limit.
AMBIGUOUS_LOT_NOT_SUPPORTEDAn item references multiple lots without a per-lot quantity.
MAX_NESTING_DEPTH_EXCEEDEDpacking_units nesting exceeds the maximum depth of 4 levels.

Note

external_shipment_id must be unique per account. Re-submitting the same value returns a DUPLICATE_SHIPMENT rejection rather than overwriting the existing shipment.

Labor Activities (Workforce Hero)

These operations expose Workforce Hero labor activities through the Experimental API: one query to audit a worker’s activities and four mutations to start, finish, correct, and remove them.

Prerequisites

  • All timestamps are UTC. Naive (zone-less) values are treated as UTC.

Required OAuth Scopes

OperationScope
labor_worker_activitiesview:labor
labor_activity_start, labor_activity_finish, labor_activity_modify, labor_activity_removechange:labor

Shared Type: LaborActivity

FieldTypeDescription
idStringWorkforce Hero activity identifier.
started_atISODateTimeUTC-normalized start timestamp.
finished_atISODateTimeUTC-normalized finish timestamp (null while ongoing).
last_activity_atISODateTimeUTC-normalized timestamp of the last activity update.
noteStringActivity note.
statusStringStatus reported by Workforce Hero (e.g. "ACTIVE", "FINISHED").
tags[String]Activity tags.
workerLaborActivityWorkerid, name, user_id (ShipHero user ID).
jobLaborActivityJobid, name, type (e.g. "PICKING").

Typical Workflow

  1. labor_activity_start — clock a worker onto a job (or backfill a completed shift).
  2. labor_activity_finish — close the ongoing activity.
  3. labor_activity_modify — fix timestamps/job on the finished activity if needed.
  4. labor_worker_activities — audit what was recorded for the worker.
  5. labor_activity_remove — delete a finished activity that should not exist.

Labor Worker Activities Query

Use labor_worker_activities to return a paginated list of Workforce Hero activities for one worker.

ArgumentTypeRequiredDescription
worker_idStringYesWorkforce Hero worker identifier. Must be an active worker.
job_idStringNoFilter by Workforce Hero job identifier.
date_fromISODateTimeNoExclusive activity start boundary. Defaults to UTC now minus 24 hours. Must be between 14 days in the past and 24 hours in the future.
unfinishedBooleanNotrue returns only ongoing activities, false only finished ones; omit for both.
firstIntNoPage size. Defaults to 20; must be between 1 and 100.
cursorStringNoend_cursor from a previous page.
query {
  labor_worker_activities(
    worker_id: "1234"
    date_from: "2026-09-16T00:00:00Z"
    unfinished: false
    first: 50
  ) {
    request_id
    complexity
    data {
      edges {
        node {
          id
          started_at
          finished_at
          last_activity_at
          note
          status
          tags
          worker { id name user_id }
          job { id name type }
        }
      }
      page_info {
        has_next_page
        has_previous_page
        start_cursor
        end_cursor
      }
    }
  }
}

Example response:

{
  "data": {
    "labor_worker_activities": {
      "request_id": "...",
      "complexity": 1,
      "data": {
        "edges": [
          {
            "node": {
              "id": "activity-123",
              "started_at": "2026-09-16T09:00:00+00:00",
              "finished_at": "2026-09-16T10:00:00+00:00",
              "last_activity_at": "2026-09-16T10:00:00+00:00",
              "note": null,
              "status": "FINISHED",
              "tags": [],
              "worker": { "id": "1234", "name": "Ada Lovelace", "user_id": 5678 },
              "job": { "id": "job-123", "name": "Picking", "type": "PICKING" }
            }
          }
        ],
        "page_info": {
          "has_next_page": false,
          "has_previous_page": false,
          "start_cursor": "cG9zaXRpb246MA==",
          "end_cursor": "cG9zaXRpb246MA=="
        }
      }
    }
  }
}

To fetch the next page, repeat the query with cursor: "<end_cursor>".

Errors:

ConditionMessage
worker_id unknown or inactiveWorker <id> is not valid for this account.
job_id unknownJob <id> is not valid for this account.
date_from malformeddate_from must be a valid ISO-8601 datetime
date_from outside allowed windowdate_from must be between 14 days before and 24 hours after the current UTC time
first out of rangefirst must be between 1 and 100
Bad cursorInvalid cursor

Labor Activity Start Mutation

Use labor_activity_start to start a new activity for a worker on a job. It can also record a historical, already-completed activity by passing both started_at and finished_at. Complexity cost: 15.

LaborActivityStartInput fields:

FieldTypeRequiredDescription
worker_idStringYesWorkforce Hero worker identifier.
job_idStringYesWorkforce Hero job identifier.
started_atISODateTimeNoStart timestamp. Omit to start now. Cannot be in the future.
finished_atISODateTimeNoFinish timestamp for a historical activity. Requires started_at, must be later than it, and cannot be in the future.
noteStringNoFree-text note (e.g. "Missed lunch punch").

The output field activity (LaborActivity!) is the created activity — or the matching completed activity when an identical historical request is repeated (the mutation is idempotent for historical activities).

Start an activity now:

mutation {
  labor_activity_start(data: { worker_id: "1234", job_id: "5678" }) {
    request_id
    complexity
    activity {
      id
      started_at
      finished_at
      status
      worker { id name }
      job { id name type }
    }
  }
}

Record a completed historical activity:

mutation {
  labor_activity_start(
    data: {
      worker_id: "1234"
      job_id: "5678"
      started_at: "2026-09-16T10:00:00Z"
      finished_at: "2026-09-16T10:30:00Z"
      note: "Missed lunch punch"
    }
  ) {
    request_id
    activity {
      id
      started_at
      finished_at
      note
      status
    }
  }
}

Errors:

ConditionMessage
Malformed timestampstarted_at must be a valid ISO-8601 datetime / finished_at must be a valid ISO-8601 datetime
finished_at without started_atfinished_at requires started_at
Future timestampstarted_at cannot be in the future / finished_at cannot be in the future
finished_at <= started_atfinished_at must be later than started_at
Invalid workerWorker <id> is not valid for this activity.
Invalid jobJob <id> is not valid for this activity.
Conflicts with existing activity dataActivity conflicts with existing Workforce Hero activity data.

Labor Activity Finish Mutation

Use labor_activity_finish to finish an ongoing activity. Complexity cost: 15.

LaborActivityFinishInput fields:

FieldTypeRequiredDescription
activity_idStringYesWorkforce Hero activity identifier.
finished_atDateTimeNoFinish timestamp. Omit or null to finish at the current time. Cannot be in the future.

The output field activity (LaborActivity!) is the completed activity.

mutation {
  labor_activity_finish(
    data: { activity_id: "activity-123", finished_at: "2026-09-16T10:30:00Z" }
  ) {
    request_id
    complexity
    activity {
      id
      started_at
      finished_at
      status
    }
  }
}

Errors:

ConditionMessage
Future finished_atfinished_at cannot be in the future
Unknown activityactivity_id is not valid for this account.
Already finishedThe activity has already been finished.
Conflicts with existing activity dataActivity conflicts with existing Workforce Hero activity data.

Labor Activity Modify Mutation

Use labor_activity_modify to correct a finished activity’s timeframe, and optionally its job. A note explaining the correction is mandatory. Complexity cost: 15.

LaborActivityModifyInput fields:

FieldTypeRequiredDescription
activity_idStringYesWorkforce Hero activity identifier.
noteStringYesReason for the correction. Must not be blank.
started_atISODateTimeYesNew start timestamp. Cannot be in the future.
finished_atISODateTimeYesNew finish timestamp. Must be later than started_at; cannot be in the future.
job_idStringNoReplacement job identifier. Must reference an active job.

The output field activity (LaborActivity!) is the modified activity.

mutation {
  labor_activity_modify(
    data: {
      activity_id: "activity-123"
      note: "Corrected activity timestamps"
      started_at: "2026-09-16T09:05:00Z"
      finished_at: "2026-09-16T10:25:00Z"
      job_id: "5678"
    }
  ) {
    request_id
    complexity
    activity {
      id
      started_at
      finished_at
      note
      job { id name type }
    }
  }
}

Errors:

ConditionMessage
Malformed / future / mis-ordered timestampsSame messages as labor_activity_start
Unknown activityactivity_id is not valid for this account.
Inactive jobjob_id must reference an active job.
Invalid jobjob_id is not valid for this activity.
Activity still ongoingThe activity cannot be modified while it is ongoing.
Overlaps another activityThe requested timeframe overlaps another activity.
Blank notenote must not be blank.

Labor Activity Remove Mutation

Use labor_activity_remove to remove a finished activity. Ongoing activities must be finished first. Complexity cost: 10.

LaborActivityRemoveInput has a single required field, activity_id (String). The output field ok (Boolean!) is true when Workforce Hero confirmed the removal.

mutation {
  labor_activity_remove(data: { activity_id: "activity-123" }) {
    request_id
    complexity
    ok
  }
}

Example response:

{
  "data": {
    "labor_activity_remove": {
      "request_id": "...",
      "complexity": 10,
      "ok": true
    }
  }
}

Errors:

ConditionMessage
Unknown activityActivity <id> is not valid for this operation.
Activity still ongoingThis activity cannot be removed because it is active. Please finish the activity before proceeding.