Skip to content
Getting Started

Getting Started

This page covers the core concepts required to make your first query or mutation.

Sections included in this article:

Overview

ShipHero’s public API lives in https://public-api.shiphero.com, and there are two main endpoints:

  1. https://public-api.shiphero.com/auth (used for getting tokens)
  2. https://public-api.shiphero.com/graphql (used for fetching and modifying your data)

In order to make requests, you will first need to get a token that validates your identity. You get a token by authenticating with your user credentials. Once you have it, every request made to the API has to include it as part of the Authorization header as Bearer <your token>.

Note

API keys are not required for the public API. Authenticate with user credentials to obtain a token. If you need a dedicated public API user, create one and use that account to generate tokens.

Authentication

Authenticated requests are made with a JWT bearer token. To generate them you will have to provide your user credentials:

Tokens generated through https://public-api.shiphero.com/auth/token are not restricted by OAuth scopes. To limit a token to selected read, write, or PII permissions, follow the Scopes guide.

curl -X POST -H "Content-Type: application/json" -d 
'{ "username": "YOUR EMAIL", 
   "password": "YOUR PASSWORD" 
}' 
"https://public-api.shiphero.com/auth/token"

The response should look something like this:

{ "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IlJUQXlOVU13T0R
rd09ETXhSVVZDUXpBNU5rSkVOVVUxUmtNeU1URTRNMEkzTWpnd05ERkdNdyJ9.aktgc3MiOiJodHRwc
zovL3NoaXBoZXJvLmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHw1YmI3YTI4MjY4YTU2YzRjNTEzMTIx
MWIiLCJhdWQiOiJzaGlwaGVyby1wdWJsaWMtYXBpIiwiaWF0IjoxNTU0OTEwODc0LCJleHAiOjE1NTc
zMzAwNzQsImF6cCI6Im10Y2J3cUkycjYxM0RjT04zOAMRYUhMcVF6UTRka2huIiwic2NvcGUiOiJlbW
FpbCBwcm9maWxlIG9mZmxpbmVfYWNjZXNzIiwiZ3R5IjoicGFzc3dvcmQifQ.lW2UalihR5msHKhJzD
Pvy5SCKxSPyUCMuQ7RXyP2ZNQ2gENjGF2nmdsYlF2CqxH_wITcK10CproQErMK_yAWUSEck8qfC1Fu_
UNc9-xW55ALeCk09ZZD--aB_QFjLVM-ooawby7y4Ysf8H4yEBQpoPwZoQ3DQnu5QBNxd5oOLIP2ezzN
Yvrwjpm-uNN8II5sK9U075Mx1HH31KG14iFt5sEZQmYOz-oSWweVuY6Sd61VFD02sncXOmEZIxu3bda
ZSn1JYaM-ilLce4s748iv75BVDgqj1b2A1lyITeqvFoYWl3PKV56fOlfm8v9QnkSqR0iTGENgV6zZq3
rPRsBLTw", "expires_in": 2419200, "refresh_token": "cBWV3BROyQn_TMxETqr7ALQBaoF
gIzkC-8KkJaIq2HmK_", "scope": "openid profile offline_access", 
"token_type": "Bearer" }

Store the access_token and refresh_token. Use the access token as the bearer token for requests to the GraphQL API.

The access token expires every 28 days. You can refresh it with the refresh token without re-entering credentials. Store refresh tokens securely because anyone with one can generate new access tokens for your account.

To refresh a token:

curl -X POST -H "Content-Type: application/json" -d 
'{ "refresh_token": "YOUR REFRESH TOKEN" }' 
"https://public-api.shiphero.com/auth/refresh"

The response should look something like this:

{ "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IlJUQXlOVU13T0Rrd
09ETXhSVVZDUXpBNU5rSkVOVVUxUmtNeU1URTRNMEkzTWpnd05ERkdNdyJ9.aktgc3MiOiJodHRwczov
L3NoaXBoZXJvLmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHw1YmI3YTI4MjY4YTU2YzRjNTEzMTIxMWIi
LCJhdWQiOiJzaGlwaGVyby1wdWJsaWMtYXBpIiwiaWF0IjoxNTU0OTEwODc0LCJleHAiOjE1NTczMzAw
NzQsImF6cCI6Im10Y2J3cUkycjYxM0RjT04zOAMRYUhMcVF6UTRka2huIiwic2NvcGUiOiJlbWFpbCBw
cm9maWxlIG9mZmxpbmVfYWNjZXNzIiwiZ3R5IjoicGFzc3dvcmQifQ.lW2UalihR5msHKhJzDPvy5SCK
xSPyUCMuQ7RXyP2ZNQ2gENjGF2nmdsYlF2CqxH_wITcK10CproQErMK_yAWUSEck8qfC1Fu_UNc9-xW5
5ALeCk09ZZD--aB_QFjLVM-ooawby7y4Ysf8H4yEBQpoPwZoQ3DQnu5QBNxd5oOLIP2ezzNYvrwjpm-u
NN8II5sK9U075Mx1HH31KG14iFt5sEZQmYOz-oSWweVuY6Sd61VFD02sncXOmEZIxu3bdaZSn1JYaM-i
lLce4s748iv75BVDgqj1b2A1lyITeqvFoYWl3PKV56fOlfm8v9QnkSqR0iTGENgV6zZq3rPRsBLTw", 
"expires_in": 2419200, "scope": "openid profile offline_access", "token_type": 
"Bearer" }

