# Python quickstart

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

Quickstart · Python

The standard library, 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.py and run python3 products.py: urllib is in the standard library, so there is nothing to install (Python 3.8 and later). On a python.org install on macOS, run its Install Certificates command once if urllib reports CERTIFICATE_VERIFY_FAILED.

products.py:

```python
import json
import os
import urllib.error
import urllib.request

request = urllib.request.Request(
    "https://portal.printedsimply.com/api/v1/products?limit=3",
    headers={"x-api-key": os.environ["PRINTEDSIMPLY_API_KEY"]},
)
try:
    with urllib.request.urlopen(request, timeout=30) as res:
        data = json.load(res)["data"]
except urllib.error.HTTPError as err:
    error = json.load(err)["error"]
    raise SystemExit(f"{err.code} {error['code']}: {error['message']}")

for product in data["products"]:
    print(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.py: python3 order.py.

order.py:

```python
import json
import os
import urllib.error
import urllib.request
import uuid

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" }
}

request = urllib.request.Request(
    "https://portal.printedsimply.com/api/v1/order",
    method="POST",
    data=json.dumps(order).encode(),
    headers={
        "x-api-key": os.environ["PRINTEDSIMPLY_API_KEY"],
        "Content-Type": "application/json",
        "Idempotency-Key": f"order-{uuid.uuid4().hex}",
    },
)
try:
    with urllib.request.urlopen(request, timeout=30) as res:
        data = json.load(res)["data"]
except urllib.error.HTTPError as err:
    error = json.load(err)["error"]
    raise SystemExit(f"{err.code} {error['code']}: {error['message']}")

# data["order_id"] is the order (test_ord_... on a test key); data["tracking_url"] is its tracking page.
print(data["order_id"], data["charged"])
print(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: python3 ship.py test_ord_… with the order_id order.py printed.

ship.py:

```python
import json
import os
import urllib.error
import urllib.request
import sys

order_id = sys.argv[1]  # the order_id order.py printed: test_ord_...

request = urllib.request.Request(
    f"https://portal.printedsimply.com/api/v1/order/{order_id}/simulate",
    method="POST",
    data=json.dumps({"target_state": "shipped"}).encode(),
    headers={"x-api-key": os.environ["PRINTEDSIMPLY_API_KEY"], "Content-Type": "application/json"},
)
try:
    with urllib.request.urlopen(request, timeout=30) as res:
        data = json.load(res)["data"]
except urllib.error.HTTPError as err:
    error = json.load(err)["error"]
    raise SystemExit(f"{err.code} {error['code']}: {error['message']}")

print(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.py 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.py:

```python
import json
import os
import urllib.error
import urllib.request
import sys

order_id = sys.argv[1]

request = urllib.request.Request(
    f"https://portal.printedsimply.com/api/v1/order/{order_id}",
    headers={"x-api-key": os.environ["PRINTEDSIMPLY_API_KEY"]},
)
try:
    with urllib.request.urlopen(request, timeout=30) as res:
        data = json.load(res)["data"]
except urllib.error.HTTPError as err:
    error = json.load(err)["error"]
    raise SystemExit(f"{err.code} {error['code']}: {error['message']}")

print(data["status"], data["tracking"]["number"], data["tracking"]["carrier"])
print(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.
