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/experimentalBecause the Experimental API can change frequently, fetch or inspect the latest GraphQL schema from this endpoint before building or updating an integration.
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
carriersandshipping_methodsqueries - Request label printing with the
label_printmutation - Get shipping rates without creating a label with the
label_quotemutation - Create an inbound shipment from an Advance Shipment Notice with the
inbound_shipment_create_from_asnmutation - Read and manage Workforce Hero labor activities with the
labor_worker_activitiesquery and thelabor_activity_start,labor_activity_finish,labor_activity_modify, andlabor_activity_removemutations
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:shipmentsIf 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:shipmentsThe mutation has a complexity cost of 15.
Input
LabelQuoteInput fields:
| Field | Type | Required | Description |
|---|---|---|---|
order_id | String | Yes | Public API order ID. |
shipping_carrier | String | No | Carrier to quote. Omit to use the order’s shipping carrier. Use "cheapest" to quote against your cheapest-carrier profiles. |
shipping_method | String | No | Carrier method to quote. Omit to use the order’s shipping method. |
include_partial_rates | Boolean | No | Defaults to false. When true, carrier errors are returned in carrier_errors together with any successful rates instead of failing the mutation. |
max_delivery_days | Int | No | Maximum transit days allowed for returned rates. |
packages | [PrintLabelPackageInput!]! | Yes | Packages 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
| Field | Type | Description |
|---|---|---|
rates | [LabelQuoteRate] | Rates returned for the requested carrier and packages. |
cheapest | LabelQuoteRate | Lowest-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:
- Creates a Purchase Order and its line items (one per SKU).
- Creates an Inbound Shipment linked to that Purchase Order, with status
PENDING_ARRIVAL. - Creates the expected inbound line items (quantity per SKU).
- 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_ordersIf 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_unitsis treated as a pallet. - A packing unit without nested
packing_unitsis 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: 50Each 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
| Field | Description |
|---|---|
external_shipment_id | BOL, shipment ID, or any shipper reference. Must be unique per account. |
warehouse_id | The receiving warehouse (encoded Public API id). |
vendor_name | The shipping vendor / trading partner name. |
edi_type | The source document type: "856" or "943". |
packing_units | Recursive 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:
| Code | Meaning |
|---|---|
WAREHOUSE_NOT_FOUND | warehouse_id does not belong to the account. |
DUPLICATE_SHIPMENT | An inbound shipment already exists for this external_shipment_id. |
NO_LINE_ITEMS | The shipment contains no line items. |
PRODUCT_NOT_FOUND | A SKU could not be resolved for the account. |
INVALID_SSCC_LENGTH | An SSCC exceeds the 22-character limit. |
AMBIGUOUS_LOT_NOT_SUPPORTED | An item references multiple lots without a per-lot quantity. |
MAX_NESTING_DEPTH_EXCEEDED | packing_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
| Operation | Scope |
|---|---|
labor_worker_activities | view:labor |
labor_activity_start, labor_activity_finish, labor_activity_modify, labor_activity_remove | change:labor |
Shared Type: LaborActivity
| Field | Type | Description |
|---|---|---|
id | String | Workforce Hero activity identifier. |
started_at | ISODateTime | UTC-normalized start timestamp. |
finished_at | ISODateTime | UTC-normalized finish timestamp (null while ongoing). |
last_activity_at | ISODateTime | UTC-normalized timestamp of the last activity update. |
note | String | Activity note. |
status | String | Status reported by Workforce Hero (e.g. "ACTIVE", "FINISHED"). |
tags | [String] | Activity tags. |
worker | LaborActivityWorker | id, name, user_id (ShipHero user ID). |
job | LaborActivityJob | id, name, type (e.g. "PICKING"). |
Typical Workflow
labor_activity_start— clock a worker onto a job (or backfill a completed shift).labor_activity_finish— close the ongoing activity.labor_activity_modify— fix timestamps/job on the finished activity if needed.labor_worker_activities— audit what was recorded for the worker.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.
| Argument | Type | Required | Description |
|---|---|---|---|
worker_id | String | Yes | Workforce Hero worker identifier. Must be an active worker. |
job_id | String | No | Filter by Workforce Hero job identifier. |
date_from | ISODateTime | No | Exclusive activity start boundary. Defaults to UTC now minus 24 hours. Must be between 14 days in the past and 24 hours in the future. |
unfinished | Boolean | No | true returns only ongoing activities, false only finished ones; omit for both. |
first | Int | No | Page size. Defaults to 20; must be between 1 and 100. |
cursor | String | No | end_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:
| Condition | Message |
|---|---|
worker_id unknown or inactive | Worker <id> is not valid for this account. |
job_id unknown | Job <id> is not valid for this account. |
date_from malformed | date_from must be a valid ISO-8601 datetime |
date_from outside allowed window | date_from must be between 14 days before and 24 hours after the current UTC time |
first out of range | first must be between 1 and 100 |
| Bad cursor | Invalid 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:
| Field | Type | Required | Description |
|---|---|---|---|
worker_id | String | Yes | Workforce Hero worker identifier. |
job_id | String | Yes | Workforce Hero job identifier. |
started_at | ISODateTime | No | Start timestamp. Omit to start now. Cannot be in the future. |
finished_at | ISODateTime | No | Finish timestamp for a historical activity. Requires started_at, must be later than it, and cannot be in the future. |
note | String | No | Free-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:
| Condition | Message |
|---|---|
| Malformed timestamp | started_at must be a valid ISO-8601 datetime / finished_at must be a valid ISO-8601 datetime |
finished_at without started_at | finished_at requires started_at |
| Future timestamp | started_at cannot be in the future / finished_at cannot be in the future |
finished_at <= started_at | finished_at must be later than started_at |
| Invalid worker | Worker <id> is not valid for this activity. |
| Invalid job | Job <id> is not valid for this activity. |
| Conflicts with existing activity data | Activity 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:
| Field | Type | Required | Description |
|---|---|---|---|
activity_id | String | Yes | Workforce Hero activity identifier. |
finished_at | DateTime | No | Finish 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:
| Condition | Message |
|---|---|
Future finished_at | finished_at cannot be in the future |
| Unknown activity | activity_id is not valid for this account. |
| Already finished | The activity has already been finished. |
| Conflicts with existing activity data | Activity 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:
| Field | Type | Required | Description |
|---|---|---|---|
activity_id | String | Yes | Workforce Hero activity identifier. |
note | String | Yes | Reason for the correction. Must not be blank. |
started_at | ISODateTime | Yes | New start timestamp. Cannot be in the future. |
finished_at | ISODateTime | Yes | New finish timestamp. Must be later than started_at; cannot be in the future. |
job_id | String | No | Replacement 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:
| Condition | Message |
|---|---|
| Malformed / future / mis-ordered timestamps | Same messages as labor_activity_start |
| Unknown activity | activity_id is not valid for this account. |
| Inactive job | job_id must reference an active job. |
| Invalid job | job_id is not valid for this activity. |
| Activity still ongoing | The activity cannot be modified while it is ongoing. |
| Overlaps another activity | The requested timeframe overlaps another activity. |
| Blank note | note 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:
| Condition | Message |
|---|---|
| Unknown activity | Activity <id> is not valid for this operation. |
| Activity still ongoing | This activity cannot be removed because it is active. Please finish the activity before proceeding. |