Replace the previous access_token with the new one for subsequent API requests.

Adding a Third-Party Developer

For third-party developers, asking a client to share their ShipHero username and password is neither ideal nor secure.

Instead, create a third-party developer user and provide that user with the Bearer Token and Refresh Token.

To do this, go to https://app.shiphero.com/dashboard/users and click +Add Third-Party Developer.

After the developer is added, the Bearer Token and Refresh Token will be available. Provide those tokens to the developer:


Note

Always communicate secrets in a secure way.

Schema & Docs

The API is built with GraphQL.

GraphQL is self-documenting, so you can inspect the schema, queries, mutations, and types to see what is available, which parameters each operation accepts, and what each response returns.

You can browse a schema extract here. It is updated with every API release.

You can also use a client IDE to send requests and explore the schema. Common options include:

Note

Prefer the desktop versions or browser extensions of these clients. Web versions often have connection issues.

Using your token and pointing your client to https://public-api.shiphero.com/graphql, you can access both the schema and the docs.

For example, in GraphQL Playground or Altair:


Note

You do not need to run a query to access the schema and docs.

Queries & Mutations

The API exposes both queries and mutations, and every operation returns an object. Although GraphQL operations can return fields, lists, or connection fields, ShipHero wraps every operation in a BaseResponse. This allows response metadata to be included without adding those fields to resource types.

Queries

Queries support two additional parameters: sort and analyze.

  • sort applies to queries that return multiple results through connection fields. Pass a comma-separated list of attributes to sort by. Sorting is ascending by default, and each field can be prefixed with + or -. Example: sort: "name, -price"
  • analyze is a boolean flag that calculates query complexity without executing the query. For more information, see Throttling & Quotas.

The BaseResponse object returned on every query will always have the following fields:

  • request_id: A unique request identifier
  • complexity: The complexity of the query
  • data: The actual results of the query (a Field, List or Connection Field)

Every connection field accepts standard Relay pagination parameters. If first and last are omitted, the default maximum of 100 is applied. Requesting more results increases query complexity and consumes more quota credits.

Example getting all products

query { 
    products { 
        complexity 
        request_id 
        data(first: 10) { 
            edges { 
                node { 
                    id 
                    sku 
                    name 
                    warehouse_products { 
                        id 
                        warehouse_id 
                        on_hand
                    }
                }
            }
        }
    }
}

Example getting a product by SKU

query { 
    product(sku: "some-sku") { 
        complexity 
        request_id 
        data{ 
            id 
            sku 
            name 
            warehouse_products { 
                id 
                warehouse_id 
                on_hand
            }
        }
    }
}

Mutations

For mutations, the same rules apply, but in this case, the result is not called data, it’s defined on each mutation, so usually if you create a product, you will have a product field in the response.

Example creating a product

mutation { 
    product_create(data: { 
        name: "New Product" 
        sku: "P0001" 
        price: "10.00" 
        value: "2.00" 
        barcode: "000001" 
        warehouse_products: [
            { warehouse_id: "V2FyZWhvdXNlOjExNA==" on_hand: 5 },
            { warehouse_id: "V2FyZWhvdXNlOjEyODg=" on_hand: 15 }
        ]
    }) { 
        request_id 
        complexity 
        product { 
            id 
            name 
            sku 
            warehouse_products { 
                id
            }
        }
    }
}

Note

For more examples on Queries and Mutations you can also visit: Examples

Throttling & Quotas

ShipHero applies two independent limits to Public API requests:

  • Credit quota: Uses a replenishing pool of credits to limit the estimated cost of GraphQL operations.
  • Request-rate limit: Limits the total number of HTTP requests in a five-minute period.

Reaching either limit prevents new requests, but each limit has a different response and recovery path.

Credit quota

The standard Public API quota defaults to 4004 credits per account. Credits are restored at a rate of 60 per second, up to that maximum. This pool is shared by every standard Public API user and access token on the account.

Each GraphQL operation has an estimated complexity. ShipHero handles that estimate as follows:

  1. Before execution, ShipHero calculates the operation’s estimated complexity from its selected fields and pagination arguments.
  2. ShipHero reserves that number of credits. The estimate must not exceed either the account’s maximum quota or its currently available credits.
  3. After execution, ShipHero calculates the final cost from the data returned and any field-specific costs.
  4. ShipHero restores the difference between the reserved estimate and the final cost.

For example, if an operation has an estimated complexity of 5 and a final cost of 3, ShipHero restores 2 credits. The account is charged 3 credits.

Important

The standard Public API credit pool is account-wide. Concurrent requests reserve credits from the same pool, so parallel requests can temporarily leave fewer credits available for other operations.

Note

