> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shoply.gg/llms.txt
> Use this file to discover all available pages before exploring further.

# Node.js

> Every route of the API as a typed method, for your own server

`@shoplygg/node` is the API for a store's own server: every route of this reference is a method, with TypeScript types, retries that are safe to repeat, lists a page at a time or all at once, and a check that a webhook came from Shoply. Each route's page shows its call.

```sh theme={null}
npm install @shoplygg/node
```

Node 20 or later. It has no dependencies.

```js theme={null}
import Shoply from "@shoplygg/node";

const shoply = new Shoply({ apiKey: process.env.SHOPLY_API_KEY });

const order = await shoply.orders.show("ORDER_ID");
console.log(order.email, order.amount, order.status);
```

Make the key under **Developers**, **API Keys** in the dashboard, with the permissions your code needs ([Authentication](/api-reference/authentication.md)). Left out, `apiKey` is read from `SHOPLY_API_KEY`. A key is a secret: the client refuses to start in a web page. For a buyer's browser, use [the Customer API](#the-customer-api) below.

## A route is a method

A route's method is its name in this reference, and its page shows the call. **Get an order** is `orders.show`, so `shoply.orders.show(orderId)`. A call takes:

1. The IDs in the route's address, in order. The store's own is filled in for you.
2. Its fields, as one object: what goes in the address and what goes in the body are sorted out for you.
3. Options, last: `idempotencyKey`, `timeout`, `maxRetries`, `signal`, `headers`.

```js theme={null}
const coupon = await shoply.coupons.create({ code: "SPRING10", value: 10, maxUses: 100 });

await shoply.products.setVisibility(productId, { visibility: "unlisted" });

await shoply.orders.markPaid(orderId);
```

A call answers the route's `data`: the order, the coupon, the list. The store is asked of the key once, the first time it is needed (`GET /key`). Pass `storeId` to skip that.

| Option | What it does |
| - | - |
| `apiKey` | The key. Left out, `SHOPLY_API_KEY`. |
| `storeId` | The store's ID. Left out, asked of the key once. |
| `timeout` | How long one try waits for an answer, in milliseconds. 60 seconds. |
| `maxRetries` | How many more tries a request that is safe to repeat is given. 2. |
| `baseURL` | Where the API is. `https://api.shoply.gg/v1`. |
| `fetch` | The `fetch` to ask with. The global one. |

## Lists

A list that is paged answers its first page, which `for await` walks row by row through every page.

```js theme={null}
const page = await shoply.orders.list({ status: "completed", perPage: 50 });

console.log(page.data.length, page.total, page.pages);

const next = await page.nextPage(); // null after the last

for await (const customer of shoply.customers.list({ search: "gmail.com" })) {
  console.log(customer.emailAddress);
}
```

A page has `data`, its rows, `page`, `pages`, `total`, and `meta`, whatever else the list answers beside its rows (counts, figures). A list that is not paged, such as **List products**, answers its rows.

## Errors

A refusal is a `ShoplyError`.

| Field | What it is |
| - | - |
| `status` | The HTTP status. 0 when nothing answered. |
| `code` | The word that does not change: `not_found`, `missing_scope`, `validation_failed`, `rate_limited`, and the route's own. `connection_error` or `timeout` when nothing answered. See [Errors](/api-reference/errors.md). |
| `message` | What is wrong, in words for a person. |
| `errors` | For `validation_failed`, what is wrong with each field, by its name. |
| `requestId` | The request's ID. Quote it to Shoply about it. |
| `retryAfter` | Seconds to wait, when told to slow down. |
| `body` | The whole answer. |

```js theme={null}
import { ShoplyError } from "@shoplygg/node";

try {
  await shoply.coupons.create({ code: "SPRING10", value: 10 });
} catch (error) {
  if (error instanceof ShoplyError && error.code === "validation_failed") {
    console.log(error.errors?.code);
  } else {
    throw error;
  }
}
```

## Trying again

Every change is sent with an [Idempotency-Key](/api-reference/idempotency.md), a new one for each call and the same on each of its tries. So a try whose answer was lost is answered as the first one was, never carried out twice. To make a change once however many times your own code runs it, pass your own. Options come after the fields, so a route whose fields you leave out takes `{}` before them:

```js theme={null}
await shoply.orders.markPaid(orderId, {}, { idempotencyKey: `paid-${invoiceNumber}` });
```

A read, or a change with a key, is tried again up to twice more when nothing answered, or the answer was 408, 429, 500, 502, 503 or 504, or 409 `request_in_progress`. It waits as long as `Retry-After` says, or half a second, then a second, then two, a little more or less. Told to wait more than a minute, it does not, and throws with `retryAfter`. An upload is not tried again, since it carries no key.

Pass `{ maxRetries: 0 }` to a call, or to the client, to try once. Pass `{ signal }` to stop a call.

## Files

