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.tstypes. The published types need neither the DOM library nor@types/node. - It sends
Authorization: Bearer <token>from yourgetToken(): an OAuth access token for a connected app. - It accepts
idempotencyKeyonly on operations that take one, which is every write. - It turns the
{ error }envelope into a typedMerchantApiError.
Install
The package is on npm under an MIT licence:
npm install @shopeazy/merchant-clientCreate 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' },
});| Option | Meaning |
|---|---|
baseUrl | The origin of the API. Paths already start with /v1/merchant. |
getToken | Returns the bearer token for each call, or null to send none (the API then answers 401 missing_token) |
fetch | Your own fetch. Defaults to the global one |
headers | Headers 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 keyIdempotency
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:
| Status | Code | When |
|---|---|---|
| 0 | network_error | fetch rejected: offline, DNS or TLS. cause holds the original error |
| any non-2xx | unexpected_response | The 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.