# TypeScript quickstart

Source: https://portal.printedsimply.com/docs/quickstarts/typescript

Quickstart · TypeScript / Node

fetch, an idempotent order, a tracking link — nothing to install.

About 5 minutes $0 sandbox key Ends with a sandbox order on its way

## Step 1: Create a sandbox key

In the dashboard, open API keys and create a key in Test mode. It starts with ps_test_ — test keys are free, never charge a card, and never reach a print facility. You can create one before adding a payment method.

[Create a test key](https://portal.printedsimply.com/dashboard/api-keys?env=test)

shell:

```bash
export PRINTEDSIMPLY_API_KEY=ps_test_YOUR_KEY
```

A live key (ps_live_) takes the same requests. A real order also needs a card on file and your own artwork: Going live, at the end of this page, has the four changes.

## Step 2: Pick a SKU

Every response is the same envelope: status, data, and request_id. Quote the request_id in any support thread and we can find the exact call. Each product's sku is what an order names. Save it as products.ts and run node products.ts: Node 22.18 and later run TypeScript as it is, and fetch is built in, so there is nothing to install. The files have no type annotations, so on Node 18 to 22.17 the same text runs as products.mjs.

products.ts:

```typescript
const apiKey = process.env.PRINTEDSIMPLY_API_KEY
if (!apiKey) throw new Error("Set PRINTEDSIMPLY_API_KEY to your test key (ps_test_…) first")

const res = await fetch("https://portal.printedsimply.com/api/v1/products?limit=3", {
  headers: { "x-api-key": apiKey },
  signal: AbortSignal.timeout(30_000),
})
const { data, error } = await res.json()
if (!res.ok) throw new Error(`${res.status} ${error.code}: ${error.message}`)
for (const product of data.products) console.log(product.sku, "-", product.name)
```

data.products:

```json
[
  {
    "sku": "sticker-die-cut-3x3",
    "name": "Die-Cut Sticker, 3×3 in",
    "category": "stickers"
  },
  {
    "sku": "wall-decal-24x36",
    "name": "Wall Decal, 24×36 in",
    "category": "decals"
  },
  {
    "sku": "magnet-3x3",
    "name": "Magnet, 3×3 in",
    "category": "magnets"
  }
]
```

Rows carry more than these three fields, the price of one unit among them; POST /v1/orders/quote prices a whole order. The order below names the first row's sticker as sticker-die-cut-3x3: a SKU is matched whatever its case.

## Step 3: Place the order

The order's reference doubles as its Idempotency-Key: send the same request again and you get this order back, never a second one; the same key with a different body is refused with 422. A new order needs a new reference as well as a new key: one reference is one order for 30 days, so the same reference under another key is refused with 409 duplicate_reference, naming the order that has it. Run it as you ran products.ts: node order.ts.

order.ts:

```typescript
const apiKey = process.env.PRINTEDSIMPLY_API_KEY
if (!apiKey) throw new Error("Set PRINTEDSIMPLY_API_KEY to your test key (ps_test_…) first")

const order = {
  "items": [
    { "sku": "sticker-die-cut-3x3", "quantity": 100,
      "artwork_url": "https://printedsimply.com/samples/sticker-3x3.png" }
  ],
  "address_to": {
    "name": "Jane Doe", "address1": "123 Main St",
    "city": "Austin", "state": "TX", "zip": "78701", "country": "US"
  },
  "shipping": { "method": "standard" }
}

const res = await fetch("https://portal.printedsimply.com/api/v1/order", {
  method: "POST",
  headers: {
    "x-api-key": apiKey,
    "Content-Type": "application/json",
    "Idempotency-Key": `order-${Date.now()}`,
  },
  body: JSON.stringify(order),
  signal: AbortSignal.timeout(30_000),
})
const { data, error } = await res.json()
if (!res.ok) throw new Error(`${res.status} ${error.code}: ${error.message}`)
// data.order_id is the order (test_ord_… on a test key); data.tracking_url is its tracking page.
console.log(data.order_id, data.charged)
console.log(data.tracking_url)
```

response:

```json
{
  "status": "success",
  "data": {
    "order_id": "test_ord_wVK9ZiG-VYiPrlSeBUBmrUVk",
    "portal_order_id": "ord_wVK9ZiG-VYiPrlSeBUBmrUVk",
    "tracking_url": "https://printedsimply.com/track/eyJ2IjoxLCJ0IjoidHJhY2tpbmciLCJ0b2siOiJ0cmtfQXF1SkZxRExEa2lzSzZKaC1IQUJIejlMIn0.199c0c58721512fcccd784902c422ca4e827c6d09b7e495bbb4db9b06ae62003",
    "test_mode": true,
    "lifecycle": "simulated",
    "charged": "$0.00",
    "artwork_check": {
      "lines": [
        {
          "item_index": 0,
          "sku": "sticker-die-cut-3x3",
          "status": "passed",
          "width_px": 1200,
          "height_px": 1200,
          "print_size_in": [
            3,
            3
          ],
          "effective_dpi": 400,
          "min_dpi": 300,
          "findings": []
        }
      ],
      "would_refuse": false
    }
  },
  "request_id": "4e3753e1-48dd-4313-bfd8-48e4af4e116a"
}
```

## Step 4: Skip the wait

Move the sandbox order straight to shipped instead of waiting for the schedule: node ship.ts test_ord_… with the order_id order.ts printed.

ship.ts:

```typescript
const apiKey = process.env.PRINTEDSIMPLY_API_KEY
if (!apiKey) throw new Error("Set PRINTEDSIMPLY_API_KEY to your test key (ps_test_…) first")
const orderId = process.argv[2] // the order_id order.ts printed: test_ord_…

const res = await fetch(`https://portal.printedsimply.com/api/v1/order/${orderId}/simulate`, {
  method: "POST",
  headers: { "x-api-key": apiKey, "Content-Type": "application/json" },
  body: JSON.stringify({ target_state: "shipped" }),
  signal: AbortSignal.timeout(30_000),
})
const { data, error } = await res.json()
if (!res.ok) throw new Error(`${res.status} ${error.code}: ${error.message}`)
console.log(data.order_id, data.status)
```

response:

```json
{
  "status": "success",
  "data": {
    "order_id": "test_ord_wVK9ZiG-VYiPrlSeBUBmrUVk",
    "portal_order_id": "ord_wVK9ZiG-VYiPrlSeBUBmrUVk",
    "status": "shipped",
    "simulated": true
  },
  "request_id": "4e3753e1-48dd-4313-bfd8-48e4af4e116a"
}
```

Left alone, a sandbox order moves on by itself: a scheduled job takes it to in_production, shipped and delivered over about five minutes, firing the same signed webhooks at each step. simulate does not wait for that schedule. It takes in_production, shipped, delivered, cancelled or issue_flagged, and only on a test key. An order the schedule has already delivered answers 400 already_terminal; to rehearse the step again, place a new order with a new reference and Idempotency-Key (the sample uses one value for both; the same key replays the delivered order, and the same reference under a new key is refused 409 duplicate_reference).

## Step 5: Open the tracking link and watch it deliver

order.ts printed the tracking link. Open it, then read the order: once it has shipped, tracking carries the sandbox tracking number (TRK-TEST-…) and its carrier. Simulate delivered, or wait, to watch it deliver.

track.ts:

```typescript
const apiKey = process.env.PRINTEDSIMPLY_API_KEY
if (!apiKey) throw new Error("Set PRINTEDSIMPLY_API_KEY to your test key (ps_test_…) first")
const orderId = process.argv[2]

