ShopEazydevelopers
Libraries

Merchant client

@shopeazy/merchant-client: the typed client for the Merchant API.

Beta

The Merchant API is in Beta, and so is its client. The package version follows the API contract's version, which is 0.9 today.

@shopeazy/merchant-client is the typed client for the Merchant API (/v1/merchant), with one method for every operation, named after its operationId.

  • No runtime dependencies. It uses only fetch, and runs unchanged on Node 18 and later, in browsers and in React Native.
  • ESM with .d.ts types. The published types need neither the DOM library nor @types/node.
  • It sends Authorization: Bearer <token> from your getToken(): an OAuth access token for a connected app.
  • It accepts idempotencyKey only on operations that take one, which is every write.
  • It turns the { error } envelope into a typed MerchantApiError.

Install

The package is on npm under an MIT licence:

npm install @shopeazy/merchant-client

Create a client

import { createMerchantClient, newIdempotencyKey, isMerchantApiError } from '@shopeazy/merchant-client';

const api = createMerchantClient({
  baseUrl: 'https://api.shopeazy.co',
  // Return the current access token. Refresh it here when it is near expiry.
  getToken: () => tokens.accessToken,
  headers: { 'X-Client': 'my-app/1.0.0' },
});
OptionMeaning
baseUrlThe origin of the API. Paths already start with /v1/merchant.
getTokenReturns the bearer token for each call, or null to send none (the API then answers 401 missing_token)
fetchYour own fetch. Defaults to the global one
headersHeaders sent on every call

To get an OAuth access token, see Merchant OAuth.

Calls

Path, query and body are typed from the specification.

// Reads
const session = await api.getSession();
const page = await api.listOrders({ path: { storeId }, query: { queue: 'fulfil', limit: 30 } });
page.items;

// Writes: make the key once, when the person acts, and pass the same key on every retry
const key = newIdempotencyKey();
await api.updateProduct({
  path: { storeId, productId },
  body: { title: 'Rooibos tea' },
  idempotencyKey: key,
});

A call resolves to the data of the success envelope. When you need the status or the replay flag, use request():

const res = await api.request('refundOrder', {
  path: { storeId, orderId },
  body: { amountMinor: 5000, viaProvider: true },
  idempotencyKey: key,
});
res.idempotentReplay; // true when ShopEazy returned the first response for this key

Idempotency

The client never retries on its own. A retry has to reuse the same Idempotency-Key, and only your code knows which action a retry belongs to. A key must be 8 to 128 characters of A-Z a-z 0-9 _ -. Anything else throws a TypeError before the request is sent. See Idempotency.

Errors

try {
  await api.refundOrder({ path: { storeId, orderId }, idempotencyKey: key });
} catch (err) {
  if (!isMerchantApiError(err)) throw err;
  if (err.code === 'subscription_locked') showPlanLapsed();
  else if (err.isNotFound) goHome();             // store_not_found
  else if (err.isRetryable) retryWithSameKey();  // network, 409 in_flight, 502 and 503
  else toast(err.message);                       // written for the merchant, safe to show
}

MerchantApiError has status, code, message, operationId, body, requestId and details. It also has the helpers isUnauthorized (401), isForbidden (403), isNotFound (404), isRefused (422) and isRetryable.

code is typed as every code the specification documents, and stays open to strings ShopEazy adds later. The client adds two of its own:

StatusCodeWhen
0network_errorfetch rejected: offline, DNS or TLS. cause holds the original error
any non-2xxunexpected_responseThe body was not the { error } envelope, for example an HTML page from a gateway

An aborted request rethrows the AbortError unchanged. Quote requestId when you ask for help. See Errors.

Types

import type { Schema, OperationData, OperationArgs, MerchantErrorCode } from '@shopeazy/merchant-client';

type Order = Schema<'OrderDetail'>;
type Page = OperationData<'listOrders'>;

SPEC_VERSION exports the contract's version, so your app can log it.

Versions

The package version is the contract's info.version without its pre-release tag. While the contract is 0.x, a breaking change bumps the minor version. A new operation, optional parameter, response field, enum value or error code is also a minor release, and a description-only change is a patch. See Versioning and deprecation.

Operations for the ShopEazy app only

Some operations accept only the ShopEazy app's own sign-in, and answer an OAuth token with 403 forbidden. They are in the client because it serves the app too, and are not part of the connected-app surface.

On this page