# GraphQL

> Read the shop, products and collections in one request, with the schema, examples, errors and limits. Carts and checkout stay in the storefront API.

Source: https://docs.bifrostorbit.com/commerce/developers/graphql/

GraphQL reads the same shop, products and collections as the [storefront API](https://docs.bifrostorbit.com/commerce/developers/storefront-api/). Use it when one page needs several things at once, such as the shop's currencies, a product and a collection menu, in a single request.

```
https://app.bifrostorbit.com/commerce/api/cdn/graphql
```

GraphQL only reads. Carts, checkout and orders stay in the storefront API.

## Requests

GraphQL has one address for every shop, so it needs to know whose products to read. Either:

- add `?organisation=` and the shop address from **Settings**, such as `?organisation=northwilds`. This needs no key.
- or send an [API key](https://docs.bifrostorbit.com/commerce/developers/api-keys/) as `Authorization: Bearer <key>` or `X-Api-Key: <key>`. The key says which shop it belongs to, and a secret key (`com_sk_`) adds draft products.

With both, the key must belong to that shop.

To read as a signed-in customer, also send their token as `X-Customer-Token: cst_…`. Prices and stock are then theirs, and `customer` returns who they are. Only the shop's allowed websites, or your server, may send a customer token. See [Customer accounts and personal prices](https://docs.bifrostorbit.com/commerce/developers/customer-accounts/).

`POST` a JSON body with `query`, and optionally `variables` (an object) and `operationName`:

```bash
curl 'https://app.bifrostorbit.com/commerce/api/cdn/graphql?organisation=northwilds' \
  -H 'Content-Type: application/json' \
  -d '{"query":"query ($slug: String!) { product(slug: $slug, locale: \"nb\", currency: \"NOK\") { title price { min currency } available } }","variables":{"slug":"tind-2-tent"}}'
```

`GET` works too, with `query`, `variables` (as JSON text) and `operationName` in the query string, URL-encoded:

```bash
curl -G 'https://app.bifrostorbit.com/commerce/api/cdn/graphql' \
  --data-urlencode 'organisation=northwilds' \
  --data-urlencode 'query={ products(tag: "winter", limit: 12) { slug title price { min currency } } }'
```

Answers are JSON with `data`, and `errors` when something went wrong. Any website may call it from a browser. A secret key works only from a server; from another website it gets `403`. Answers aren't cached by us: cache them on your side.

## Queries

| Query | Arguments | Returns |
| --- | --- | --- |
| `store` | | `Store!`: name, logo, colour, currencies, languages, whether prices include VAT and the hosted shop address |
| `customer` | | `Customer`: the signed-in customer, or `null` without a customer token |
| `products` | `ids`, `slugs`, `tag`, `collection`, `query`, `limit`, `locale`, `currency` | `[Product!]!`: active products, as in the REST list |
| `product` | `slug: String!` (address or id), `locale`, `currency` | `Product`, or `null` when there is no such active product |
| `collections` | `ids`, `locale` | `[Collection!]!`: active collections |
| `collection` | `slug: String!` (address or id), `locale`, `currency`, `limit` | `Collection` with its `products`, or `null` |

`locale` and `currency` work as in REST. `limit` is 100 unless you say, at most 250 for `products`.

## Types

| Type | Fields |
| --- | --- |
| `Store` | `slug`, `name`, `logoUrl`, `colour`, `currency`, `currencies`, `locales`, `defaultLocale`, `pricesIncludeTax`, `shopUrl`, `termsUrl`, `returnsDays` |
| `Product` | `id`, `slug`, `title`, `summary`, `description` (HTML), `vendor`, `productType`, `tags`, `options { name values }`, `variants`, `images { url alt }`, `specs { label value }`, `price`, `available`, `status`, `seo { title description }`, `url`, `locale`, `updatedAt`, `stockSyncedAt`, `pricing { source account checkedAt }`, `metafields` (JSON, by key), `bundle { components { productId variantId quantity } }` |
| `Variant` | `id`, `title`, `options`, `sku`, `price`, `compareAtPrice`, `available`, `quantity` (`null` when stock isn't tracked), `listPrice`, `priceSource`, `branches { code name quantity yours }`, `requiresShipping`, `digital`, `plans { id name interval intervalCount price }`, `metafields` (JSON) |
| `Customer` | `email`, `name`, `company { name terms termsDays }`, `account { number status onStop creditLimit balance branch branchName }`, `priceList` |
| `PriceRange` | `min`, `max`, `currency`, `compareAt` |
| `Collection` | `id`, `slug`, `title`, `description`, `image { url alt }`, `productCount`, `url`, `products` (only on `collection`) |

Amounts are whole minor units, as in REST: `38900` with `EUR` is €389.00. `price` is `null` on a variant with no price in the chosen currency.

## Examples

### A product page

```graphql
query ProductPage($slug: String!, $locale: String, $currency: String) {
  store {
    name
    currencies
    pricesIncludeTax
  }
  product(slug: $slug, locale: $locale, currency: $currency) {
    title
    description
    images {
      url
      alt
    }
    options {
      name
      values
    }
    variants {
      id
      title
      price
      compareAtPrice
      available
      quantity
    }
    specs {
      label
      value
    }
  }
}
```

```json
{ "slug": "fjell-gtx-boot", "locale": "nb", "currency": "NOK" }
```

### A collection with the menu

```graphql
{
  collection(slug: "tents", limit: 12) {
    title
    products {
      slug
      title
      price {
        min
        currency
      }
      available
    }
  }
  collections {
    slug
    title
    productCount
  }
}
```

## Errors

Errors come in `errors`, each with a `message`, a `code` under `extensions`, and the `path` and `locations` of the field when there is one.

| Status | When |
| --- | --- |
| `400` | No `query`, `variables` that aren't a JSON object, a query that doesn't parse or match the schema (`bad_request`), or one over the limits (`query_too_deep`, `query_too_complex`) |
| `401` | Neither a key nor `?organisation=`, or a key that is wrong, revoked, expired or for another shop (`unauthorized`) |
| `403` | A secret key sent from a browser on another website (`forbidden`) |
| `402` | The shop is paused because its Commerce Orbit trial or plan ended (`subscription_inactive`) |
| `404` | No shop with that address (`not_found`) |
| `413` | A query longer than 20,000 characters (`query_too_large`) |
| `429` | Too many requests in a short time (`rate_limited`). Wait a minute. |
| `500` | Something failed on our side (`server_error`). Try again. |

`product` and `collection` answer `null` when nothing matches, without an error.

## Limits

- A query may be at most 20,000 characters long, nest at most 10 levels and ask for at most 300 fields. Each alias and each use of a fragment counts.
- Requests share the catalogue limit of the storefront API: 120 a minute per IP address and shop.
