Submarine GraphQL API

Your gateway to Submarine.

1. Introduction

The Submarine GraphQL API offers flexibility and the ability to define precisely the data you want to fetch. We assume that the reader has a basic understanding of how GraphQL APIs work.

1.1. About GraphQL

The GraphQL data query language has the following properties.
  • It’s a specification The spec determines the validity of the schema on the API server. The schema determines the validity of client calls.
  • It’s strongly typed The schema defines an API's type system and all object relationships.
  • It’s introspective A client can query a GraphQL schema for details about itself.
  • It’s hierarchical The shape of a GraphQL call mirrors the shape of the JSON data it returns. Nested fields let you query for and receive only the data you specify in a single round trip.
  • It’s an application layer GraphQL is not a storage model or a database query language. The graph refers to graph structures defined in the schema, where nodes define objects and edges define relationships between objects. The API traverses and returns application data based on the schema definitions, independent of how the data is stored.

1.2. GraphQL endpoint

Although REST APIs usually have many endpoints, Submarine’s GraphQL API only has one.
All queries are executed by sending POST HTTP requests to:
plain text
https://api.getsubmarine.com/graphql
The Submarine GraphQL API endpoint.
This endpoint remains constant, no matter what operation you perform.

1.3. Authentication

Every request to the API (including those for introspection) require a valid API key.
  • If you’re making requests on behalf of a Shopify store, you will need a channel token. This can be retrieved from the Submarine UI:
    • The Submarine UI exposes the API key that needs to used in every request.
      The Submarine UI exposes the API key that needs to used in every request.
  • If you’re making requests on behalf of a Shopify customer, you will need a customer token. See XXX for details about how to generate such a token.
The API key should be presented in the Authorization header as a bearer token:
plain text
curl -H "Authorization: Bearer API_KEY" -X POST -d " \ { \ \"query\": \"query { channel { identifier } }\" \ } \ " https://api.getsubmarine.com/graphql
Presenting the Submarine API key as a bearer token.

1.4. Rate limiting

The Submarine API has limitations in place to protect against excessive or abusive calls to Submarine's servers. It’s calculated differently to more typical REST API rate limits, and performs validations on a query analysis of the incoming request.
Query complexity
Submarine GraphQL fields have a “complexity” value, which is indicative of how complex it is to retrieve the value internally. Each Submarine service defines a per-query maximum complexity, which, if exceeded, will result in a fatal error.
Most fields have a complexity of 1 and most mutations a cost of -10 points and most services max at at a complexity of 1000.
Connection fields
The concept of complexity is extended to consider query connections. The calculations are essentially:
  • adding 1 for pageInfo and each of its sub-selections;
  • adding 1 for counttotalCount or total;
  • adding 1 for the connection field itself; and
  • multiplying the complexity of other fields by the largest possible page size, which is the greater of first: or last:.
