Error Glossary
This page is a lookup reference for the errors the ShipHero Public API returns. Use it to go from the message and code you received to the reason the request was rejected and the change that resolves it.
Most failures are not specific to one operation. Before looking up an individual message, check Common causes — authentication, scopes, user permissions, 3PL account access and identifier handling account for the majority of rejected requests. The per-operation tables only document what is specific to that operation.
What is covered on this page:
- Error response shape
- Error codes
- Common causes
- Scope warnings are not errors
- Authentication endpoint errors
- Messages by operation group
- Carrier errors on label operations
- Operations gated by internal permissions
- Reporting an error to support
Error response shape
GraphQL always answers with HTTP 200 when the query itself is valid. Failures are reported in the errors array, and the affected field is null in data. A single response can contain several errors.
{
"errors": [
{
"message": "Order not found",
"operation": "order_update",
"field": "order_update",
"request_id": "5da7dc13f3079f0def208711",
"code": 5
}
],
"data": {
"order_update": null
}
}| Field | Description |
|---|---|
message | Human-readable reason. This is the value you look up on this page. |
operation | The top-level query or mutation that failed. |
field | The field being resolved when the error was raised. Mutations report the operation name; queries usually report data, the wrapper field the resolver failed on. |
request_id | Identifier for this request. Always capture it — support uses it to retrieve your exact payload. |
code | Numeric error class. See Error codes. Not every error carries one. |
details | Optional array of { code, message } entries when one request failed several validations at once. |
Throttling errors use a different set of fields: they carry required_credits, remaining_credits and time_remaining instead of field. See Throttling and rate limiting.
Note
Errors raised by GraphQL itself — unknown field, missing required argument, wrong argument type, malformed query — are rejected before your operation runs. They have no code, operation or request_id, and they are not catalogued on this page. See the GraphQL Primer if you are unsure whether a request is well formed.
mutation { order_create(data: {order_numberr: "SO-1001"}) { request_id } }
# → { "message": "Field 'order_numberr' is not defined by type 'CreateOrderInput'.",
# "locations": [{ "line": 1, "column": 31 }] } // no code, no request_idError codes
Codes follow the Google RPC code convention, with a few values added by the Public API.
| Code | Name | Meaning | What to do |
|---|---|---|---|
| 3 | Invalid Argument | The input is malformed, out of range, or a logically required field is missing. | Correct the payload. Retrying the same request will fail again. |
| 5 | Not Found | The resource does not exist, or is not visible to the authenticated account. | Query the resource first and use the identifier it returns, under the correct account. See the schema reference. |
| 6 | Already Exists | The resource or identifier you are creating already exists. | Look up the existing record instead of creating it, or supply a unique identifier. See the schema reference. |
| 7 | Permission Denied | A scope, a user permission, or access to the target 3PL account is missing. | See Common causes. |
| 9 | Failed Precondition | The resource exists, but its current state does not allow the action. | Move the resource into the required state, then retry. |
| 10 | Aborted | The operation was aborted, typically because of a conflict. | Read the resource to confirm its state, then retry. |
| 14 | Unavailable | A dependency needed to serve the request was temporarily unavailable. | Retry with backoff. |
| 20 | Format Error | A value could not be parsed into the expected format. | Check the types and formats of the values you sent. |
| 21 | Service Not Available | A downstream service timed out. | Retry with backoff. |
| 22 | Unexpected Error | An unhandled error occurred. | Do not retry a mutation blindly. Read the resource first, then contact support with the request_id. |
| 30 | Throttling Error | The operation costs more credits than your quota currently allows. | See Throttling and rate limiting. |
Codes 3, 5, 6 and 9 mean the request was understood and rejected: retrying it unchanged produces the same result. For errors with codes 10, 14, 21 and 22, the API automatically refunds credits reserved for the failed request. A retry is a new request and is subject to normal credit accounting.
Warning
A server-side error on a mutation does not guarantee the mutation did not take effect. Before retrying a mutation that returned code 10, 14, 21 or 22, query the resource to check whether the change was already applied.
# after a code 22 on order_update, read before you retry
query { order(id: "T3JkZXI6MTIzNDU2") { data { order_number fulfillment_status } } }Throttling and rate limiting
Two independent limits can reject a request, and they fail in different ways.
Credit quota returns GraphQL error code 30 with required_credits, remaining_credits and time_remaining. Whether waiting helps depends on which limit you hit:
- The operation fits your account maximum but not your current balance — wait
time_remainingand retry. - The operation costs more than your account may spend on a single request — waiting never helps, because the balance can never grow that large. Reduce the request: select fewer fields, lower
first, and page through the results.
The decision logic and worked examples are in Example 3: Handle an insufficient-credit error; costs and optimization are covered under Throttling & Quotas.
Request-rate limit is separate and is not a GraphQL error at all. Exceeding 7,000 requests in any five-minute window returns HTTP 429 Too Many Requests with no errors array and no request_id. Failed and introspection requests count toward it. Back off until the window clears — see Request-rate limit.
Note
If you are handling errors by reading errors[0].code, a 429 has no body to read. Check the HTTP status before parsing a GraphQL error response.
POST /graphql
# → HTTP 429 Too Many Requests, empty body — nothing to parse as a GraphQL errorCommon causes
These causes are not tied to a single operation. They explain most errors, so they are documented once here rather than repeated in every table below.
Authentication
A missing or malformed Authorization header is rejected before GraphQL runs, with HTTP 401 and no error code.
| Message | Cause | Solution |
|---|---|---|
Authorization header is expected | No Authorization header was sent. | Send Authorization: Bearer <access_token>. |
Authorization header must start with 'Bearer' | The scheme is missing or not Bearer. | Use the Bearer scheme exactly. |
Token not found | The header contains Bearer with no token after it. | Include the access token. |
Authorization header must be Bearer token | The header has extra parts beyond scheme and token. | Send exactly two space-separated values. |
A token can also be well formed and still be rejected because of the state of the user or the account. These are HTTP-level responses with a plain message and no error code.
| Status | Message | Cause | Solution |
|---|---|---|---|
403 | Public API access is disabled for this user | API access is turned off for this user. | You cannot turn this on yourself. Ask an account admin to enable it, following How to Give a ShipHero User API Access; the steps are also in Public API access is disabled for this user. |
401 | Account is canceled | The ShipHero account is canceled. | Nothing in the API will change this. Ask the account owner to reactivate the account; if you do not know who that is, ask in the ShipHero Community. |
401 | Trial account is expired | The trial period ended. | Nothing in the API will change this. Ask the account owner to move the account onto a paid plan, or ask in the ShipHero Community who to contact. |
401 | Access to the Public API is not available on the ShipHero Essential plan | The plan does not include Public API access. | Nothing in the API will change this. Ask the account owner which plan includes API access, or ask in the ShipHero Community. |
Note
These four are rejections of the user or the account, not of your request, so no change to the query will resolve them. They all need someone with access to the ShipHero account to act. If you are an external developer and do not know who that is, ask in the ShipHero Community rather than retrying.
Scopes
Tokens issued through the OAuth flow carry scopes, and an operation fails with code 7 when the token is missing one.
| Message | Cause | Solution |
|---|---|---|
Missing required scope(s): <scope> | The token does not include every scope the operation requires. | Re-authorize requesting the listed scopes. Queries need view:, mutations need change:. |
User does not have permissions to access [<resources>] | The user behind the token is not granted Public API access to the listed resource. This is checked against the user’s own API permissions, not against the token’s scopes, so requesting a new token will not fix it. | Ask an account admin to give the user API access to that area — see How to Give a ShipHero User API Access — or ask in the ShipHero Community. |
Some operations need more than one scope. Creating an inbound shipment from an ASN, for example, requires both change:inbound_shipments and change:purchase_orders. See Scopes for the full list.
Note
Tokens generated through https://public-api.shiphero.com/auth/token do not enforce scopes. If you get a scope error, you are using an OAuth token.
User permissions
A handful of operations also check whether the ShipHero user behind the token holds a product permission, on top of the token’s scopes: user_update (users.manage.update), warehouse_profile_create (warehouses.manage.create), bill_add_ad_hoc_charge (billing.create), bill_update_ad_hoc_charge (billing.update), and the Experimental billing_charges and billing_rates (billing.manage.read).
Note
Developer users skip this check. If you authenticate with a developer user, you will not see this error on the operations above. It only reaches regular ShipHero users calling the API. The one place a developer user is hard-blocked is Operations gated by internal permissions, where a separate service does its own check.
Scopes are checked before permissions, so a token missing the scope reports Missing required scope(s): … first and never reaches the permission check.
| Message | Cause | Solution |
|---|---|---|
User has no permission for resource | Returned by the operations above when a regular user lacks the product permission. | If you administer the account, grant the permission under Users & Roles (how to do it). If you do not, ask an account admin, naming the operation you are calling. If nobody can identify the permission, ask in the ShipHero Community. |
3PL account access
3PL users act on a customer account by passing customer_account_id. Operations reject an account the token cannot reach with code 7.
| Message | Cause | Solution |
|---|---|---|
User cannot act on behalf of this account | The customer_account_id is not a child account of the authenticated user. | Pass an account the token is authorized for. List them with account. |
Cannot create an order for this account | The account in the order payload is not reachable by this token. | Use an authorized customer_account_id. |
Cannot access <resource> | The record exists but belongs to an account the token cannot reach. | Confirm you are querying under the right account. |
Cannot access the requested <resource>. Permission denied | A mutation was sent with a customer_account_id the token is not authorized for. <resource> is the record type, or the literal word resource when none is named. This one arrives as code 9. | Pass an account the token can reach. List them with account. |
Note
An unauthorized customer_account_id produces two different codes depending on which operation you call, not on how wrong the account is. Operations that check it explicitly — cycle counts, inbound shipments, totes and the Experimental ASN operations — answer User cannot act on behalf of this account with code 7. Everywhere else the rejection comes from deeper in ShipHero and arrives as Cannot access the requested <resource>. Permission denied with code 9. Treat both as the same mistake.
mutation { cycle_count_items_create(data: {customer_account_id: "QWNjb3VudDox", ...}) { request_id } }
# → { "message": "User cannot act on behalf of this account", "code": 7 }
mutation { product_update(data: {sku: "ABC", customer_account_id: "QWNjb3VudDox"}) { request_id } }
# → { "message": "Cannot access the requested resource. Permission denied", "code": 9 }Note
Cannot access <resource> and <resource> not found (code 5) are different answers. “Not found” means no such record exists; “cannot access” means it exists under an account you may not read. Both are returned by inventory, returns, wholesale and LPN operations.
The code on an access error is not always 7. Operations that raise it directly return code 7, but the same kind of rejection raised deeper in ShipHero arrives as code 9, as Cannot access the requested order. Permission denied does. Do not treat code 7 as the only sign of an access problem.
query { product(sku: "NOT-A-SKU") { data { sku } } }
# → { "message": "Product not found", "code": 5 } // no such record
# → { "message": "Cannot access Location", "code": 7 } // exists, other accountWarning
An account problem does not always produce an error at all. List queries such as products and orders answer a customer_account_id the token cannot reach with an empty page and no error, exactly as they would for an account that genuinely has no records. If a list comes back empty when you expect data, confirm the customer_account_id and the account the token belongs to before concluding there is nothing there. Single-resource lookups such as product and order do return <resource> not found (code 5) in the same situation.
query { products(customer_account_id: "QWNjb3VudDox") { data(first: 1) { edges { node { sku } } } } }
# → { "data": { "products": { "data": { "edges": [] } } } } // no errors array at allAccount types
Several errors on this page depend on what kind of account the token belongs to, and no query tells you which kind it is.
| Account type | What it means for the API |
|---|---|
| Brand | Owns its own products, inventory and warehouses. customer_account_id does not apply. |
| 3PL parent | Owns the warehouses and acts for client accounts by passing customer_account_id. |
| 3PL client | Its products and orders live under its own account, but the warehouses belong to the 3PL. A client account normally has no warehouses of its own, so operations that need a warehouse fail unless the account is set up as a hybrid. |
| Hybrid | A client account that also operates its own warehouses. |
This matters most for anything that takes a warehouse_id: on a 3PL client the warehouse belongs to the 3PL, not to the account you are authenticated as. If you are unsure which type you are integrating against, ask the people who contracted the warehouse, or email support@shiphero.com. Guessing from an empty warehouse list is not reliable, because an empty list can also mean an access problem. For help identifying your setup, ask in the ShipHero Community.
Identifiers
Most Invalid id and “not found” errors come from constructing identifiers by hand.
| Message | Cause | Solution |
|---|---|---|
Invalid id <id> | The value is not a well-formed ShipHero identifier. | Use an identifier returned by a query, unmodified. The schema reference lists the lookup query for each resource. |
Invalid ids | An id field was sent with nothing usable in it: an empty list, or null. A badly formed identifier returns Invalid id <id> instead. | Send at least one identifier returned by a query. The schema reference lists the lookup query for each resource. |
Missing or invalid field: '<field>' | A field that is logically required for this input combination is absent or unusable. | Supply the field named in the message. |
One of the fields <field> of type 'String!' is required. | A group of mutually dependent fields was left incomplete. | Provide the field named in the message. |
Public API identifiers are Base64 strings that encode an entity type and a number, for example T3JkZXI6MTIzNDU2. Treat them as opaque: do not decode, edit or rebuild them, and do not compare them numerically. To turn a numeric legacy_id into one, use the uuid query rather than encoding it yourself. See Core concepts.
Note
Queries do not report a bad identifier as a format problem. An identifier that is not valid Base64, encodes the wrong entity type, or belongs to another account all return <resource> not found (code 5). So “not found” does not always mean the record is missing — check the identifier before concluding the data is gone. The Invalid id errors below come from mutations, which convert identifiers before running.
query { order(id: "12345") { data { order_number } } } # not Base64
query { order(id: "UHJvZHVjdDoxMjM=") { data { order_number } } } # Base64 of Product:123
# → both: { "message": "Order not found", "code": 5 }Scope warnings are not errors
When a token lacks the scope required for an individual field, the request still succeeds. The field resolves to null and a warning is reported under extensions.scope_warnings, keyed by <ParentType>.<field_name>.
{
"data": {
"order": {
"id": "T3JkZXI6MTIzNDU2",
"email": null
}
},
"extensions": {
"scope_warnings": {
"Order.email": {
"code": "FIELD_SCOPE_MISSING",
"message": "Field 'email' was not resolved because scope(s) 'view:pii' are missing",
"field_id": "Order.email",
"parent_type": "Order",
"field": "email",
"required_scopes": ["view:pii"],
"occurrences": 1
}
}
}
}occurrences counts how many times the field was blocked across the response, so a blocked field inside a list is reported once rather than per item.
Read required_scopes and re-authorize with those scopes if you need the data. If a field you expected comes back null and no error was returned, inspect extensions.scope_warnings before treating it as missing data.
Authentication endpoint errors
The token endpoints are plain HTTP endpoints, not GraphQL. They take a POST with a JSON body and answer with an HTTP status code and a bare JSON string as the body — there is no errors array, no code and no request_id to read.
| Endpoint | Message | Solution |
|---|---|---|
/auth/token | username is required | Include username in the body. |
/auth/token | password is required | Include password in the body. |
/auth/token | Service Unavailable | The identity provider did not answer. Retry with backoff. |
/auth/refresh | refresh_token is required | Include refresh_token in the body. |
/auth/refresh | Service Unavailable | The identity provider did not answer. Retry with backoff. |
See Authentication for the full request format.
Messages by operation group
Messages specific to each group of operations. Causes that apply everywhere are in Common causes and are not repeated here.
Orders
| Code | Message | Returned by | Solution |
|---|---|---|---|
| 3 | Invalid input. Details: country is required | order_create and order_update. The shipping or billing address has no country. | Send a two-letter ISO country code. See Country or State codes. |
| 3 | Invalid order. Details: <errors> | order_create. One or more order fields failed validation. | Fix each field listed in the message. |
| 3 | Invalid input. Details: Missing shipping_lines.carrier, Missing shipping_lines.method | order_update. An order was sent with shipping_lines but without both a carrier and a method. | Send both together, for example carrier: "UPS" and method: "Ground". |
| 3 | At least one line item is required | order_create. line_items was empty. | Include at least one line item. |
| 3 | Invalid line items. Details: <errors> | order_add_line_items and order_update_line_items. One or more line items failed validation. | Fix each item listed in the message. |
| 3 | Invalid input. Details: cancel_on_platform and void_on_platform are only valid when fulfillment_status is exactly 'canceled' | order_update_fulfillment_status. Either flag was sent with another fulfillment_status. | Set fulfillment_status to canceled, or remove both flags. |
| 3 | Need to specify at least one line item id | order_remove_line_items. A line item operation received no ids. | Send the ids of the line items to act on. |
| 3 | Need to specify at least one line item | order_update_line_items. A line item operation received no items. | Send at least one line item. |
| 3 | Need to specify old and new sku | order_set_line_item_sku. A SKU replacement was sent with only one SKU. | Send both the current and the replacement SKU. |
| 3 | Invalid History Entry. Details: <errors> | order_add_history_entry. An order history entry failed validation. | Fix the fields listed in the message. |
| 5 | Order not found | order_set_line_item_sku and order_add_attachment. No order matches the identifier under this account. For order_set_line_item_sku, this also covers an identifier that is malformed or belongs to another entity type. | Look the order up with orders and use the id it returns. |
| 9 | Cannot access the requested order. Permission denied | Any order mutation. It is raised in ShipHero’s own order layer rather than by a single operation. The order exists but belongs to an account this token cannot reach. Note the code is 9, not the 7 you would expect from the wording. | Use a token for the account that owns the order, or pass an authorized customer_account_id. See 3PL account access. |
| 9 | Order with order_number '<order_number>' already exists | order_create. An order with that order_number is already in the account. | Use a unique order_number, or update the existing order. |
| 9 | SKUs not found: <skus> | order_create. The listed SKUs do not exist in the account. | Create the products first, or correct the SKUs. |
| 9 | Duplicate partner_line_item_id not allowed: <ids> | order_create. The listed partner_line_item_id values repeat within the order or already exist. | Make each partner_line_item_id unique. |
Note
An order_create that is rejected for a duplicate order number, an unknown SKU or a repeated partner_line_item_id comes back as code 9, not the code 6 you would expect from “already exists”. If your integration decides what to do from the code alone, read the message too.
mutation { order_create(data: {order_number: "SO-1001", ...}) { request_id } }
# → { "message": "Order with order_number 'SO-1001' already exists", "code": 9 }Order fulfillment, merging and tracking flows are described in Order Fulfillment and Merge Orders.
Products
| Code | Message | Returned by | Solution |
|---|---|---|---|
| 3 | Sku cannot be empty | product_create. sku was absent or blank. | Send a non-empty SKU. Confirm it with products. |
| 3 | Missing or invalid image url | product_create, product_delete_image and product_update. An image entry is null, or its src is empty or only whitespace. The field is called src even though the message says “url”. | Send a non-empty src on every image. Nothing else about the value is validated, so an unreachable address is accepted here and simply fails to display. |
| 3 | Missing or invalid image case_barcode | product_create and product_update. A case pack entry is null, or its case_barcode is empty or only whitespace. This message mentions “image” but refers to a case pack. | Send a non-empty case_barcode. No format or length rule is applied. |
| 3 | Missing or invalid image case_quantity | product_create and product_update. A case pack entry’s case_quantity is 0 or absent. This message mentions “image” but refers to a case pack. | Send a quantity other than 0. |
| 3 | Tag values must be strings | product_update. A tag was sent as a number, boolean or object. | Send every tag as a string. See Removing commas from tags. |
| 5 | Product not found | The product query cannot find the SKU under the requested account. | See Product not found under Inventory. |
| 5 | Sku '<sku>' not found | product_update could not find that SKU under the account it searched. Without customer_account_id it searches the token’s own account, so a 3PL gets this for a SKU that exists under a client. An empty sku reports as Sku '' not found, not as a missing-field error. | If the product belongs to a 3PL client, resend with that client’s customer_account_id. Otherwise confirm the SKU with products. |
| 5 | Sku '<sku>' not found in warehouse | warehouse_product_update found no record for that SKU in the given warehouse. | Confirm the SKU is stocked in that warehouse with warehouse_products. |
| 5 | Product <sku> not found for account <account_id> | warehouse_product_update. The SKU does not exist under the account named in the message. This is the only product lookup error that tells you which account was searched; the others leave it implicit. | Read the account id in the message. If it is your 3PL parent account and the product belongs to a client, resend with that client’s customer_account_id. Otherwise confirm the SKU with products. |
Note
A null list is different from a null entry: product_update accepts images: null and cases: null, but images: [null] and cases: [null] return the validation errors above. Sending images: null leaves existing images unchanged; it does not delete them.
Inventory, locations and LPNs
| Code | Message | Returned by | Solution |
|---|---|---|---|
| 3 | Invalid sku | inventory_replace. sku is empty or only whitespace. The value itself is not validated against existing products here. | Send a non-empty SKU. |
| 9 | One of the following fields is required: location_id or lpn_id | inventory_add, inventory_remove, inventory_replace and inventory_subtract. Neither a location nor an LPN was given. | Send exactly one of the two. |
| 9 | One of the fields <field> of type 'String!' is required. | inventory_transfer. A move is missing its source or destination. The message names the field it needs. | Send the field named in the message. |
| 3 | Location name cannot be empty | location_create. name was blank on a location create or update. | Send a non-empty name. |
| 3 | Location zone cannot be empty | location_create. zone was blank. | Send a non-empty zone. |
| 3 | Invalid container_type. Must be one of: BOX, PALLET, STAGING | license_plate_numbers_create. An unsupported container_type was sent for an LPN. | Use one of the listed values. |
| 3 | Quantity must be between 1 and 1300 | license_plate_numbers_create. The quantity is outside the accepted range. | Split the operation into several requests. |
| 3 | Invalid ids | item_location_delete. The ids list was sent empty, or sent as null. A badly formed id returns Invalid id <id> instead. | Send at least one item location id from item_locations. Each must already have quantity 0. |
| 5 | Location not found | inventory_transfer. No location matches the identifier in this warehouse. | Look it up with locations and use a returned id. |
| 5 | LPN not found | inventory_add, inventory_remove, inventory_replace, inventory_subtract and inventory_transfer. No LPN matches the identifier. | Look it up with license_plate_numbers and use a returned id. |
| 5 | Product not found | inventory_add, inventory_remove, inventory_subtract and inventory_transfer. The SKU does not exist under this account. For a 3PL, check whether the SKU belongs to a client account and needs customer_account_id. | Confirm the SKU with products, then create the product or correct the SKU. |
| 5 | Product within the location for the given warehouse not found | inventory_transfer. There is no inventory record for that SKU in that location and warehouse. As above, omitting customer_account_id makes the lookup use the token’s own account, so a 3PL can get this for stock that is really there under a client. | Confirm the stock with warehouse_products, and pass the client’s customer_account_id if the inventory belongs to a 3PL client. |
| 9 | Unable to delete location | location_delete. The location still has item locations attached. | Move or remove the inventory, then delete the location. |
| 9 | ... staging location for wholesale order ... | location_delete. The location is reserved as a staging location for a wholesale order. | Resolve the wholesale order first. See Staging Locations. |
| 7 | Cannot access Location / Cannot access LPN | the inventory_* mutations and return_receive_line_item. The record belongs to an account the token cannot reach. | See 3PL account access. |
| 7 | Cannot access the product within the location for the given warehouse | inventory_transfer. The inventory record belongs to an unreachable account. | See 3PL account access. |
location_bulk_create does not fail on duplicate names. It reports them in skipped_count and skipped_names, so check both fields rather than assuming every location was created. Full walkthrough in Inventory.
Returns
| Code | Message | Returned by | Solution |
|---|---|---|---|
| 3 | quantity_received must be > 0 | return_receive_line_item. A received quantity of zero or less was sent. | Send a positive quantity. |
| 3 | restock_quantity must be >= 0 | return_receive_line_item. A negative restock quantity was sent. | Send zero or more. |
| 3 | restock_quantity cannot exceed quantity_received | return_receive_line_item. More units were restocked than were received. | Restock at most the received quantity. |
| 3 | bin_id is required when restocking inventory | return_receive_line_item. A restock was requested without a destination bin. | Send the bin_id the units go into. |
| 5 | Invalid warehouse_id / Invalid lot_id / Invalid bin_id | return_receive_line_item. No warehouse, lot or bin matches the identifier under this account. Invalid warehouse_id is also returned by product and inventory mutations. | Use an identifier from account or the matching lookup query. |
| 5 | Return not found | return_create_exchange and return_receive_line_item. No return matches the identifier under this account. | Look it up with returns and use a returned id. |
| 5 | return_item not found in return | return_add_attachment and return_receive_line_item. The item id does not belong to that return. | Use an item id taken from the return itself. |
| 5 | Product not found | return_receive_line_item. The SKU on the return item does not exist under the requested account. | See Product not found under Inventory. |
| 3 | Bin does not belong to the given warehouse | return_receive_line_item. The bin and the warehouse do not match. | Use a bin from the same warehouse. |
| 9 | Return label generation requires a user with an assigned warehouse. | return_create. The authenticated user has no default warehouse. | Assign a warehouse to the user, or authenticate as a user who has one. |
| 3 | Invalid value <value> in field label_cost | return_create. label_cost is not a valid decimal. | Send a decimal number, not a formatted string. |
| 7 | Cannot access Warehouse / Cannot access Product / Cannot access Location | inventory_transfer and return_receive_line_item. The record belongs to an unreachable account. | See 3PL account access. |
Return flows are described in Returns.
Shipments and shipping containers
| Code | Message | Returned by | Solution |
|---|---|---|---|
| 3 | Must provide at least one shipping container ID | shipping_container_delete and shipping_container_print_barcode_labels. The id list was empty. | Send at least one container id. |
| 3 | Specify exactly one of shipping_container_id or shipping_container_barcode | shipping_container_add_shipping_label and shipping_container_remove_shipping_label. Both or neither identifier was sent. | Send exactly one. |
| 3 | Container quantity must be at least 1 | shipping_container_create. A quantity below 1 was sent. | Send 1 or more. |
| 3 | Cannot create more than 100 shipping containers | shipping_container_create. More than 100 containers in one request. | Split into batches of 100. |
| 3 | Cannot delete more than 100 shipping containers | shipping_container_delete. More than 100 ids in one delete. | Split into batches of 100. |
| 3 | Cannot print more than 100 shipping container barcode labels | shipping_container_print_barcode_labels. More than 100 labels in one request. | Split into batches of 100. |
| 3 | Invalid value <value> in field cost | shipment_create and shipment_create_shipping_label. cost is not a valid decimal. | Send a decimal number. |
Purchase orders and shipping plans
| Code | Message | Returned by | Solution |
|---|---|---|---|
| 3 | Invalid line_items: null items are not allowed | purchase_order_create. The line_items list contains a null entry, such as line_items: [null]. | Remove null entries and send a valid line item object for each item. |
| 3 | Invalid value <value> in field <field> | the purchase_order_*, shipment_*, return_create and shipping_plan_create mutations. A decimal field received a value that cannot be parsed. | Send a plain decimal, without currency symbols or thousands separators. |
| 7 | Only ShipHero Fulfillment accounts can create shipping plans. | shipping_plan_create is restricted to a ShipHero-operated fulfillment account. Standard accounts cannot create shipping plans at all, so no change to the request will work. | Not something the API can grant. Ask in the ShipHero Community or email support@shiphero.com to find out whether your account can use this operation. |
| 7 | Shipping plans can only be created for ShipHero Fulfillment warehouses. | shipping_plan_create was pointed at a warehouse that is not one the operation accepts. | Not something the API can grant. Ask in the ShipHero Community or email support@shiphero.com. |
See Purchase Orders.
Cycle counts
| Code | Message | Returned by | Solution |
|---|---|---|---|
| 3 | Invalid sort_by '<value>'. Supported values: <values> | cycle_count_items_create, cycle_count_locations_create and cycle_count_recount_create. An unsupported sort field was requested. | Use one of the listed values. |
| 3 | 'skus' and 'item_filters' are mutually exclusive — provide one or the other, not both. | cycle_count_items_create. Both filters were sent in one request. | Send only one filter. |
See Cycle Counts.
Totes
| Code | Message | Returned by | Solution |
|---|---|---|---|
| 5 | Tote not found | clear_tote, tote_clear, tote_delete, tote_transfer_to_location and tote_update. No tote matches the identifier. | Look it up with totes and use a returned id. |
| 5 | Location not found | tote_transfer_to_location. No location matches the identifier. | Look it up with locations and use a returned id. |
| 9 | Tote and location must belong to the same warehouse | tote_transfer_to_location. The tote and the destination are in different warehouses. | Use a location in the tote’s warehouse. |
| 9 | Tote has no current picks to transfer | tote_transfer_to_location. The transfer mutation was called on a tote with nothing picked into it. | Check the tote first with tote: picks lists what is in it, picked_quantity counts the units, and status is Empty when there is nothing to transfer. |
Lots
| Code | Message | Returned by | Solution |
|---|---|---|---|
| 5 | Lot not found | lot_assign_to_location. No lot matches the identifier. | Look it up with expiration_lots and use a returned id. |
| 5 | Location not found | lot_assign_to_location. No location matches the identifier. | Look it up with locations and use a returned id. |
See Lots.
Wholesale orders
| Code | Message | Returned by | Solution |
|---|---|---|---|
| 3 | Invalid line_items: null items are not allowed | wholesale_order_create. The line_items list contains a null entry, such as line_items: [null]. | Remove null entries and send a valid line item object for each item. |
| 7 | Cannot create an order for this account | wholesale_order_create. The token cannot create an order for the supplied customer_account_id. | Use an authorized customer_account_id. See 3PL account access. |
| 9 | Order contains regular kits, convert regular kits to build kits before proceeding | wholesale_order_create. A line item refers to a regular kit rather than a build kit. | Convert the regular kit to a build kit before creating the wholesale order. |
| 3 | Missing or invalid order_id | the wholesale_* operations. order_id is absent or malformed. | Send an order_id from wholesale_orders. |
| 5 | Wholesale line items not found: <ids> | wholesale_order_update. The listed line item ids do not belong to the order. | Use ids taken from the order itself. |
| 5 | Wholesale order not found | the wholesale_* operations. No wholesale order matches the identifier. | Look it up with wholesale_orders and use a returned id. |
| 5 | The requested resource picking_job does not exist | wholesale_set_as_ready_to_pick. No picking job was found for the order’s pending shipment. This refers to an internal picking resource, not the order identifier. | Check the wholesale order’s allocation and picking state in the app. If the order appears ready but the error persists, contact support@shiphero.com with the order id and request id. |
| 9 | You cannot select a staging location that is already in use | wholesale_order_update_staging_location. The selected staging location is already assigned to a wholesale order. | Select an unused staging location, or resolve its existing assignment in the app before retrying. See Staging Locations. |
| 3 | Wholesale order has multiple or no pending shipments | wholesale_order_auto_allocate_for_picking, wholesale_order_fulfill, wholesale_order_generate_labels and wholesale_order_transfer_picks_to_staging. The operation acts on a single shipment, and the order has none pending or more than one. | Check the order’s shipments with wholesale_order. Changing which shipments are pending is done in the app, not the API — see Making Changes to Wholesale Orders. |
| 9 | Cannot edit the outbound schedule of a fulfilled order | wholesale_order_edit_outbound_schedule, once the order is fulfilled. | The schedule is fixed at fulfillment and cannot be reopened through the API. See Outbound Schedule and Order Completion for Wholesale. |
| 9 | Cannot print packing slip for this order | wholesale_order_print_packing_slip. The wholesale order has not reached a state that produces a packing slip. | Packing slips exist only once the order is packed. See How to Print Packing Slips and Generate Bills of Lading for Wholesale. |
| 9 | The order is not ready to print, warehouse completed or fulfilled | wholesale_order_print_labels. Labels were requested before the wholesale order reached a printable state. | Finish picking and packing first, then print. |
| 7 | Cannot access wholesale order | the wholesale_* operations. The order belongs to an unreachable account. | See 3PL account access. |
See Wholesale.
Work orders
| Code | Message | Returned by | Solution |
|---|---|---|---|
| 3 | Missing input parameters, assembly_sku is required for Work Orders of type Assembly | work_order_create. An Assembly work order was created without assembly_sku. | Send the SKU of the assembled product. See Assembly Products. |
| 9 | File too large for <filename>: <detail> | work_order_create. An attachment exceeds the allowed size. | Reduce the file size and retry. |
| 9 | Invalid file URL for <filename> | work_order_create. ShipHero refused to fetch the attachment. Same safety check as the invoice logo: the URL was rejected before any download: it must be https, must have a hostname that resolves, and must not resolve to a private or internal address. | Host the file at a public https URL. |
| 9 | Too many redirects for <filename> | work_order_create. The attachment URL redirects too many times. | Link the final URL directly. |
| 9 | Error while processing file <filename> | work_order_create. The attachment was fetched but could not be processed. | Confirm the file is valid and not corrupt. |
Users
| Code | Message | Returned by | Solution |
|---|---|---|---|
| 3 | Invalid default_warehouse_id | user_update. The id is well formed but no warehouse matches it, or the warehouse is not accessible to the account the user belongs to. When customer_account_id is sent, the warehouse is checked against that account, not against the token’s. A badly formed id returns Invalid id <id> instead. | Use a warehouse that belongs to the same account as the user you are updating. For a 3PL updating a client’s user, that means one of the client’s warehouses, not the parent’s. |
| 3 | Invalid default_order_status | user_update. The value is not one of the custom order statuses configured on that account. These are not a fixed schema enum, so the accepted values differ from account to account. | Use one of the order statuses configured on the account you are updating, as they appear in the ShipHero app. |
| 3 | Cannot set default_warehouse_id, default_order_status or default_pick_order_tag on this user | user_update. The user being updated has no warehouse app login. All three fields only exist for users who sign in to the warehouse app. | Set them only on users that have a warehouse app login. |
Warehouse profiles
| Code | Message | Returned by | Solution |
|---|---|---|---|
| 3 | <address>.<field> cannot be empty | warehouse_profile_create. A required address field is blank. | Fill in the field named in the message. |
| 3 | profile cannot be empty | warehouse_profile_create. The profile name is blank. | Send a non-empty name. |
| 3 | profile cannot exceed 40 characters | warehouse_profile_create. The profile name is too long. | Shorten it to 40 characters or fewer. |
| 3 | profile must be a non-default warehouse profile | warehouse_profile_create. The operation targeted the default profile. | Target a non-default profile. |
| 3 | manual_order_prefix cannot exceed 10 characters | warehouse_profile_create. The prefix is too long. | Shorten it to 10 characters or fewer. |
| 3 | invoice_email is invalid | warehouse_profile_create. The address fails a syntax check. Deliverability is deliberately not checked, so a well-formed address that does not exist is accepted. | Send a syntactically valid address. |
| 9 | Invalid invoice_logo_url | warehouse_profile_create. ShipHero refused to fetch the URL. This is a safety check, not a format one: the URL was rejected before any download: it must be https, must have a hostname that resolves, and must not resolve to a private or internal address. | Host the logo at a public https URL. A URL that is reachable but fails to download reports Unable to download invoice logo instead. |
| 9 | Invoice logo cannot exceed 5 MB | warehouse_profile_create. The image is too large. | Supply an image of 5 MB or less. |
| 9 | Invoice logo must be a PNG or JPEG image | warehouse_profile_create. The image is another format. | Convert it to PNG or JPEG. |
| 9 | Invoice logo URL has too many redirects | warehouse_profile_create. The URL redirects too many times. | Link the final image URL. |
| 9 | Unable to download invoice logo | warehouse_profile_create. The image could not be fetched. | Make sure the URL is publicly reachable. |
| 5 | Physical warehouse not found | warehouse_profile_create. The warehouse does not exist under this account. | Use a warehouse id from account. |
Experimental API
Operations on the Experimental API return the same codes. These messages are specific to it. For identifier and cross-account access errors, see Identifiers and 3PL account access.
| Code | Message | Returned by | Solution |
|---|---|---|---|
| 9 | Feature not enabled | inbound_shipment_create_from_asn. The account is not enabled for this Experimental operation. Access is granted per account, not per token, so no scope or credential change will help. | Email support@shiphero.com to have it enabled for the account, or ask in the ShipHero Community. |
| — | Experimental GraphQL schema access is not enabled | /graphql/experimental itself, as HTTP 403, before the query runs, when the account is not enabled for the Experimental endpoint at all. It has no GraphQL errors array and no code. Enablement is per account. | Email support@shiphero.com to have the account enabled. Until then every Experimental operation returns this, regardless of scopes. |
| 7 | User cannot act on behalf of this account | the cycle_count_*, inbound_shipment_* and tote_* mutations, the only ones that check customer_account_id explicitly. The account is not one the token may act for. | See 3PL account access. |
| 7 | TPL billing is not enabled | billing_charges and billing_rates. Billing queries were called on an account without 3PL billing. | 3PL billing is an account setting, not an API one. Ask an account admin to enable it — see 3PL Billing for what it covers — or ask in the ShipHero Community. |
| 3 | date_from must be a valid ISO-8601 datetime | labor_worker_activities. The date could not be parsed. | Send an ISO-8601 datetime. See Dates. |
| 3 | date_from must be between 14 days before and <n> hours after the current UTC time | labor_worker_activities only serves the last 14 days. Splitting the range into smaller windows does not reach further back. | For anything older, use the LABOR module of Data Exports, which covers activities, workers, jobs and warehouses. |
| 3 | started_at/finished_at must be a valid ISO-8601 datetime | labor_activity_start and labor_activity_modify. A labor activity timestamp could not be parsed. | Send an ISO-8601 datetime. |
| 3 | finished_at requires started_at | labor_activity_start and labor_activity_modify. An end time was sent with no start time. | Send both, or neither. |
| 3 | finished_at must be later than started_at | labor_activity_start and labor_activity_modify. The interval is inverted. | Order the timestamps correctly. |
| 3 | <timestamp> cannot be in the future | labor_activity_start and labor_activity_modify. A labor activity timestamp is in the future. | Send a past or current timestamp. |
| 3 | first must be between 1 and <max> | labor_worker_activities. first is below 1 or above the maximum, which is 100 for these operations. | Request 100 or fewer and page with the cursor. |
| 3 | Invalid cursor | labor_worker_activities. The cursor is not a value this operation issued. Cursors do not expire, so this is a format problem, not a stale-page one. | Pass a cursor exactly as the previous page returned it, or omit it to start from the first page. |
| 3 | Activity <id> is not valid for this operation. | labor_activity_remove. The activity cannot be used for this operation. | Confirm the activity id and its state. |
| 3 | activity_id is not valid for this account. | labor_activity_finish and labor_activity_modify. The activity belongs to another account. | Use an activity from the authenticated account. |
| 3 | Worker <id> is not valid for this activity. | labor_activity_start. The worker is not assignable to the activity. | Use a worker returned for that activity. |
| 3 | Job <id> is not valid for this activity. | labor_activity_start. The job is not assignable to the activity. | Use a job returned for that activity. |
| 9 | The activity has already been finished. | labor_activity_finish. The activity is already closed. | Finished activities cannot be changed. |
| 9 | This activity cannot be removed because it is active. Please finish the activity before proceeding. | labor_activity_remove. The activity is still running. | Finish it, then remove it. |
| 9 | Activity conflicts with existing Workforce Hero activity data. | labor_activity_finish and labor_activity_start. The interval overlaps an existing activity. | Adjust the interval so it does not overlap. |
| 7 | User does not have permission to manage Workforce Hero activities. | labor_activity_finish, labor_activity_modify, labor_activity_remove and labor_activity_start. The user lacks the Warehouse Hero permission. | See Operations gated by internal permissions. |
Note
labor_worker_activities only accepts workers that are currently active in Workforce Hero. An inactive worker cannot be queried at all, and that is an upstream Workforce Hero limitation rather than something a different request shape can work around. The same operation pages at most 100 records and reads timestamps as UTC.
Warning
Data is temporarily unavailable. Please try again later comes back with code 14 from the labor mutations and code 9 from the labor queries. Despite the wording it is also what Workforce Hero returns when it rejects the request on permission grounds, so a failure that repeats is usually not an outage. Two things to check, in order: that the user is a regular ShipHero user holding the Warehouse Hero permission rather than a developer user (see Operations gated by internal permissions), and that Workforce Hero is active for the warehouse — that one is enabled by support@shiphero.com, not through the API.
Note
The labor queries return an empty page — not an error — when Workforce Hero does not recognize the warehouse. An empty result does not confirm the warehouse has no data; confirm the warehouse_id is one Workforce Hero tracks.
Carrier errors on label operations
The Experimental API operations that create, quote and void shipping labels call carriers directly. When a carrier rejects a request, the text in the response is the carrier’s own, not ShipHero’s: it can contain JSON, field paths, carrier error codes and provider-specific wording. Treat it as opaque text to surface or log, never as a stable string to branch on.
These errors come from the carrier’s systems, so the codes and common causes above do not classify them. What matters is which channel a given operation reports them through.
| Operation | Scope | How carrier problems are reported |
|---|---|---|
label_quote | view:shipments | As data, in errors, warnings and carrier_errors |
label_print | change:shipments | As a GraphQL error, with the carrier’s text as the message |
label_void | change:shipments | As a GraphQL error |
Quoting rates
label_quote reports carrier problems as data rather than as GraphQL errors, across three fields:
errors— top-level quote errors.warnings— non-blocking warnings; rates are still returned.carrier_errors— per-carrier errors, each entry carrying thecarrierand its list oferrors.
The include_partial_rates input decides whether a partial failure is fatal. It defaults to false:
false— if a carrier fails, the mutation fails. You get a GraphQL error and no rates.true— successful rates are returned inratesand the failures are reported incarrier_errorsalongside them.
Set include_partial_rates: true when quoting several carriers and a usable rate is better than none. A successful response can then still contain carrier failures, so check carrier_errors even when rates is populated.
Warning
With include_partial_rates: true, a quote that partially failed returns HTTP 200 with no errors array at the GraphQL level. Code that only inspects the top-level errors array will treat it as a clean success and silently miss that a carrier was unavailable.
mutation { label_quote(data: {order_id: "T3JkZXI6MTIzNDU2", include_partial_rates: true, ...}) {
rates { carrier cost } carrier_errors { carrier errors } } }
# → HTTP 200, rates populated, and carrier_errors: [{ "carrier": "ups", "errors": ["..."] }]Printing and voiding labels
label_print returns labels and weight_discrepancies. It has no field for carrier errors, so a carrier rejection surfaces as a GraphQL error whose message is the carrier’s raw text. The same applies to label_void, whose label_id must be an outbound ShippingLabel: return and RMA labels are rejected.
Important
This section covers only the Experimental label_quote, label_print and label_void operations. Labels produced by the Generate Label webhook are a different mechanism with their own behaviour, and nothing here describes how those are voided.
Because the message is passed through, the same underlying problem can read differently from one carrier to the next. Common causes worth checking before contacting support:
| What to check | Typical cause |
|---|---|
| Carrier connection | The carrier is not connected to the account, or its credentials no longer authenticate. |
| Shipping method mapping | The order’s shipping method is not mapped to a service the carrier account offers. |
| Carrier billing | The carrier account has a billing hold or has hit a spending limit. |
| Shipping address | The address is incomplete, undeliverable, or a field is longer than the carrier allows. |
| Package data | Weight or dimensions are missing, zero, or outside the carrier’s accepted range. |
| Carrier availability | The carrier is rate limiting the account or is temporarily down. |
Note
A failed label request does not always mean nothing happened at the carrier. Before retrying label_print, query the order’s shipments to confirm no label was created, so you do not pay for two.
# a label_print that failed — confirm nothing was created before retrying
query { order(id: "T3JkZXI6MTIzNDU2") { data { shipments { edges { node { shipping_labels { tracking_number } } } } } } }Operations gated by internal permissions
Appearing in the schema does not mean every token can run an operation. The labor and performance-dashboard operations call Warehouse Hero, a separate ShipHero service that re-authorizes the same user against its own warehousehero.manage.* permissions.
A developer user cannot run these at all. Developer users bypass the Public API’s own permission check and ShipHero’s internal one, but Warehouse Hero has no such bypass, and developer users hold no permissions for it to find. There is no scope, token or request change that gets past this.
The affected operations are pickers_performance_dashboard, packers_performance_dashboard, labor_workers, labor_jobs, labor_worker_activities, labor_activity_start, labor_activity_modify, labor_activity_remove and labor_activity_finish. They surface the rejection as Data is temporarily unavailable. Please try again later, which is why a persistent failure here is not an outage.
To use them, authenticate as a regular ShipHero user who holds the Warehouse Hero permission, rather than as a developer user. Which warehousehero.manage.* permission applies varies by operation and is not visible in the Public API schema, so ask in the ShipHero Community rather than inferring it. Workforce Hero also has to be active for the warehouse, which support@shiphero.com enables.
None of these accept customer_account_id. They always run against the account that owns the token, so a 3PL cannot target a client account with them.
Note
A downstream permission requirement can be added without any schema change, so this list is accurate as of this page’s last update rather than permanently complete. If an operation fails consistently with a permission or availability error and nothing on this page explains it, ask in the ShipHero Community with the request_id.
Reporting an error to support
Include the request_id from the error. It identifies your exact request and payload, which is usually enough to explain the failure without a reproduction.
Every query and mutation can also return request_id as a field, so you can capture it on successful operations that behaved unexpectedly:
{
request_id
complexity
orders {
request_id
complexity
}
}Ask in the ShipHero Community with the request_id. See Error Handling & Support.