ShipHero MCP and Public API Skill requests use a separate AI Toolkit credit pool. They do not reduce the credits available for standard Public API requests.

GraphQL responses include the current quota under extensions.throttling.user_quota:

FieldMeaning
credits_remainingCredits available after the request’s final cost is settled.
max_availableMaximum credits the account can hold and the largest total estimated complexity a request can reserve.
increment_rateCredits restored per second.

Use the values returned by the API as the current limits for the account. The standalone user_quota query returns the same quota fields when you need to check them without running another business operation.

Example 1: Estimate a query without executing it

Add analyze: true to a query to calculate its complexity without executing it or consuming credits. Keep the same selected fields, filters, and pagination arguments that the executed query will use.

query AnalyzeProducts {
  products(analyze: true) {
    complexity
    request_id
    data(first: 2) {
      edges {
        node {
          id
          sku
          warehouse_products {
            warehouse_id
            sell_ahead
          }
        }
      }
    }
  }
}

Because the query is only analyzed, its data result contains the estimated complexity without product data:

{
  "data": {
    "products": {
      "complexity": 5,
      "request_id": "5cea345gsn87a",
      "data": {
        "edges": []
      }
    }
  }
}

Example 2: Compare estimated and final cost

Run the same query without analyze: true to retrieve data:

query Products {
  products {
    complexity
    request_id
    data(first: 2) {
      edges {
        node {
          id
          sku
          warehouse_products {
            warehouse_id
            sell_ahead
          }
        }
      }
    }
  }
}

This example requests up to two products but returns one. The response shows the estimated complexity, final cost, cost breakdown, and settled quota:

{
  "data": {
    "products": {
      "complexity": 5,
      "request_id": "5cea345gsn87a",
      "data": {
        "edges": [
          {
            "node": {
              "id": "UHJvZHVjdDoxMjM0NQ==",
              "sku": "DEMO-SKU-001",
              "warehouse_products": [
                {
                  "warehouse_id": "V2FyZWhvdXNlOjExNA==",
                  "sell_ahead": 0
                }
              ]
            }
          }
        ]
      }
    }
  },
  "extensions": {
    "throttling": {
      "estimated_complexity": 5,
      "cost": 3,
      "cost_detail": {
        "products": {
          "items_count": 1,
          "cost": 1,
          "total_cost": 3,
          "fields": {
            "WarehouseProduct.sell_ahead": {
              "count": 1,
              "value": 1,
              "total_cost": 1
            }
          }
        }
      },
      "user_quota": {
        "credits_remaining": 4001,
        "max_available": 4004,
        "increment_rate": 60
      }
    }
  }
}

ShipHero reserved 5 credits before execution, charged the final cost of 3, and restored the remaining 2. The quota values in this example assume the account started with all 4004 credits and had no concurrent requests.

Example 3: Handle an insufficient-credit error

If an operation’s estimate exceeds the credits currently available, the operation is not executed and the response includes error code 30:

{
  "errors": [
    {
      "code": 30,
      "message": "There are not enough credits to perform the requested operation, which requires 241 credits, but there are only 61 left. In 3 seconds you will have enough credits to perform the operation",
      "operation": "inventory_changes",
      "request_id": "5da7dc13f3079f0def208711",
      "required_credits": 241,
      "remaining_credits": 61,
      "time_remaining": "3 seconds"
    }
  ],
  "data": {
    "inventory_changes": null
  }
}

Handle credit errors according to the estimate and quota:

if estimated_complexity > max_available:
    reduce the selected fields or connection page sizes
else if estimated_complexity > credits_remaining:
    wait until enough credits are restored, then retry
else:
    execute the operation

Waiting only helps when the estimate fits within max_available. If the estimate exceeds max_available, reduce the operation’s cost before retrying. When a credit error provides time_remaining, wait at least that long and avoid sending parallel retries from the same account.

Request-rate limit

The Public API also allows a maximum of 7,000 requests during any five-minute period. This limit includes failed requests and IntrospectionQuery requests.

Exceeding this limit returns HTTP 429 Too Many Requests until the request count falls below the limit. This response is separate from the GraphQL credit error shown above.

Optimizing a Query

Connection fields can multiply a query’s estimated complexity, especially when connections are nested. To keep estimates predictable:

  • Set first or last on every connection. If neither is present, the complexity calculation assumes a page size of 100.
  • Use a realistic page size. A larger value reserves more credits even when the response ultimately contains fewer records.
  • Paginate nested connections independently instead of requesting a large outer page and large nested pages together.
  • Request only the fields the integration needs. Fields with an additional cost appear in extensions.throttling.cost_detail.
  • Use analyze: true with the final fields, filters, and page sizes before executing a complex query.

Include pageInfo.hasNextPage and pageInfo.endCursor on each connection so the integration can continue while more records are available:

query Orders($after: String) {
  orders {
    complexity
    request_id
    data(first: 10, after: $after) {
      edges {
        node {
          id
          order_number
          line_items(first: 10) {
            edges {
              node {
                id
                sku
                quantity
              }
            }
            pageInfo {
              hasNextPage
              endCursor
            }
          }
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
}