For example, this query has complexity 26:
graphql
query { channel { # +1 identifier # +1 presaleCampaigns(first: 10) { # +1 edges { node { # +10 (+1, multiplied by `first:` above) id # +10 (ditto) } } pageInfo { # +1 endCursor # +1 } totalCount # +1 } } }
A breakdown of the complexity calculation for a query.
Should a request hit this limit, a top-level error will be returned in the response:
json
{ "errors": [ { "message": "Query has complexity of 1023, which exceeds max complexity of 1000" } ] }
Hitting the maximum-complexity limit.
Deeply nested queries
The last limit applied is a strict upper bound on how deep the nesting of the query goes (from the top-level query down to the lowest resource). There is a flat maximum depth defined here of 20.
Should a request hit this limit, a top-level error will be returned in the response:
json
{ "errors": [ { "message": "Query has depth of 22, which exceeds max depth of 20" } ] }
Hitting the maximum-depth limit

1.5. IDs

There are lots of times when you are going to need to specify an ID in a GraphQL request:
  • identifying a customer to update;
  • specifying what products define a presale campaign;
  • retrieving a campaign order group.
Internally, Submarine uses ULIDs to identify resources. These are essentially UUIDs, but are lexicographically sortable. An example is “018436f0-5a23-12dd-882e-89af29dfb5d6”.
The Submarine API does not expose these IDs directly though. It instead decorates them with additional data so they become Global IDs.
Suppose the above ULID identified a customer. If you wanted to reference that customer by their ID in the Submarine API, you would need to present it as a Submarine GlobalID type:
plain text
gid://submarine/Customer/018436f0-5a23-12dd-882e-89af29dfb5d6
An example of a Submarine Global ID (defining a customer).
The component parts of the Global ID are the app name (”submarine” here), the resource name (”Customer”) and the resource ID (”018436f0-5a23-12dd-882e-89af29dfb5d6”).
This is fine most of the time. Occasionally, however, there are situations where you don’t have access to a Submarine ID, but you do have access to a Shopify ID. A common example is getting all the Submarine campaign orders for a Shopify order, which you would feasibly have to do if you were building a customer portal.
To support use cases like this, Submarine maintains an external ID for resources that have a presence in the external e-commerce platform. As mentioned, the associated resource for a Submarine campaign order group is a Shopify order, so Submarine persists the Shopify order ID as the campaign order group’s externalId. Not only that, it also allows you to use that external ID as the lookup identifier.
To perform a lookup on an external ID, we just need to tweak the format of the Global ID a bit — swap the app name to be “external”, and replace the resource ID part to point to the external ID:
plain text
gid://external/Customer/6204442509552
An example of an external Global ID (defining the same customer).
The lookup references the same resource (albeit using a different ID), so returns the same data. An indication that you can use this lookup mechanism is if the underlying GraphQL type is SharedGlobalID (as opposed to GlobalID).

1.6. Errors

In a significant departure from more traditional RESTful error handling, all but the most fatal errors return a 200 OK response. In such cases, it is the responsibility of the caller to interrogate the returned data to determine if the request was successful or not.
Validation errors
Because GraphQL is strongly typed, it performs validation on all queries before executing them. If an incoming query is invalid, it isn’t executed. Instead, a response is sent back with a top-level “errors” block.
So, this query (which asks for the non-existent identifiers field on the Channel type):
graphql
query { channel identifiers } }
Querying an invalid field.
Returns this response:
json
{ "errors": [ { "message": "Cannot query field 'identifiers' on type 'Channel'. Did you mean 'identifier'?", "locations": [ { "line": 1, "column": 32 } ], "path": [ "query channel", "channel", "identifiers" ], "extensions": { "code": "undefinedField", "typeName": "Channel", "fieldName": "identifiers" } } ] }
The response from querying an invalid field.
To aid with debugging, each error has a message, line, column and path.
Analysis errors
As mentioned in Submarine GraphQL APISubmarine GraphQL API above, Submarine performs some analysis of each request to determine whether it fits within our fair-use policy. Should any of those validations fail, a top-level error will be surfaced.
“Not found” errors
Instead of returning a 404 HTTP code when a resource is not found, Submarine instead returns a null value where the resource would normally be.
For example, if we provided an unknown channel ID to this query:
graphql
query { channel(id: "gid://submarine/Channel/01827ac5-2dd1-b0a3-cd1b-45c3a0e942db") { identifier } }
Querying an unknown resource.
We’d get a response like this:
json
{ "data": { "channel": null } }
The response from querying an unknown resource.
Mutation errors
A GraphQL mutation is the equivalent of submitting an HTML form or a POST RESTful API request. Even though a mutation may pass Submarine’s schema validation and complexity analysis, there is a reasonable expectation that it will occasionally flout the constraints on the underlying business logic, and will be rejected.
In such cases, Submarine returns an array of user errors, which describe all issues with the mutation payload.
Consider the creation of a new customer:
graphql
mutation customerCreate($input: CustomerCreateInput!) { customerCreate(input: $input) { customer { id } userErrors { field message } } }
The mutation to create a customer.
The following input is supplied:
json
{ "input": { "externalId": "6204442509552" } }
The input used to create a new customer.
The mutation complies with the GraphQL schema, and definitely sits within the rate limits. However, there are two problems:
  • the external ID has already been taken by another customer; and
  • Submarine requires that one of email of phone is provided.
The response we get back describes both of these errors, and returns a null value for the customer, indicating that nothing was created:
json
{ "data": { "customerCreate": { "customer": null, "userErrors": [ { "field": [ "externalId" ], "message": "External ID has already been taken" }, { "field": [ "contact" ], "message": "Either email or phone needs to be present" } ] } } }
The response from the invalid mutation.

1.7. Metadata

Most updatable Submarine objects have a metadata field, which can be used to attach arbitrary key-value data — useful for storing additional, structured information on an object.
Some Submarine objects also support a description field (e.g. Charge, Refund). You could use this field, for example, to annotate a charge with a human-readable description, e.g. “Presale: SKU-123, due July 23, 2023”. Unlike metadatadescription is a single string, and is intended to be exposed to end users.

1.8. Pagination

The Submarine API uses a cursor-based implementation of Relay’s connection-style pagination (see the the docs and the associated specification).
This is best described by an example, e.g. getting a list of customers:
graphql
query customers { customers(first: 2, after: "Mg") { edges { node { id } cursor } pageInfo { endCursor } } }
Getting the first two customers after the one with a cursor of “Mg”.
We asked for the next two customers after the one with a cursor of “Mg”:
json
{ "data": { "customers": { "edges": [ { "node": { "id": "gid://submarine/Customer/0184e067-17cd-b04c-d830-1bf60e33e30b" }, "cursor": "Mw" }, { "node": { "id": "gid://submarine/Customer/0184e072-e87a-1519-6026-0bc25dbd9109" }, "cursor": "NA" } ], "pageInfo": { "endCursor": "NA" } } } }
The requested customers, with their corresponding cursors, along with the end cursor.
If we wanted to get the next “page”, we’d update the value of after to be “NA”.

1.9. Search

Currently, the search options are very limited. This is next on our roadmap for the Submarine API.

1.10. Request IDs

Each request to the Submarine API has an associated request identifier. You can find this value in the response headers, under Request-Id.

1.11. Versioning

The Submarine GraphQL API is not yet versioned.

2. API reference

See the generated docs for the Submarine GraphQL API.

GraphQL API Reference