A API Overview#

a. Regions

#

The FRAGMENT API is available in the following AWS regions:

  • us-east-1
  • us-east-2
  • us-west-2
  • eu-west-1

The region for a workspace can be found in the API URL in the settings tab of the dashboard. Use the top-left dropdown in the dashboard to create a new workspace.

Contact us if you don't see your desired AWS region.

b. Authentication

#

FRAGMENT uses OAuth2's client credentials flow to authenticate API clients. The call requesting an access token follows the OAuth2 spec. Use an OAuth2 library that supports the client credentials grant to retrieve the token or make an HTTP request. The flow is:

  1. Create an API client in the FRAGMENT dashboard. API Clients have complete access to the workspace.
  2. Note the auth endpoint URL and OAuth scope for the API Client. This is specific to the AWS region for a given workspace.
  3. Get a fresh access token from the token endpoint. The access token expires in 1 hour. We recommend retrieving a new token for each set of calls needed to be made. Multiple tokens can be generated and use simultaneously.
  4. To use the token, add the Authorization request header with the value Bearer {{access_token}} when calling the GraphQL API.

The token request payload should be in x-www-form-urlencoded format, with the keys grant_type, scope and client_id.

Token request body
grant_type=client_credentials&scope={{scope}}&client_id={{client_id}}

The token request headers should contain the following items, where client_credentials is the Base64-encoded version of: {{client_id}}:{client_secret}}.

Token request headers
{
    "Content-Type": "application/x-www-form-urlencoded",
    "Authorization": "Basic {{client_credentials}}",
    "Accept": "*/*"
}

The response is a JSON object containing the access token.

Token response body
{
    "access_token": "<access token>",
    "expires_in": 3600,
    "token_type": "Bearer"
}

Putting it all together:

Javascript
import axios from "axios";

const btoa = (str) => Buffer.from(str).toString("base64");

// credentials from the dashboard
const clientId = "2h4t0cv7qv5c9r0qgs1o3erkuk";
const secret = "superSecretClientSecret";
const scope = "https://api.us-west-2.fragment.dev/*";
const authUrl = "https://auth.us-west-2.fragment.dev/oauth2/token";

const getToken = async () => {

  // encode the client id and secret
  const auth = btoa(`${clientId}:${secret}`);

  // create the request body
  const data = new URLSearchParams();
  data.append("grant_type", "client_credentials");
  data.append("scope", scope);
  data.append("client_id", clientId);

  // retrieve the token
  const response = await axios.request({
    method: "POST",
    url: authUrl,
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      Authorization: `Basic ${auth}`,
      Accept: "*/*",
    },
    data,
  });

  if (!response.data.access_token) {
    throw new Error(
      "didn't get an access token from auth endpoint"
    );
  }
  return response.data.access_token;
};
 

c. Idempotency

#

To ensure write operations are executed exactly once, all write mutations in FRAGMENT are idempotent. This enables applications to safely retry operations without risk of duplicate effects. Applications only need to guarantee that the API is called at least once.

Mutations require a unique idempotency key (ik). For calls to reconcileTx, syncCustomAccounts and syncCustomTxs, FRAGMENT internally uses the transaction or account ID from the request as the idempotency key.

Additional requests with the same ik are ignored and the original response is returned with isIkReplay: true in the response.

Idempotency keys are scoped per-Ledger; requests with the same IK to different Ledgers will execute the mutation and return isIkReplay: false.

d. Errors

#

According to the GraphQL spec, all responses will return an object containing data and errors.

data: the result of a query or mutation. In the FRAGMENT API, all successful mutations return a union type that represents an application error or a successful result.

For example, the response type of the addLedgerEntry mutation is:

API Response Union Types
union AddLedgerEntryResponse =
    AddLedgerEntryResult | BadRequestError | InternalError

Queries can return null if an error was raised during the request. This is accompanied by a non-empty errors list.

errors: a list of errors raised during the request. This typically gets populated when issuing a query if a required argument is missing or an invalid ID is provided.

When calling the API, handle errors in the following order:

  1. Handle retryable HTTP errors. These are 429 or 5XX.
  2. Handle non-retryable HTTP errors such as other 4XX status codes. This typically happens if the query does not conform to the GraphQL spec, like omitting a required variable or selecting a nonexistent field. This class of error can be avoided by using a generated, typed SDK.
  3. Handle null responses from queries by checking the errors key. This typically happens when querying an item with an invalid ID.
  4. Query the __typename field. This contains BadRequestError, InternalError or the appropriate <Mutation>Result type.
  5. Handle InternalError with retries and exponential backoff.
  6. Handle BadRequestError by failing the operation.
  7. Handle <Mutation>Result types such as AddLedgerEntryResult.

An example of this written in TypeScript:

Handling API Responses
import assert from 'assert';

type RequestParams = {
  query: string;
  variables: Record<string, unknown>;
};

const makeRequest = async ({ query, variables }: RequestParams): Promise<Response> => {
  const token = await getToken();
  return fetch('Fragment GraphQL URL', {
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ query, variables }),
  });
};

const query = '''
  mutation addLedgerEntry($ik: SafeString, $entry: LedgerEntryInput!) {
    addLedgerEntry(ik: $ik, entry: $entry) {
      __typename
      ...
    }
  }
''';
const variables = { ik: 'sample-ik', entry: {...} };

const handleRequest = (req: Response) => {
  if (req.status === 429 || req.status >= 500) {
    // Rate-limited or intermittent http failures. Retry the request.
    return handleRequest(await makeRequest({ query, variables }));
  }
  if ((req.status >= 400 || req.status < 500) && req.status !== 429) {
    // Invalid GraphQL request provided to Fragment. Handle the error.
    throw new Error('Invalid GraphQL request');
  }

  // .json() checks that it was a 200
  const response = await req.json();
  if (response.data.addLedgerEntry.__typename === 'InternalError') {
    // Retry the request in case of internal errors, with backoff.
    return handleRequest(await makeRequest({ query, variables }));
  }
  if (response.data.addLedgerEntry.__typename === 'BadRequestError') {
    // Invalid request provided to Fragment. Handle the error.
    throw new Error('Invalid API request to Fragment');
  }
  return response;
};

const response = handleRequest(await makeRequest({ query, variables }));
// Entry successfully posted to Fragment. Handle the response.
assert(response.data.addLedgerEntry.__typename === 'AddLedgerEntryResult');
handlePostedEntry(response.data.addLedgerEntry);
 

To test how your application handles errors, use the X-Fragment-Return-Error request header to instruct the API to return erroneous responses.

Set the X-Fragment-Return-Error header to:

  • internalError to instruct the API to return an InternalError
  • badRequestError to instruct the API to return a BadRequestError

When requesting an erroneous response, FRAGMENT will skip processing your request and return the error immediately.