A route that answers a file, a CSV or a PDF, answers the `Response` itself:

```js theme={null}
const file = await shoply.orders.export({ status: "completed" });
await fs.promises.writeFile("orders.csv", Buffer.from(await file.arrayBuffer()));
```

An upload takes `file` as a `Blob`, a `File`, a `Buffer` or a `Uint8Array`:

```js theme={null}
import { readFileSync } from "node:fs";

await shoply.productImages.upload(productId, {
  file: readFileSync("cover.png"),
  filename: "cover.png",
  contentType: "image/png",
});
```

What else the route takes goes beside `file`: a dispute's file says what it is.

```js theme={null}
await shoply.disputes.addFile(disputeId, { file: readFileSync("chat.pdf"), filename: "chat.pdf", kind: "communication" });
```

## A route newer than the package

`request()` reaches any route by its method and its address after `/v1`, with `{storeId}` filled in. It answers the whole envelope ([Conventions](/api-reference/conventions.md)).

```js theme={null}
const { data, meta } = await shoply.request("GET", "/stores/{storeId}/products", { query: { search: "pro" } });
```

## Webhooks

`webhooks.verify` checks a delivery's signature with your endpoint's signing secret, refuses one signed more than five minutes ago, and answers the event. Pass the raw body, as it arrived: a body parsed and written out again is not what was signed. See [Webhooks](/api-reference/webhooks.md).

```js Express theme={null}
import express from "express";
import { webhooks } from "@shoplygg/node";

const app = express();

app.post("/webhooks/shoply", express.raw({ type: "application/json" }), async (request, response) => {
  let event;

  try {
    event = await webhooks.verify(request.body, request.headers, process.env.SHOPLY_WEBHOOK_SECRET);
  } catch {
    return response.status(401).send("Bad signature");
  }

  // Answer first, then do the work.
  response.status(200).send("ok");

  switch (event.event) {
    case "order.paid":
      // event.data is the order, as Get an order answers it
      break;
    case "subscription.cancelled":
      // event.data is the subscription
      break;
  }
});
```

```js Next.js theme={null}
import { webhooks } from "@shoplygg/node";

export async function POST(request) {
  try {
    const event = await webhooks.verify(await request.text(), request.headers, process.env.SHOPLY_WEBHOOK_SECRET);
    // ...
    return new Response("ok");
  } catch {
    return new Response("Bad signature", { status: 401 });
  }
}
```

With TypeScript, `switch (event.event)` narrows `event.data` to what that event sends. `webhooks.sign(body, secret)` makes the headers Shoply would send, to test your handler with.

## The Customer API

`ShoplyCustomer` is [the Customer API](/guides/customer-api.md): a buyer signs in with a code emailed to them, and reads their own orders, keys, subscriptions and affiliate page at your store. It holds the buyer's token and no key, so it runs in a browser or an app as well as on a server.

```js theme={null}
import { ShoplyCustomer } from "@shoplygg/node";

const buyer = new ShoplyCustomer({ storeId: "YOUR_STORE_ID" });

await buyer.login({ email });
await buyer.verify({ email, code }); // keeps the token it answers

const me = await buyer.show();
const order = await buyer.orders.show(me.orders[0].id);

await buyer.logout(); // and forgets it
```

To keep a buyer signed in, keep `buyer.token` where only your page or app can read it, and pass it back as `token` next time. A change made with a buyer's token is never sent again by itself.

## OAuth

An app that sellers connect to their own stores (see [Apps and OAuth](/guides/oauth.md)) passes the token it is handed as `apiKey`, and `oauth` has the steps that get one: `oauth.authorizeUrl()`, `oauth.state()`, `oauth.pkce()`, `oauth.exchange()` and `oauth.revoke()`.

```js theme={null}
import Shoply, { oauth } from "@shoplygg/node";

const pkce = await oauth.pkce();
const url = oauth.authorizeUrl({ clientId, redirectUri, state, codeChallenge: pkce.challenge });

// Back at your redirect URL:
const token = await oauth.exchange({ clientId, clientSecret, code, redirectUri, codeVerifier: pkce.verifier });
const shoply = new Shoply({ apiKey: token.accessToken, storeId: token.storeId });
```

A refusal from the token address is a `ShoplyError` whose `code` is OAuth's word: `invalid_grant`, `invalid_client`. A notice that a seller disconnected your app is checked with `webhooks.verify` and your app's notice secret.

## TypeScript

The types come with the package. A route's fields are typed as this reference describes them, with their words, so a missing field or a value a field does not take is caught as you write it.

What a route answers is typed from an answer that route gave, as its page's example is. A field that was text there may be null in another answer, and a route with no example answers `unknown`. Each type is named after its route: `OrdersShowResult`, `CouponsCreateParams`, `OrdersListItem`.

```ts theme={null}
import Shoply, { type OrdersShowResult } from "@shoplygg/node";
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.