const res = await fetch(`https://portal.printedsimply.com/api/v1/order/${orderId}`, {
  headers: { "x-api-key": apiKey },
  signal: AbortSignal.timeout(30_000),
})
const { data, error } = await res.json()
if (!res.ok) throw new Error(`${res.status} ${error.code}: ${error.message}`)
console.log(data.status, data.tracking.number, data.tracking.carrier)
console.log(data.tracking.page_url)
```

Sandbox orders run the real lifecycle on a compressed clock: created → in_production → shipped → delivered, firing genuinely HMAC-signed webhooks at every transition through the same delivery pipeline live orders use. Leave the tracking page open.

## Going live

The sandbox order above never charges, prints or ships. A real one takes four changes.

### Add a card in Billing

A live order is charged to the card on file when it is placed. Without one, POST /v1/order answers 402 no_payment_method and nothing is placed.

[Open Billing](https://portal.printedsimply.com/dashboard/billing)

### Create a live key

On the API keys page, create a key in Live mode. It starts with ps_live_ and is shown in full once, so store it where your server reads secrets, never in a browser.

[Create a live key](https://portal.printedsimply.com/dashboard/api-keys?env=live)

### Send the same request with your own artwork

Swap in the live key, your customer's address and your artwork: an https link to the file (PNG, PDF or SVG), or the id POST /v1/assets returns, as print_file. A live order's artwork is checked before anything is charged: a line with no artwork answers 422 artwork_required, and a file that cannot be printed answers 422 artwork_not_printable.

### Subscribe a live-mode webhook

A subscription in test mode receives sandbox events only. For real orders, subscribe your endpoint in live mode (or all) on the Webhooks page or with POST /v1/webhooks, and verify X-PS-Signature-V2 on every delivery.

[Open Webhooks](https://portal.printedsimply.com/dashboard/webhooks)

## Scopes this page needs

A key created without an explicit scope list gets all of them. If you scope the key down, grant at least these:

- `products:read`
- `orders:write`
- `orders:read`

## Order placed. What's next?

- [Subscribe to webhooks](https://portal.printedsimply.com/docs/webhooks)

  Get order.shipped pushed to you instead of polling — signed, replayable, deduped.

- [Understand the limits](https://portal.printedsimply.com/docs/rate-limits)

  Per-plan rate and daily limits, the headers we return, and how to back off honestly.

- [Browse the API reference](https://portal.printedsimply.com/docs/api-reference)

  Every v1 endpoint, with its required scopes and response shape.

- [Try another quickstart](https://portal.printedsimply.com/docs/quickstarts)

  The same order in curl, TypeScript, Python or PHP, or from your framework.
