npm package: @sellersmith/merchize-sdk
Description: TypeScript SDK for Merchize fulfillment API
Author: SellerSmith
Latest version: 0.1.0

# @sellersmith/merchize-sdk

TypeScript SDK for the [Merchize](https://merchize.com) print-on-demand fulfillment API.

> [!WARNING]
> **Monthly Token Renewal Required**
>
> Merchize access tokens expire every month. Your integration **must** handle token rotation
> or all API calls will fail with 401 errors. See [Token Rotation](#token-rotation) below.

---

## Installation

```bash
npm install @sellersmith/merchize-sdk
```

## Quick Start

```typescript
import { Merchize } from "@sellersmith/merchize-sdk";

const client = new Merchize({
  baseUrl: "https://bo-group-1-2.merchize.com/{store_id}/bo-api",
  accessToken: "your-monthly-access-token",
});

// Import an order for fulfillment
const result = await client.orders.import({
  order_id: "shopify-1001",
  shipping_info: {
    full_name: "Jane Doe",
    address_1: "123 Main St",
    city: "Austin",
    state: "TX",
    postcode: "78701",
    country: "US",
    email: "jane@example.com",
    phone: "+15125550100",
  },
  items: [
    {
      name: "Classic Tee",
      product_id: 42,
      sku: "TEE-WHT-M",
      merchize_sku: "MCZ-TEE-001",
      quantity: 1,
      price: 15.99,
      currency: "USD",
      image: "https://cdn.example.com/tee.png",
      design_front: "https://cdn.example.com/design-front.png",
      attributes: [
        { name: "Color", option: "White" },
        { name: "Size", option: "M" },
      ],
    },
  ],
});

console.log(result.data.id);
```

> All request/response fields use **snake_case**.

## Configuration

```typescript
const client = new Merchize({
  baseUrl: "https://bo-group-1-2.merchize.com/{store_id}/bo-api",
  accessToken: "your-access-token",
  timeout: 30000,
  enableLogging: false,
  onTokenExpired: () => {
    console.error("Merchize token expired — renewal required");
  },
});
```

| Option | Type | Required | Default | Description |
|---|---|---|---|---|
| `baseUrl` | `string` | Yes | — | API base URL from Integrations > API in your dashboard |
| `accessToken` | `string` | Yes | — | Monthly access token from your dashboard |
| `timeout` | `number` | No | `30000` | Request timeout in milliseconds |
| `onTokenExpired` | `() => void` | No | — | Callback fired automatically on 401 responses |
| `enableLogging` | `boolean` | No | `false` | Log requests and responses to console |

## Token Rotation

Tokens have a **monthly expiry**. When expired, all calls return 401.

```typescript
import { Merchize } from "@sellersmith/merchize-sdk";

const client = new Merchize({
  baseUrl: process.env.MERCHIZE_BASE_URL!,
  accessToken: await loadTokenFromDb(),
  onTokenExpired: async () => {
    // Alert your team or trigger an automated renewal flow
    console.error("Merchize token expired — renew at dashboard: Integrations > API");
    // After obtaining the new token, hot-swap without reinstantiating:
    // const newToken = await renewToken();
    // client.setAccessToken(newToken);
  },
});

// Update the token at any time without creating a new client
client.setAccessToken("new-monthly-token");
```

Store your token in a secrets manager (AWS Secrets Manager, Vault, etc.) and rotate it on a schedule before expiry.

---

## API Reference

### Orders

All 13 methods are on `client.orders`.

#### `orders.import(order)`

Import an order for fulfillment.

```typescript
import(order: CreateOrderRequest): Promise<MerchizeResponse<ImportOrderResponse>>
```

```typescript
const res = await client.orders.import({
  order_id: "my-order-123",      // your external order ID
  identifier: "shopify",         // optional — adds tag "api_shopify"
  shipping_info: {
    full_name: "John Smith",
    address_1: "456 Oak Ave",
    address_2: "Apt 2B",         // optional
    city: "New York",
    state: "NY",
    postcode: "10001",
    country: "US",               // ISO 3166-1 alpha-2
    email: "john@example.com",
    phone: "+12125551234",
  },
  items: [
    {
      name: "Custom Hoodie",
      product_id: 7,
      sku: "HOODIE-BLK-L",
      merchize_sku: "MCZ-HOOD-007",
      quantity: 2,
      price: 29.99,
      currency: "USD",
      image: "https://cdn.example.com/hoodie.png",
      design_front: "https://cdn.example.com/front.png",
      design_back: "https://cdn.example.com/back.png",  // optional
      attributes: [
        { name: "Color", option: "Black" },
        { name: "Size", option: "L" },
      ],
    },
  ],
  tags: ["vip", "rush"],         // optional
});
```

#### `orders.getDetail(params)`

Get order detail by Merchize code, external number, or identifier.

```typescript
getDetail(params: OrderLookupParams): Promise<MerchizeResponse<OrderDetail>>
// params: { code?, external_number?, identifier? }
```

```typescript
const order = await client.orders.getDetail({ code: "RX-0001-ABCD" });
const order = await client.orders.getDetail({ external_number: "shopify-1001" });
const order = await client.orders.getDetail({ external_number: "1001", identifier: "shopify" });
```

#### `orders.getInvoice(params)`

Get invoice/cost breakdown for an order.

```typescript
getInvoice(params: OrderLookupParams): Promise<MerchizeResponse<OrderInvoice>>
```

```typescript
const invoice = await client.orders.getInvoice({ code: "RX-0001-ABCD" });
console.log(invoice.data.fulfillment_cost.total);
```

#### `orders.getProgress(params)`

Get fulfillment progress for an order.

```typescript
getProgress(params: OrderLookupParams): Promise<MerchizeResponse<OrderDetail>>
```

```typescript
const progress = await client.orders.getProgress({ code: "RX-0001-ABCD" });
```

#### `orders.getTracking(params)`

Get shipment tracking info for an order.

```typescript
getTracking(params: OrderLookupParams): Promise<MerchizeResponse<OrderTracking>>
```

```typescript
const tracking = await client.orders.getTracking({ code: "RX-0001-ABCD" });
console.log(tracking.data.status, tracking.data.service);
```

#### `orders.updateStatus(request)`

Pause (`hold`) or resume an order.

```typescript
updateStatus(request: OrderLinkRequest): Promise<MerchizeResponse<boolean>>
// OrderLinkRequest: { order: { code, external_number, identifier, action: "hold" | "resume" } }
```

```typescript
// Pause an order
await client.orders.updateStatus({
  order: { code: "RX-0001-ABCD", external_number: "shopify-1001", identifier: "shopify", action: "hold" },
});

// Resume a paused order
await client.orders.updateStatus({
  order: { code: "RX-0001-ABCD", external_number: "shopify-1001", identifier: "shopify", action: "resume" },
});
```

#### `orders.push(request)`

Push an order to fulfillment.

```typescript
push(request: OrderLinkRequest): Promise<MerchizeResponse<boolean>>
```

```typescript
await client.orders.push({
  order: { code: "RX-0001-ABCD", external_number: "shopify-1001", identifier: "shopify" },
});
```

#### `orders.cancel(request)`

Cancel an order.

```typescript
cancel(request: OrderLinkRequest): Promise<MerchizeResponse<boolean>>
```

```typescript
await client.orders.cancel({
  order: { code: "RX-0001-ABCD", external_number: "shopify-1001", identifier: "shopify" },
});
```

#### Batch methods

All batch methods accept an `OrderListRequest` — an object with an `order` array.

```typescript
// Common type for all batch methods
interface OrderListRequest {
  order: Array<{ code: string; external_number: string; identifier: string }>;
}

const req = {
  order: [
    { code: "RX-0001-ABCD", external_number: "1001", identifier: "shopify" },
    { code: "RX-0002-EFGH", external_number: "1002", identifier: "shopify" },
  ],
};
```

| Method | Return type | Description |
|---|---|---|
| `orders.listDetails(request)` | `MerchizeResponse<OrderDetail[]>` | Get details for multiple orders |
| `orders.listInvoices(request)` | `MerchizeResponse<OrderInvoice[]>` | Get invoices for multiple orders |
| `orders.listProgress(request)` | `MerchizeResponse<OrderProgressDetail[]>` | Get progress for multiple orders |
| `orders.listTracking(request)` | `MerchizeResponse<OrderTracking[]>` | Get tracking for multiple orders |
| `orders.listTickets(request)` | `MerchizeResponse<OrderTicket[]>` | Get tickets for multiple orders |

```typescript
const details = await client.orders.listDetails(req);
const invoices = await client.orders.listInvoices(req);
const progresses = await client.orders.listProgress(req);
const trackings = await client.orders.listTracking(req);
const tickets = await client.orders.listTickets(req);
```

---

### Products

#### `products.getCatalog(params?)`

Get the product catalog. Maximum 50 products per page.

```typescript
getCatalog(params?: { page?: number; limit?: number }): Promise<MerchizeResponse<ProductCatalogPage>>
// ProductCatalogPage: { products: Product[]; total: number; limit: number; page: number }
```

```typescript
const page1 = await client.products.getCatalog({ page: 1, limit: 50 });
page1.data.products.forEach((p) => console.log(p._id, p.title));
```

#### `products.searchCollections(params?)`

Search product collections by name.

```typescript
searchCollections(params?: { limit?: number; name?: string; page?: number }): Promise<MerchizeResponse<Paging<Collection[]>>>
```

```typescript
const results = await client.products.searchCollections({ name: "t-shirt", page: 1, limit: 20 });
results.data.data.forEach((c) => console.log(c._id, c.name));
```

#### `products.getAllVariants(productId)`

Get all variants for a specific product.

```typescript
getAllVariants(productId: string): Promise<MerchizeResponse<Variant[]>>
```

```typescript
const variants = await client.products.getAllVariants("product-id-here");
variants.data.forEach((v) => {
  console.log(v.sku, v.title);
  v.options.forEach((o) => console.log(o.attribute.name, o.value));
});
```

---

### Tickets

#### `tickets.create(request)`

Open a support ticket for one or more orders.

```typescript
create(request: CreateTicketRequest): Promise<MerchizeResponse<Ticket>>
```

```typescript
const ticket = await client.tickets.create({
  order_codes: ["RX-0001-ABCD"],      // Merchize order codes
  category: "Wrong item",
  subcategory: "Wrong size",
  product_type: ["T-Shirt"],          // optional
  preferred: "Reprint",               // optional preferred resolution
  description: "Customer received size M instead of L.",  // optional
  images: [                           // optional
    { original_name: "photo.jpg", image_url: "https://cdn.example.com/photo.jpg" },
  ],
});

console.log(ticket.data._id);
```

#### `tickets.update(ticketId, request)`

Add a comment/message to an existing ticket.

```typescript
update(ticketId: number, request: { message: string; images?: TicketImage[] }): Promise<MerchizeResponse<UpdateTicket>>
```

```typescript
await client.tickets.update(12345, {
  message: "Replacement has been dispatched.",
  images: [
    { original_name: "label.png", image_url: "https://cdn.example.com/label.png" },
  ],
});
```

#### `tickets.reopen(ticketId, request)`

Reopen a resolved ticket.

```typescript
reopen(ticketId: number, request: { message: string; is_escalate: boolean; cf_satisfied?: string }): Promise<MerchizeResponse<UpdateTicket>>
```

```typescript
await client.tickets.reopen(12345, {
  message: "Issue persists — replacement also arrived damaged.",
  is_escalate: true,
});
```

#### `tickets.resolve(ticketId)`

Mark a ticket as resolved.

```typescript
resolve(ticketId: number): Promise<MerchizeResponse<UpdateTicket>>
```

```typescript
await client.tickets.resolve(12345);
```

---

## Error Handling

All failed API calls throw `MerchizeError`.

```typescript
import { Merchize, MerchizeError } from "@sellersmith/merchize-sdk";

try {
  const order = await client.orders.getDetail({ code: "RX-NONEXISTENT" });
} catch (err) {
  if (err instanceof MerchizeError) {
    console.error(err.status);       // HTTP status code, e.g. 404
    console.error(err.message);      // human-readable message
    console.error(err.responseBody); // raw response body string
  } else {
    // Network error, timeout, etc.
    throw err;
  }
}
```

| Property | Type | Description |
|---|---|---|
| `status` | `number` | HTTP status code |
| `message` | `string` | Error message |
| `responseBody` | `string` | Raw response body for debugging |

---

## Base URL

Get your `baseUrl` from the **Merchize Dashboard > Integrations > API**.

The URL follows the format:

```
https://bo-group-X-Y.merchize.com/{store_id}/bo-api
```

Tokens are store-scoped. Each store has its own base URL and access token.

---

## Requirements

- Node.js 18+ (uses native `fetch`)
- Zero runtime dependencies

## License

MIT — see [LICENSE](./LICENSE)