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 texthttps://api.getsubmarine.com/graphql
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:
- 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 textcurl -H "Authorization: Bearer API_KEY" -X POST -d " \ { \ \"query\": \"query { channel { identifier } }\" \ } \ " https://api.getsubmarine.com/graphql
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
count
,totalCount
ortotal
;
- 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:
orlast:
.
For example, this query has complexity 26:
graphqlquery { 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 } } }
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" } ] }
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" } ] }
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 textgid://submarine/Customer/018436f0-5a23-12dd-882e-89af29dfb5d6
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 textgid://external/Customer/6204442509552
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):graphqlquery { channel identifiers } }
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" } } ] }
To aid with debugging, each error has a message, line, column and path.
Analysis errors
As mentioned in Submarine 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:
graphqlquery { channel(id: "gid://submarine/Channel/01827ac5-2dd1-b0a3-cd1b-45c3a0e942db") { identifier } }
We’d get a response like this:
json{ "data": { "channel": null } }
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:
graphqlmutation customerCreate($input: CustomerCreateInput!) { customerCreate(input: $input) { customer { id } userErrors { field message } } }
The following input is supplied:
json{ "input": { "externalId": "6204442509552" } }
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" } ] } } }
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 metadata
, description
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:
graphqlquery customers { customers(first: 2, after: "Mg") { edges { node { id } cursor } pageInfo { endCursor } } }
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" } } } }
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
https://graphql-docs.getsubmarine.com/