This page needs JavaScript. The same reference as text, for AI agents and tools: /api/documentation.md (Markdown; one endpoint: /api/documentation.md?method=GET&path=/seller-api/catalog), /api/documentation.openapi (OpenAPI) and /llms.txt (index with the environments and the MCP server).
# SenPrints Seller API
The API is a service for creating and managing orders, including product listings, order processing, and more.
Requests use standard HTTP methods: GET, POST, PUT, DELETE. Responses are in JSON format.
Authentication: send your API key as a Bearer token. Get it in the seller dashboard of the environment you call (table below) -> Settings -> General -> Get API Key. Only the account owner, signed in directly, can see, create or replace the key (not a team member). A key is valid for one year; replacing it there stops the old one at once.
## Environments
| Setting | Production | Development |
| --- | --- | --- |
| API base URL | `https://api-sg.senprints.net` | `https://apis.dev.senprints.net` |
| API key | [seller.senprints.com](https://seller.senprints.com/settings/general) -> Settings -> General | [seller.senprints.xyz](https://seller.senprints.xyz/settings/general) -> Settings -> General |
| MCP server | `https://api.senprints.com/mcp/seller` | `https://apis.dev.senprints.net/mcp/seller` |
| This reference | [api-sg.senprints.net/api/documentation](https://api-sg.senprints.net/api/documentation) | [apis.dev.senprints.net/api/documentation](https://apis.dev.senprints.net/api/documentation) |
The two environments share nothing: accounts, API keys, orders, balances and webhooks of one do not exist in the other, so a production key answers HTTP 401 on development and the other way round. Paths and bodies are the same; only the base URL and the key change. Pick the server in the selector below to send the "Test request" calls of this page to it.
## Quickstart: your first order
1. Send your API key on every call: `Authorization: Bearer <your API key>`, to the base URL of the environment the key comes from.
2. Pick a product: `GET /seller-api/catalog` lists the products you can order (their `id` is the `template_id` of an order line), with their options and print areas; `GET /seller-api/catalog/info?id={template_id}` gives the variants, costs and shipping.
3. Check the order without creating it: `POST /seller-api/orders/validate` with the order body. It answers HTTP 400 with the errors, or `valid: true`.
4. Create it: `POST /seller-api/orders` with the same body. Use your own `order_number`: it is unique per account, so a retry cannot create the order twice.
5. Follow it: set a webhook once (`PUT /seller-api/webhook` with `{"url": "https://..."}`) to receive `order.validated`, `order.paid`, `order.tracking_updated` and the other events, or read `GET /seller-api/orders/{id}`.
```bash
# Development: https://apis.dev.senprints.net, production: https://api-sg.senprints.net
export SENPRINTS_API_URL=https://apis.dev.senprints.net
export SENPRINTS_API_KEY=<the key from the same environment>
curl -X POST "$SENPRINTS_API_URL/seller-api/orders/validate" \
-H "Authorization: Bearer $SENPRINTS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"order_number": "1001",
"customer_name": "Jane Doe",
"address": "1 Main St",
"city": "Austin",
"state": "TX",
"postcode": "78701",
"country": "US",
"products": [{
"template_id": 101,
"quantity": 1,
"options": {"color": "white", "size": "m"},
"designs": [{"file_url": "https://example.com/design.png", "print_space": "front"}]
}]
}'
```
A valid order is charged from your balance and sent to production one hour after its last update when auto fulfill is on; until then you can change it (`PUT /seller-api/orders/{id}`), hold it or cancel it.
## AI assistants and agents
- **No code, with MCP:** add `https://api.senprints.com/mcp/seller` (development: `https://apis.dev.senprints.net/mcp/seller`) as a custom connector in Claude, ChatGPT or any MCP client, then sign in with your SenPrints seller account of that environment. The assistant can search your orders, look up the fulfillment catalog, upload artwork, create fulfillment orders and read your sales and balance; creating an order or changing campaigns asks you to confirm first.
- **Docs for agents:** this whole reference as Markdown at [/api/documentation.md](/api/documentation.md) (one endpoint: `?method=PUT&path=/seller-api/webhook`), the OpenAPI spec at [/api/documentation.openapi](/api/documentation.openapi), and an index at [/llms.txt](/llms.txt). "Copy for AI" at the top of this page copies them or opens them in ChatGPT or Claude.
- **Try an order without paying:** `POST /seller-api/orders/validate` runs every check of `POST /seller-api/orders` and creates nothing.
## Tracking and order updates
Set a webhook with `PUT /seller-api/webhook` (API key only, no dashboard step; the same webhook shows in the seller dashboard -> Settings -> General) and SenPrints posts the steps of your orders you pick (all by default) to your URL: `order.validated` / `order.invalid` after the checks, `order.paid`, `order.tracking_updated` for each tracking update, `order.completed`, and `order.on_hold` / `order.cancelled`. The Webhooks section describes the payload, the signature and the retries.
Without a webhook, read the order again to follow it:
- Tracking is per product line, in `products[].tracking_code`, `tracking_url` and `shipping_carrier` of `GET /seller-api/orders/{id}`. A line that has tracking moves to `fulfill_status` `on_delivery`.
- When every line has shipped or been cancelled, the order moves to `status` `completed` and `fulfill_status` `on_delivery`: you can stop reading it.
- To find the orders that started shipping, list them by the time of their first tracking: `GET /seller-api/orders?column_date=delivered_at&date_range=last_24_hours`. Lines can ship separately, so keep reading `GET /seller-api/orders/{id}` until the order's `fulfill_status` is `on_delivery`.
- For an FBA order with a shipping label, a line can stay `processing` until the carrier scans the parcel.
The webhooks in the seller dashboard are different and do not cover these orders: Settings -> Notification -> Webhook only announces new storefront sales, and a store's WooCommerce webhook only covers orders created by the SenPrints WooCommerce plugin.
## Errors and limits
Every response is JSON with `success`. Read it, not only the HTTP status: a failed check on an order body answers HTTP 200 with `success: false`.
| HTTP | Body | Meaning |
| --- | --- | --- |
| 200 | `success: false`, `message: {"field": ["..."]}` | A request rule failed on `POST`/`PUT /seller-api/orders` (missing or malformed field, `order_number` already used). Nothing was changed. |
| 400 | `success: false`, `message: "..."` | A business rule failed (address, product, print area), or a required field is missing. `POST /seller-api/orders/validate` answers every failed check this way, with the errors in `data.errors`. |
| 401 | `error: "api_key_invalid"` or `"api_key_expired"` | The key is unknown, comes from the other environment, is not a seller key, or was replaced or expired. Get the current one in the seller dashboard of that environment -> Settings -> General. |
| 404 | `success: false`, `message: "Order not found"` or `"Webhook not set"` | Not one of your fulfillment or FBA orders, or no webhook set yet. |
| 406 | `error: "api_key_absent"` | No `Authorization: Bearer` header. |
| 409 | `success: false` | An order with the same `order_number` is being created by another call. Look it up with `GET /seller-api/orders?s={order_number}`. |
| 429 | `success: false` | Over the limit: 300 requests a minute per API key, all endpoints together. Wait the `Retry-After` seconds; `X-RateLimit-Remaining` tells how many calls are left. |
| 500 | `success: false`, `message: "Server internal error"` | Our error. Sending the same `POST /seller-api/orders` again is safe: if the order was created after all, the retry answers that the `order_number` is already taken. |
**Retries:** sending `POST /seller-api/orders` again with the same `order_number` never creates a second order, so retry a timeout freely. Webhook deliveries are at least once; deduplicate them on their `id`, and see each body sent and your URL's answer with `GET /seller-api/webhook/deliveries`.
## Changelog
- **2026-10-21**: `GET /seller-api/webhook/deliveries`, the log of webhook attempts with the body sent and your URL's answer. Rate limit of 300 requests a minute per API key (HTTP 429). A second `POST /seller-api/orders` with an `order_number` still being created answers 409. `order.cancelled` is also sent when an order is cancelled from WooCommerce or refunded on Shopify. `GET /seller-api/me` no longer returns `klaviyo_private_key`, `crisp_signature` and `crisp_token_id`. The webhook signing secret is optional and off by default (`rotate_secret`, `remove_secret`). The Seller API takes a seller API key only; other key types answer 401 `api_key_invalid`. The API key can be replaced from the dashboard. This reference as Markdown, `/llms.txt`, and the seller MCP server.
- **2026-10-20**: Webhooks: `GET`/`PUT`/`DELETE /seller-api/webhook`, `POST /seller-api/webhook/test`, and the events `order.validated`, `order.invalid`, `order.paid`, `order.tracking_updated`, `order.completed`, `order.on_hold`, `order.cancelled`. This reference replaces the Swagger page.
- **2026-10-12**: `POST /seller-api/orders/validate`, a dry run of `POST /seller-api/orders`. A missing `customer_name`, `address` or `country` answers HTTP 400 naming the fields, instead of HTTP 500.
- **2026-09-04**: An order line without `designs` or `mockups`, or with `options` sent as a list, no longer answers HTTP 500.
## Base URLs
- `https://api-sg.senprints.net` (Production)
- `https://apis.dev.senprints.net` (Development)
## Authentication
Send `Authorization: Bearer <API key>` with every request.
Get an API key in the seller dashboard -> Settings -> General -> Get API Key: [seller.senprints.com](https://seller.senprints.com/settings/general) for production, [seller.senprints.xyz](https://seller.senprints.xyz/settings/general) for development; a key works on its own environment only (account owner only, valid for one year). A missing key answers HTTP 406 (`api_key_absent`), an invalid or expired key HTTP 401 (`api_key_invalid`, `api_key_expired`).
## Orders
### Get orders
`GET /seller-api/orders`
Retrieve the list of fulfillment orders. Each item is the order object described in Get order details.
#### Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `s` | query | string | no | Search keyword, support search by order id, order number, customer name, customer email. |
| `status` | query | string | no | The status of the order. One of: `draft`, `pending`, `processing`, `completed`, `on_hold`, `cancelled`. |
| `date_range` | query | string | no | Preset date range applied to column_date. Defaults to custom. One of: `lifetime`, `custom`, `today`, `yesterday`, `last_hour`, `last_24_hours`, `last_3_days`, `last_5_days`, `last_7_days`, `last_14_days`, `last_30_days`, `last_quarter`, `last_6_months`, `this_week`, `last_week`, `this_month`, `this_quarter`, `last_month`, `this_year`, `last_year`. |
| `start_date` | query | string | no | The start date of the custom range (YYYY-MM-DD). With date_range custom and no start_date, the last month is returned. |
| `end_date` | query | string | no | The end date of the custom range (YYYY-MM-DD), at most 365 days after start_date. |
| `column_date` | query | string | no | The date column the range applies to. Defaults to created_at. One of: `created_at`, `updated_at`, `paid_at`, `delivered_at`, `fulfilled_at`. |
| `page` | query | integer | no | The page number. |
| `per_page` | query | integer | no | The number of items per page, 1 to 500. Defaults to 15. |
| `sort` | query | string | no | The column to sort by. Defaults to column_date, newest first. One of: `id`, `total_amount`, `updated_at`, `created_at`. |
| `direction` | query | string | no | The direction to sort. One of: `asc`, `desc`. |
#### Responses
- `200`
```json
{
"success": true,
"message": null,
"data": [
{
"id": 1,
"order_number": "ORD-123456",
"store_name": "ABC Store",
"store_domain": "abc.com",
"customer_name": "Nguyen Van A",
"customer_email": "email@example.com",
"customer_phone": "0123456789",
"address": "123 ABC Street",
"address_2": "Apartment 101",
"house_number": "12B",
"mailbox_number": "456",
"city": "Hanoi",
"state": "Ha Dong",
"postcode": "100000",
"country": "VN",
"order_note": "Deliver during business hours",
"shipping_label": "http://example.com/label.pdf",
"shipping_method": "standard",
"cross_shipping": true,
"personalized": false,
"status": "pending",
"fulfill_status": "unfulfilled",
"address_verified": true,
"fulfill_log": "Order sent to production",
"type": "standard",
"created_at": "2025-02-28T17:01:29+07:00",
"updated_at": "2025-02-28T17:01:29+07:00",
"paid_at": "2025-03-01T12:00:00+07:00",
"delivered_at": "2025-03-05T09:00:00+07:00",
"received_at": "2025-03-06T14:00:00+07:00",
"total_quantity": 3,
"total_product_amount": 59.99,
"total_shipping_amount": 5.99,
"total_amount": 65.98,
"total_paid": 65.98,
"total_product_cost": 30,
"total_shipping_cost": 3,
"total_seller_profit": 32.98,
"processing_fee": 2,
"total_fulfill_fee": 4,
"processing_fee_paid": true,
"fulfill_fee_paid": true,
"products": [
{
"id": 1,
"product_name": "T-shirt",
"sku": "SKU12345",
"quantity": 2,
"price": 19.99,
"total_amount": 39.98,
"seller_profit": 10,
"fulfill_status": "fulfilled",
"full_printed": true,
"barcode": "ABC123456789",
"fulfill_fba_by": "Amazon",
"design_by": "seller",
"campaign_title": "Valentine Campaign",
"tracking_code": "TRACK123456",
"tracking_url": "http://tracking.com/TRACK123456",
"shipping_carrier": "DHL",
"cross_shipping": false,
"color": "Red",
"options": {
"color": "white",
"size": "30x20in"
},
"custom_options": [
{
"type": "text",
"label": "Name",
"value": "Anna"
}
],
"thumb_url": "http://example.com/thumb.jpg",
"mockups": [
{
"id": 1,
"file_url": "http://example.com/mockup.jpg",
"print_space": "front"
}
],
"designs": [
{
"id": 1,
"file_url": "http://example.com/design.jpg",
"print_space": "front"
}
]
}
]
}
]
}
```
- `400` Invalid query parameter
```json
{
"success": false,
"message": "The selected status is invalid.",
"data": null
}
```
- `401` Invalid or expired API key
```json
{
"success": false,
"error": "api_key_invalid"
}
```
- `404` No order matches
```json
{
"success": false,
"message": "No orders found",
"data": null
}
```
### Create a new order
`POST /seller-api/orders`
Creates a fulfillment order. The call is accepted before the order is fully checked: address verification,
downloading the artwork files, shipping cost and supplier assignment run afterwards in a queue. Read the order
again with GET /seller-api/orders/{id} until status is pending (the order is valid) or fulfill_status is invalid
(fulfill_log gives the reason), or set a webhook (PUT /seller-api/webhook) to receive order.validated or
order.invalid instead.
> **Warning:** a valid pending order is charged and sent to production automatically one hour after its
> last update when the account has auto fulfill enabled and enough balance. To avoid the charge, hold or cancel
> the order before then (PUT /seller-api/orders/{id}/hold or /cancel).
Errors: a failed request rule (missing or malformed field) answers HTTP 200 with success=false and message as an
object keyed by field, so check the success field and not only the HTTP status. A failed business rule (address,
product, print space) answers HTTP 400 with a text message, and so does a missing customer_name, address or
country (message: Missing required fields: ...). Nothing is created in either case.
Retries are safe: order_number is unique per account, so sending the same order again after a timeout creates
nothing and answers success=false with "order_number": ["The order number has already been taken."]. A call sent
while the first one with that number is still running answers HTTP 409. Find the order already created with
GET /seller-api/orders?s={order_number}.
#### Request body (JSON)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `order_number` | string | yes | Your order number, unique per seller. |
| `store_name` | string | no | Store name. |
| `store_domain` | string | no | Store domain. |
| `customer_name` | string | yes | Required, also for FBA orders (HTTP 400 when the key is missing). |
| `customer_email` | string | no | Customer email. |
| `customer_phone` | string | no | Customer phone number. |
| `address` | string | yes | Required, also for FBA orders (HTTP 400 when the key is missing). city and postcode are required unless shipping_label is set. |
| `address_2` | string | no | Additional address. |
| `house_number` | string | no | House number. |
| `mailbox_number` | string | no | Mailbox number. |
| `city` | string | yes | Required unless shipping_label is set (FBA order). |
| `state` | string | no | Required except for FBA orders and GB/AU/PR/DE/SE/NO/FR/GR/PT/RO/PL. |
| `postcode` | string | yes | Required unless shipping_label is set (FBA order). |
| `country` | string | yes | Required, also for FBA orders (HTTP 400 when the key is missing). Two-letter country code, for example US, CA, GB, AU, DE, FR, IT, ES, JP. |
| `shipping_method` | string | no | Shipping method. One of: `standard`, `express`. |
| `order_note` | string | no | Order note. |
| `shipping_label` | string | no | Shipping label URL (required if the order is platform). Setting it makes the order an FBA order. |
| `products` | array of object | yes | The products of the order. |
| `products[].campaign_title` | string | no | Campaign name. |
| `products[].quantity` | integer | yes | Product quantity. For an FBA order fulfilled by amazon it must be a multiple of 30, from 30 to 300. |
| `products[].template_id` | integer | yes | Id of a product returned by GET /seller-api/catalog (the same id as in the product page of the catalog). An inactive or unknown id is rejected with 'Product ID N not found'. |
| `products[].options` | object | yes | Product options as key-value pairs. |
| `products[].custom_options` | array of object | no | Custom options for the product (e.g., text, image), each with type, label and value. |
| `products[].barcode` | string | no | Barcode (required only when fulfill_fba_by is amazon). |
| `products[].fulfill_fba_by` | string | no | Fulfillment type (required only when order is platform). One of: `ebay`, `etsy`, `poshmark`, `tiktok`, `amazon`. |
| `products[].design_by_sen` | boolean | no | If true, the design will be process by SenPrints. |
| `products[].designs` | array of object | no | Artwork files, one per print area. Send them: an order without artwork cannot be produced and may be flagged invalid (fulfill_log: No artworks found). Each file_url must be a public URL ending in jpg, jpeg, png, gif or webp, or a URL on a trusted host (Google Drive, Dropbox, SenPrints CDNs). PSD, PDF, DST and EMB files are rejected, and the file is downloaded and checked after the order is created. |
| `products[].designs[].file_url` | string | yes | Artwork file URL. |
| `products[].designs[].print_space` | string | yes | Print area of the product (names are in GET /seller-api/catalog/info), or default for the first print area. It must be one of the product's own print areas, otherwise the order is rejected with 'Artwork print space X invalid'. For all-over-print products it must also match the size option. |
| `products[].mockups` | array of object | no | Mockup image per print area. Required for embroidery and all-over-print products: without it the order is accepted but cannot be produced (fulfill_log: No mockups found, or No mockups found for <print_space>). Same file rules as designs. |
| `products[].mockups[].file_url` | string | yes | Mockup file URL. |
| `products[].mockups[].print_space` | string | yes | Print area, same rules as designs. |
Example:
```json
{
"order_number": "ORD-123456",
"store_name": "ABC Store",
"store_domain": "abc.com",
"customer_name": "Nguyen Van A",
"customer_email": "email@example.com",
"customer_phone": "0123456789",
"address": "123 ABC Street",
"address_2": "Apartment 101",
"house_number": "12B",
"mailbox_number": "456",
"city": "Los Angeles",
"state": "CA",
"postcode": "90001",
"country": "US",
"shipping_method": "standard",
"order_note": "Deliver during business hours",
"products": [
{
"template_id": 101,
"quantity": 1,
"options": {
"color": "white",
"size": "m"
},
"designs": [
{
"file_url": "https://example.com/design.png",
"print_space": "front"
}
],
"mockups": [
{
"file_url": "https://example.com/mockup.jpg",
"print_space": "front"
}
]
}
]
}
```
#### Responses
- `200`
- `400`
- `401` Invalid or expired API key
```json
{
"success": false,
"error": "api_key_invalid"
}
```
- `409` The same order_number is being created by another call
```json
{
"success": false,
"message": "An order with this order_number is already being created. Look it up with GET /seller-api/orders?s={order_number} before sending it again.",
"data": null
}
```
- `500` Server error
```json
{
"success": false,
"message": "Server internal error",
"data": null
}
```
### Get order details
`GET /seller-api/orders/{id}`
Retrieve the details of a specific order by its ID. To get tracking without polling, set a webhook
(PUT /seller-api/webhook): order.tracking_updated sends this same object.
#### Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | integer | yes | The unique ID of the order. |
#### Responses
- `200`
```json
{
"success": true,
"message": null,
"data": {
"id": 1,
"order_number": "ORD-123456",
"store_name": "ABC Store",
"store_domain": "abc.com",
"customer_name": "Nguyen Van A",
"customer_email": "email@example.com",
"customer_phone": "0123456789",
"address": "123 ABC Street",
"address_2": "Apartment 101",
"house_number": "12B",
"mailbox_number": "456",
"city": "Hanoi",
"state": "Ha Dong",
"postcode": "100000",
"country": "VN",
"order_note": "Deliver during business hours",
"shipping_label": "http://example.com/label.pdf",
"shipping_method": "standard",
"cross_shipping": true,
"personalized": false,
"status": "pending",
"fulfill_status": "unfulfilled",
"address_verified": true,
"fulfill_log": "Order sent to production",
"type": "standard",
"created_at": "2025-02-28T17:01:29+07:00",
"updated_at": "2025-02-28T17:01:29+07:00",
"paid_at": "2025-03-01T12:00:00+07:00",
"delivered_at": "2025-03-05T09:00:00+07:00",
"received_at": "2025-03-06T14:00:00+07:00",
"total_quantity": 3,
"total_product_amount": 59.99,
"total_shipping_amount": 5.99,
"total_amount": 65.98,
"total_paid": 65.98,
"total_product_cost": 30,
"total_shipping_cost": 3,
"total_seller_profit": 32.98,
"processing_fee": 2,
"total_fulfill_fee": 4,
"processing_fee_paid": true,
"fulfill_fee_paid": true,
"products": [
{
"id": 1,
"product_name": "T-shirt",
"sku": "SKU12345",
"quantity": 2,
"price": 19.99,
"total_amount": 39.98,
"seller_profit": 10,
"fulfill_status": "fulfilled",
"full_printed": true,
"barcode": "ABC123456789",
"fulfill_fba_by": "Amazon",
"design_by": "seller",
"campaign_title": "Valentine Campaign",
"tracking_code": "TRACK123456",
"tracking_url": "http://tracking.com/TRACK123456",
"shipping_carrier": "DHL",
"cross_shipping": false,
"color": "Red",
"options": {
"color": "white",
"size": "30x20in"
},
"custom_options": [
{
"type": "text",
"label": "Name",
"value": "Anna"
}
],
"thumb_url": "http://example.com/thumb.jpg",
"mockups": [
{
"id": 1,
"file_url": "http://example.com/mockup.jpg",
"print_space": "front"
}
],
"designs": [
{
"id": 1,
"file_url": "http://example.com/design.jpg",
"print_space": "front"
}
]
}
]
}
}
```
- `401` Invalid or expired API key
```json
{
"success": false,
"error": "api_key_invalid"
}
```
- `404` Order not found
```json
{
"success": false,
"message": "Order not found",
"data": null
}
```
#### Response fields
| Field | Type | Description |
| --- | --- | --- |
| `data.id` | integer | Order ID |
| `data.order_number` | string | Your order number |
| `data.store_name` | string | Store name |
| `data.store_domain` | string | Store domain |
| `data.customer_name` | string | Customer name |
| `data.customer_email` | string | Customer email |
| `data.customer_phone` | string | Customer phone number |
| `data.address` | string | Customer address |
| `data.address_2` | string | Additional address |
| `data.house_number` | string | House number |
| `data.mailbox_number` | string | Mailbox number |
| `data.postcode` | string | Postal code |
| `data.country` | string | Country code |
| `data.order_note` | string | Order note |
| `data.shipping_label` | string | Shipping label URL |
| `data.shipping_method` | string | Shipping method |
| `data.cross_shipping` | boolean | Whether the order uses cross shipping |
| `data.personalized` | boolean | Whether the order is personalized |
| `data.status` | string | Order status: draft, pending, processing, completed, on_hold or cancelled. pending means the order passed validation and is paid and produced automatically one hour after its last update when auto fulfill is on. A new order is returned as draft before it is validated. |
| `data.fulfill_status` | string | Fulfillment status. invalid means validation failed: read fulfill_log. on_delivery means every line has shipped or been cancelled. Other values include unfulfilled, processing, fulfilled, on_hold and cancelled. |
| `data.address_verified` | boolean | Whether the address has been verified |
| `data.fulfill_log` | string | Reason the order is invalid (for example File invalid: <url>, We do not ship to <country>, No artworks found), empty when the order is valid |
| `data.type` | string | Order type |
| `data.created_at` | string | Order creation timestamp |
| `data.updated_at` | string | Order update timestamp |
| `data.paid_at` | string | Timestamp when the order was paid |
| `data.delivered_at` | string | When the first line of the order got tracking (handed to the carrier, not delivered to the customer) |
| `data.received_at` | string | Timestamp when the order was received |
| `data.total_quantity` | integer | Total quantity of products in the order |
| `data.total_product_amount` | number | Total amount for products |
| `data.total_shipping_amount` | number | Total shipping fee |
| `data.total_amount` | number | Total order amount |
| `data.total_paid` | number | Total amount paid |
| `data.total_product_cost` | integer | Total product cost |
| `data.total_shipping_cost` | integer | Total shipping cost |
| `data.total_seller_profit` | number | Total profit for the seller |
| `data.processing_fee` | integer | Processing fee for the order |
| `data.total_fulfill_fee` | integer | Total fulfill fee |
| `data.processing_fee_paid` | boolean | Whether the processing fee has been paid |
| `data.fulfill_fee_paid` | boolean | Whether the fulfill fee has been paid |
| `data.products` | array of object | List of products in the order |
| `data.products[].id` | integer | Order Product ID in the order |
| `data.products[].product_name` | string | Product name |
| `data.products[].sku` | string | Product SKU |
| `data.products[].quantity` | integer | Product quantity |
| `data.products[].price` | number | Price of the product |
| `data.products[].total_amount` | number | Total amount for this product line |
| `data.products[].seller_profit` | integer | Profit earned by seller from this product |
| `data.products[].fulfill_status` | string | Fulfillment status of the product. on_delivery once it has tracking. |
| `data.products[].full_printed` | boolean | Whether the product is full printed |
| `data.products[].barcode` | string | Barcode of the product |
| `data.products[].fulfill_fba_by` | string | Fulfillment service provider |
| `data.products[].design_by` | string | Design will be done by seller or SenPrints |
| `data.products[].campaign_title` | string | Campaign name related to the product |
| `data.products[].tracking_code` | string | Tracking code for this product |
| `data.products[].tracking_url` | string | Tracking URL for this product |
| `data.products[].shipping_carrier` | string | Shipping carrier used |
| `data.products[].cross_shipping` | boolean | Whether the product uses cross shipping |
| `data.products[].color` | string | Product color |
| `data.products[].options` | object | Product options as key-value pairs |
| `data.products[].custom_options` | array of object | Custom options for the product (e.g., text, image) |
| `data.products[].thumb_url` | string | Product thumbnail URL |
| `data.products[].mockups` | array of object | List of product mockups (id, file_url, print_space) |
| `data.products[].designs` | array of object | List of product designs (id, file_url, print_space) |
### Update an order
`PUT /seller-api/orders/{id}`
Only send fields that need updating. Allowed while the order is draft, pending or on_hold (otherwise HTTP 400).
After a successful update the order is validated again in the queue, which can move an on_hold order back to
pending, so a held order is no longer safe from the automatic charge (a valid pending order is paid and produced
one hour after its last update when auto fulfill is on). Errors work as for creating an order: a failed request
rule answers HTTP 200 with success=false and message as an object keyed by field, a failed business rule
answers HTTP 400.
#### Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | integer | yes | The unique ID of the order. |
#### Request body (JSON)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `order_number` | string | no | Your order number, unique per seller. |
| `customer_name` | string | no | Customer name. |
| `customer_email` | string | no | Customer email. |
| `customer_phone` | string | no | Customer phone number. |
| `address` | string | no | Customer address. |
| `address_2` | string | no | Additional address. |
| `house_number` | string | no | House number. |
| `mailbox_number` | string | no | Mailbox number. |
| `city` | string | no | City. |
| `state` | string | no | Required when the country needs a state and the order has none. |
| `postcode` | string | no | Postal code. |
| `country` | string | no | Two-letter country code, for example US, CA, GB, AU, DE, FR, IT, ES, JP. |
| `shipping_method` | string | no | Shipping method. One of: `standard`, `express`. |
| `order_note` | string | no | Order note. |
| `shipping_label` | string | no | Shipping label URL (required if the order is platform). |
| `products` | array of object | no | The products of the order. |
| `products[].id` | integer | no | Order Product ID, fill only when updating an existing product. |
| `products[].campaign_title` | string | no | Campaign name. |
| `products[].quantity` | integer | yes | Product quantity. For an FBA order fulfilled by amazon it must be a multiple of 30, from 30 to 300. |
| `products[].template_id` | integer | yes | Id of a product returned by GET /seller-api/catalog. |
| `products[].options` | object | yes | Product options as key-value pairs. |
| `products[].custom_options` | array of object | no | Custom options for the product, each with type, label and value. |
| `products[].barcode` | string | no | Barcode (required only when fulfill_fba_by is amazon). |
| `products[].fulfill_fba_by` | string | no | Fulfillment type (required only when order is platform). One of: `ebay`, `etsy`, `poshmark`, `tiktok`, `amazon`. |
| `products[].design_by_sen` | boolean | no | If true, the design will be process by SenPrints. |
| `products[].designs` | array of object | no | Artwork files, one per print area. Same file rules as POST /seller-api/orders. |
| `products[].designs[].id` | integer | no | ID of the design file. |
| `products[].designs[].file_url` | string | yes | Artwork file URL. |
| `products[].designs[].print_space` | string | yes | Print area of the product (names are in GET /seller-api/catalog/info), or default for the first print area. |
| `products[].mockups` | array of object | no | Mockup image per print area. Same file rules as designs. |
| `products[].mockups[].id` | integer | no | ID of the mockup file. |
| `products[].mockups[].file_url` | string | yes | Mockup file URL. |
| `products[].mockups[].print_space` | string | yes | Print area, same rules as designs. |
Example:
```json
{
"order_number": "ORD-123456",
"customer_name": "Nguyen Van A",
"customer_email": "email@example.com",
"customer_phone": "0123456789",
"address": "123 ABC Street",
"address_2": "Apartment 101",
"house_number": "12B",
"mailbox_number": "456",
"city": "Los Angeles",
"state": "CA",
"postcode": "90001",
"country": "US",
"shipping_method": "standard",
"order_note": "Deliver during business hours",
"products": [
{
"template_id": 101,
"quantity": 1,
"options": {
"color": "white",
"size": "m"
},
"designs": [
{
"file_url": "https://example.com/design.png",
"print_space": "front"
}
],
"mockups": [
{
"file_url": "https://example.com/mockup.jpg",
"print_space": "front"
}
]
}
]
}
```
#### Responses
- `200`
- `400`
- `401` Invalid or expired API key
```json
{
"success": false,
"error": "api_key_invalid"
}
```
- `404` Order not found
```json
{
"success": false,
"message": "Order not found",
"data": null
}
```
- `500` Server error
```json
{
"success": false,
"message": "Server internal error",
"data": null
}
```
### Check an order without creating it
`POST /seller-api/orders/validate`
Dry run of POST /seller-api/orders, and takes the same body. It runs the same request rules and product checks,
and writes nothing: no order is created, no file is stored, no job is queued and nothing is charged. Use it to
test a payload. A failed check answers HTTP 400, including a missing customer_name, address or country
(POST /seller-api/orders answers HTTP 200 with success=false for failed request rules). Checks that only run
after an order exists (address verification, downloading the artwork files, shipping cost and supplier
assignment) are listed in data.not_checked.
#### Request body (JSON)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `order_number` | string | yes | Your order number, unique per seller. |
| `store_name` | string | no | Store name. |
| `store_domain` | string | no | Store domain. |
| `customer_name` | string | yes | Required, also for FBA orders (HTTP 400 when the key is missing). |
| `customer_email` | string | no | Customer email. |
| `customer_phone` | string | no | Customer phone number. |
| `address` | string | yes | Required, also for FBA orders (HTTP 400 when the key is missing). city and postcode are required unless shipping_label is set. |
| `address_2` | string | no | Additional address. |
| `house_number` | string | no | House number. |
| `mailbox_number` | string | no | Mailbox number. |
| `city` | string | yes | Required unless shipping_label is set (FBA order). |
| `state` | string | no | Required except for FBA orders and GB/AU/PR/DE/SE/NO/FR/GR/PT/RO/PL. |
| `postcode` | string | yes | Required unless shipping_label is set (FBA order). |
| `country` | string | yes | Required, also for FBA orders (HTTP 400 when the key is missing). Two-letter country code, for example US, CA, GB, AU, DE, FR, IT, ES, JP. |
| `shipping_method` | string | no | Shipping method. One of: `standard`, `express`. |
| `order_note` | string | no | Order note. |
| `shipping_label` | string | no | Shipping label URL (required if the order is platform). Setting it makes the order an FBA order. |
| `products` | array of object | yes | The products of the order. |
| `products[].campaign_title` | string | no | Campaign name. |
| `products[].quantity` | integer | yes | Product quantity. For an FBA order fulfilled by amazon it must be a multiple of 30, from 30 to 300. |
| `products[].template_id` | integer | yes | Id of a product returned by GET /seller-api/catalog (the same id as in the product page of the catalog). An inactive or unknown id is rejected with 'Product ID N not found'. |
| `products[].options` | object | yes | Product options as key-value pairs. |
| `products[].custom_options` | array of object | no | Custom options for the product (e.g., text, image), each with type, label and value. |
| `products[].barcode` | string | no | Barcode (required only when fulfill_fba_by is amazon). |
| `products[].fulfill_fba_by` | string | no | Fulfillment type (required only when order is platform). One of: `ebay`, `etsy`, `poshmark`, `tiktok`, `amazon`. |
| `products[].design_by_sen` | boolean | no | If true, the design will be process by SenPrints. |
| `products[].designs` | array of object | no | Artwork files, one per print area. Send them: an order without artwork cannot be produced and may be flagged invalid (fulfill_log: No artworks found). Each file_url must be a public URL ending in jpg, jpeg, png, gif or webp, or a URL on a trusted host (Google Drive, Dropbox, SenPrints CDNs). PSD, PDF, DST and EMB files are rejected, and the file is downloaded and checked after the order is created. |
| `products[].designs[].file_url` | string | yes | Artwork file URL. |
| `products[].designs[].print_space` | string | yes | Print area of the product (names are in GET /seller-api/catalog/info), or default for the first print area. It must be one of the product's own print areas, otherwise the order is rejected with 'Artwork print space X invalid'. For all-over-print products it must also match the size option. |
| `products[].mockups` | array of object | no | Mockup image per print area. Required for embroidery and all-over-print products: without it the order is accepted but cannot be produced (fulfill_log: No mockups found, or No mockups found for <print_space>). Same file rules as designs. |
| `products[].mockups[].file_url` | string | yes | Mockup file URL. |
| `products[].mockups[].print_space` | string | yes | Print area, same rules as designs. |
Example:
```json
{
"order_number": "ORD-123456",
"store_name": "ABC Store",
"store_domain": "abc.com",
"customer_name": "Nguyen Van A",
"customer_email": "email@example.com",
"customer_phone": "0123456789",
"address": "123 ABC Street",
"address_2": "Apartment 101",
"house_number": "12B",
"mailbox_number": "456",
"city": "Los Angeles",
"state": "CA",
"postcode": "90001",
"country": "US",
"shipping_method": "standard",
"order_note": "Deliver during business hours",
"products": [
{
"template_id": 101,
"quantity": 1,
"options": {
"color": "white",
"size": "m"
},
"designs": [
{
"file_url": "https://example.com/design.png",
"print_space": "front"
}
],
"mockups": [
{
"file_url": "https://example.com/mockup.jpg",
"print_space": "front"
}
]
}
]
}
```
#### Responses
- `200` The order data passed every check that can run without creating the order
```json
{
"success": true,
"message": "Order data is valid. Nothing was created and nothing was charged.",
"data": {
"valid": true,
"errors": [],
"warnings": [
"No mockups found"
],
"not_checked": [
"The address is verified after the order is created.",
"Artwork and mockup files are downloaded and checked after the order is created.",
"Shipping cost, supplier assignment and the order total are calculated after the order is created."
]
}
}
```
- `400` The order data is not valid
```json
{
"success": false,
"message": "Order data is not valid",
"data": {
"valid": false,
"errors": {
"products.0.template_id": [
"The products.0.template_id field is required."
]
},
"warnings": [],
"not_checked": [
"The address is verified after the order is created.",
"Artwork and mockup files are downloaded and checked after the order is created.",
"Shipping cost, supplier assignment and the order total are calculated after the order is created."
]
}
}
```
- `401` Invalid or expired API key
```json
{
"success": false,
"error": "api_key_invalid"
}
```
- `500` Server error
```json
{
"success": false,
"message": "Server internal error",
"data": null
}
```
#### Response fields
| Field | Type | Description |
| --- | --- | --- |
| `data.valid` | boolean | True when every check passed |
| `data.errors` | array of any | What to fix: an object keyed by field for request rules, a list of messages for product checks |
| `data.warnings` | array of string | Problems that do not fail POST /seller-api/orders but leave the order unusable, for example 'No artworks found' or 'No mockups found' |
| `data.not_checked` | array of string | Checks that only run after the order is created |
### Update order status
`PUT /seller-api/orders/{id}/{action}`
hold: stops the automatic charge while the order stays untouched (editing it later validates it again and can
return it to pending). resume: puts the order back to draft; it becomes pending when validation passes.
cancel: cancels the order; do it within one hour of the last update to avoid the automatic charge of a valid
pending order.
#### Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | integer | yes | The unique ID of the order. |
| `action` | path | string | yes | Action to update order status: hold, resume or cancel. |
#### Responses
- `200`
```json
{
"success": true,
"message": "Order ORD-123456 status updated successfully",
"data": null
}
```
- `401` Invalid or expired API key
```json
{
"success": false,
"error": "api_key_invalid"
}
```
- `403` Unknown action
```json
{
"success": false,
"message": "Invalid action",
"data": null
}
```
- `404` Order not found
```json
{
"success": false,
"message": "Resource not found",
"data": null,
"errors": null
}
```
- `500` Error updating order status
```json
{
"success": false,
"message": "Server internal error",
"data": null
}
```
## Webhooks
Get order updates pushed to your server instead of polling. Set the URL and the events you want with `PUT /seller-api/webhook`, using your API key only; no dashboard step and no review.
The seller can also see and change the same webhook in Dashboard -> Settings -> General.
**Events**, for your fulfillment and FBA orders, in the order an order usually goes through them. You receive the ones listed in the webhook's `events` (all of them unless you pick some):
- `order.validated`: the order passed the checks run after it was created or updated; `status` is `pending`. With auto fulfill on and enough balance it is charged one hour after its last update.
- `order.invalid`: the order failed them; `fulfill_status` is `invalid` and `fulfill_log` says why. Fix it with `PUT /seller-api/orders/{id}`.
- `order.paid`: the order was charged from your balance and sent to production; `status` is `processing`.
- `order.tracking_updated`: a line got tracking (`products[].tracking_code`, `tracking_url`, `shipping_carrier`). Sent about one minute after the update, once per update, so an order shipped in several parcels sends several.
- `order.completed`: every line has shipped or been cancelled; `status` is `completed`, `fulfill_status` `on_delivery`.
- `order.on_hold`: the order is held: `status` `on_hold` when you hold it (`PUT /seller-api/orders/{id}/hold`), or `fulfill_status` `on_hold` when SenPrints holds it for review (for example a large quantity or amount).
- `order.cancelled`: `status` became `cancelled`.
- `ping`: sent only by `POST /seller-api/webhook/test`.
**Request we send**: `POST` to your URL with a JSON body `{"id": "<delivery id>", "event": "order.paid", "created_at": "<ISO 8601>", "data": {"order": {...}, "previous": {"status": "pending", "fulfill_status": "unfulfilled"}}}` and the headers `X-SenPrints-Event`, `X-SenPrints-Delivery` (same as `id`), `X-SenPrints-Timestamp` (Unix seconds), plus `X-SenPrints-Signature` when the webhook has a signing secret. `data.order` is the same object as `GET /seller-api/orders/{id}`, read when the delivery is sent. `data.previous` is the order's `status` and `fulfill_status` before the change, `null` for `order.tracking_updated`.
**Signature (optional)**: a webhook only needs a URL and events; it has no signing secret and its deliveries are not signed unless you ask. To verify deliveries, create a secret with `PUT /seller-api/webhook` and `"rotate_secret": true` (drop it again with `"remove_secret": true`). Every delivery then carries `X-SenPrints-Signature`: `sha256=` followed by the hex HMAC-SHA256 of `{X-SenPrints-Timestamp}.{raw request body}`, keyed with the `secret` returned by `GET`/`PUT /seller-api/webhook`. Compute it on the raw body before parsing the JSON, compare in constant time, and reject a timestamp more than 5 minutes old. In PHP: `hash_equals('sha256=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret), $signature)`. In Node.js: `'sha256=' + crypto.createHmac('sha256', secret).update(timestamp + '.' + rawBody).digest('hex')`.
**Delivery**: answer any 2xx within 10 seconds. Anything else, a timeout or a redirect counts as a failure and is retried 5 times over about 3 hours (after 1 min, 5 min, 15 min, 1 h, 2 h). Delivery is at least once: deduplicate on `id`. Events can arrive out of order, so trust the `order` you receive over its arrival order, or read `GET /seller-api/orders/{id}`.
**Delivery log**: every attempt, with the body sent and how your URL answered it, is listed by `GET /seller-api/webhook/deliveries` for 30 days.
### Get the webhook
`GET /seller-api/webhook`
Returns the URL that receives your webhooks, its events, and the secret that signs them (null when
deliveries are not signed).
#### Responses
- `200`
```json
{
"success": true,
"message": null,
"data": {
"url": "https://example.com/senprints/webhook",
"secret": "whsec_3jT9qX2mPz8LkV4nR7sW1yB6cD0fG5hJ2aE8uQ",
"events": [
"order.validated",
"order.invalid",
"order.paid",
"order.on_hold",
"order.cancelled",
"order.completed",
"order.tracking_updated"
]
}
}
```
- `401` Invalid or expired API key
```json
{
"success": false,
"error": "api_key_invalid"
}
```
- `404` No webhook set
```json
{
"success": false,
"message": "Webhook not set",
"data": null
}
```
#### Response fields
| Field | Type | Description |
| --- | --- | --- |
| `data.url` | string | URL that receives the webhooks |
| `data.secret` | string | Signing secret for X-SenPrints-Signature, null when deliveries are not signed |
| `data.events` | array of string | Events sent to the URL (the ones you picked) |
### Set the webhook
`PUT /seller-api/webhook`
Creates the webhook or changes its URL and events. A new webhook receives every event unless `events`
picks some; a later call without `events` keeps the current list. Only `url` is required: the webhook
has no signing secret until `rotate_secret` is true, which creates one (or replaces it); `remove_secret`
drops it. Any other call keeps the current secret, or the lack of one. Takes effect for the next event.
#### Request body (JSON)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | string | yes | HTTPS URL that receives the webhooks, up to 255 characters. It must resolve to a public address and answer 2xx; redirects are not followed. |
| `events` | array of string | no | Events to receive. A new webhook gets every event when this is omitted; on a change, omitting it keeps the current list. One of: `order.validated`, `order.invalid`, `order.paid`, `order.on_hold`, `order.cancelled`, `order.completed`, `order.tracking_updated`. |
| `rotate_secret` | boolean | no | Set true to create a signing secret, or replace the current one. A webhook has none until you ask: deliveries are not signed. |
| `remove_secret` | boolean | no | Set true to drop the signing secret; deliveries are no longer signed. Cannot be combined with rotate_secret. |
Example:
```json
{
"url": "https://example.com/senprints/webhook",
"events": [
"order.paid",
"order.tracking_updated"
],
"rotate_secret": false,
"remove_secret": false
}
```
#### Responses
- `200`
```json
{
"success": true,
"message": "Webhook saved",
"data": {
"url": "https://example.com/senprints/webhook",
"secret": null,
"events": [
"order.paid",
"order.tracking_updated"
]
}
}
```
- `400` Invalid URL
```json
{
"success": false,
"message": "The url must resolve to a public address.",
"data": null,
"errors": {
"url": [
"The url must resolve to a public address."
]
}
}
```
- `401` Invalid or expired API key
```json
{
"success": false,
"error": "api_key_invalid"
}
```
### Delete the webhook
`DELETE /seller-api/webhook`
Stops the webhooks. Deliveries already queued are dropped.
#### Responses
- `200`
```json
{
"success": true,
"message": "Webhook deleted",
"data": null
}
```
- `401` Invalid or expired API key
```json
{
"success": false,
"error": "api_key_invalid"
}
```
- `404` No webhook set
```json
{
"success": false,
"message": "Webhook not set",
"data": null
}
```
### List recent deliveries
`GET /seller-api/webhook/deliveries`
The webhook requests SenPrints sent to your URL in the last 30 days, newest first, one row per attempt:
the exact body posted, how your URL answered (status and the start of its response), and why an attempt
failed. Use it to find out why an event did not arrive, or to replay a body against your handler. A failed
attempt is retried (see Delivery above), and each retry is a new row with the same `id`; its body is
rebuilt with the order as it is then. Test pings are listed too.
#### Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `order_id` | query | integer | no | Only the deliveries about this order. |
| `failed_only` | query | boolean | no | Set 1 to list only the attempts that failed. |
| `limit` | query | integer | no | How many attempts to return, newest first, from 1 to 100. Defaults to 20 (each row carries the order body). |
#### Responses
- `200`
```json
{
"success": true,
"message": null,
"data": [
{
"id": "0d8f3c9e-5b6a-4a7e-9f1c-2b3d4e5f6a7b",
"event": "order.tracking_updated",
"order_id": 123456,
"attempt": 2,
"delivered": true,
"request_body": {
"id": "0d8f3c9e-5b6a-4a7e-9f1c-2b3d4e5f6a7b",
"event": "order.tracking_updated",
"created_at": "2026-10-21T09:16:02+00:00",
"data": {
"order": {
"id": 123456,
"order_number": "ORD-123456",
"status": "processing",
"fulfill_status": "on_delivery",
"products": [
{
"id": 987654,
"fulfill_status": "on_delivery",
"tracking_code": "9400111899223344556677",
"tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223344556677",
"shipping_carrier": "USPS"
}
]
},
"previous": null
}
},
"status_code": 200,
"response_body": "{\"received\":true}",
"error": null,
"duration_ms": 184,
"created_at": "2026-10-21T09:16:02+00:00"
},
{
"id": "0d8f3c9e-5b6a-4a7e-9f1c-2b3d4e5f6a7b",
"event": "order.tracking_updated",
"order_id": 123456,
"attempt": 1,
"delivered": false,
"request_body": {
"id": "0d8f3c9e-5b6a-4a7e-9f1c-2b3d4e5f6a7b",
"event": "order.tracking_updated",
"created_at": "2026-10-21T09:15:01+00:00",
"data": {
"order": {
"id": 123456,
"order_number": "ORD-123456"
},
"previous": null
}
},
"status_code": 503,
"response_body": "Service Unavailable",
"error": "Your URL answered HTTP 503",
"duration_ms": 92,
"created_at": "2026-10-21T09:15:01+00:00"
},
{
"id": "6c1a2b3c-4d5e-4f60-8a9b-0c1d2e3f4a5b",
"event": "ping",
"order_id": null,
"attempt": 1,
"delivered": false,
"request_body": {
"id": "6c1a2b3c-4d5e-4f60-8a9b-0c1d2e3f4a5b",
"event": "ping",
"created_at": "2026-10-20T14:02:11+00:00",
"data": {
"message": "This is a test webhook from SenPrints."
}
},
"status_code": null,
"response_body": null,
"error": "Could not connect to your URL (timeout, refused connection or TLS error).",
"duration_ms": 10003,
"created_at": "2026-10-20T14:02:11+00:00"
}
]
}
```
- `401` Invalid or expired API key
```json
{
"success": false,
"error": "api_key_invalid"
}
```
### Send a test event
`POST /seller-api/webhook/test`
Sends a `ping` event to your URL now (signed when the webhook has a secret) and reports how it answered.
Use it to check the URL, and your signature check if you use one, before the first order ships. It is
not retried.
#### Responses
- `200`
- `401` Invalid or expired API key
```json
{
"success": false,
"error": "api_key_invalid"
}
```
- `404` No webhook set
```json
{
"success": false,
"message": "Webhook not set",
"data": null
}
```
## Catalog
### Get the list of catalogs
`GET /seller-api/catalog`
Retrieve the list of catalogs available in the system. The tags below document GET /seller-api/catalog for Scribe.
Products are grouped by category. A product's `id` is the `template_id` of POST /seller-api/orders and the `id`
of GET /seller-api/catalog/info; `options` are the values an order line picks from, and `print_spaces` the names
its `designs[].print_space` uses (sizes in pixels).
#### Responses
- `200`
```json
{
"success": true,
"message": null,
"data": [
{
"id": 12,
"name": "T-Shirts",
"full_name": "T-Shirts",
"slug": "t-shirts",
"products": [
{
"id": 101,
"name": "Unisex Standard T-Shirt",
"sku": "AMST",
"thumb_url": "https://example.com/thumb.jpg",
"base_cost": 8.5,
"base_costs": null,
"suggested_price": 24.99,
"options": {
"color": [
"white",
"black",
"navy"
],
"size": [
"s",
"m",
"l",
"xl",
"2xl"
]
},
"print_spaces": [
{
"name": "front",
"width": 4200,
"height": 4800
},
{
"name": "back",
"width": 4200,
"height": 4800
}
],
"extra_print_cost": 2,
"priority": null,
"mockup_type": "flat",
"attributes": null,
"pricing_mode": "adjust_price",
"full_printed": 0,
"status": "active",
"market_location": null,
"system_type": "regular",
"quantity": null,
"default_option": null,
"custom_options": null,
"colors": [
{
"name": "white",
"hex_code": "#ffffff"
},
{
"name": "black",
"hex_code": "#000000"
},
{
"name": "navy",
"hex_code": "#1f2a44"
}
],
"sellable_option_types": [],
"mockups": [],
"selected": false,
"mockups_psd": []
}
],
"child_menu": []
}
]
}
```
- `401` Invalid or expired API key
```json
{
"success": false,
"error": "api_key_invalid"
}
```
### Get catalog info
`GET /seller-api/catalog/info`
Get details of a specific catalog product. Public API get template product info for catalog; the tags below
document GET /seller-api/catalog/info for Scribe.
`variants` hold the base cost per option combination (`variant_key` like `white-s`) and location (`*` = any);
`shipping_rules` the shipping cost per method and location; `shipping_times` the delivery days per method,
keyed by location code (`*` = everywhere else); `processing_time` is in days.
#### Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | query | integer | yes | The ID of the catalog product, from GET /seller-api/catalog. |
#### Responses
- `200`
```json
{
"success": true,
"message": null,
"data": {
"name": "Unisex Standard T-Shirt",
"thumb_url": "https://example.com/thumb.jpg",
"sku": "AMST",
"variants": [
{
"product_id": 101,
"variant_key": "white-s",
"base_cost": 8.5,
"location_code": "*",
"out_of_stock": 0,
"adjust_price": 0,
"price": 24.99,
"old_price": 0,
"weight": 0.15,
"quantity": null,
"check_quantity": 0,
"sku": "AMST-WHITE-S"
},
{
"product_id": 101,
"variant_key": "black-2xl",
"base_cost": 10.5,
"location_code": "*",
"out_of_stock": 0,
"adjust_price": 2,
"price": 26.99,
"old_price": 0,
"weight": 0.18,
"quantity": null,
"check_quantity": 0,
"sku": "AMST-BLACK-2XL"
}
],
"shipping_rules": [
{
"product_id": 101,
"variant_key": null,
"shipping_method": "standard",
"location_code": "US",
"shipping_cost": 4.99,
"extra_cost": 2
},
{
"product_id": 101,
"variant_key": null,
"shipping_method": "standard",
"location_code": "*",
"shipping_cost": 8.99,
"extra_cost": 3
}
],
"description": "Classic fit, 100% cotton.",
"market_location": null,
"colors": [
{
"name": "white",
"hex_code": "#ffffff"
},
{
"name": "black",
"hex_code": "#000000"
}
],
"print_spaces": [
{
"name": "front",
"width": 4200,
"height": 4800
},
{
"name": "back",
"width": 4200,
"height": 4800
}
],
"attributes": null,
"options": {
"color": [
"white",
"black"
],
"size": [
"s",
"m",
"l",
"xl",
"2xl"
]
},
"size_guides": [],
"shipping_times": {
"standard": {
"US": {
"min": 3,
"max": 8
},
"*": {
"min": 7,
"max": 15
}
},
"express": {
"US": {
"min": 2,
"max": 4
},
"*": {
"min": 5,
"max": 9
}
}
},
"processing_time": 2
}
}
```
- `401` Invalid or expired API key
```json
{
"success": false,
"error": "api_key_invalid"
}
```
- `404` Product template not found
```json
{
"success": false,
"message": "Product template not found",
"data": null
}
```
## Me
### Get current user info
`GET /seller-api/me`
Retrieve the details of the authenticated user. The tags below document GET /seller-api/me for Scribe.
The account is under `user` (not `data`) and carries more settings fields than the example shows;
`balance` is what fulfillment orders are charged from.
#### Responses
- `200`
```json
{
"success": true,
"user": {
"id": 1234,
"name": "John Doe",
"email": "john@example.com",
"status": "active",
"role": "seller",
"balance": 152.4,
"hold_amount": 0,
"currency": "USD",
"timezone": "America/New_York",
"language": "en",
"market_location": "*",
"price_tag": "default",
"pricing_mode": "adjust_price",
"auto_fulfill": 1,
"first_order_at": "2026-03-02 08:15:00",
"last_order_at": "2026-10-20 17:42:10"
}
}
```
- `401` Invalid or expired API key
```json
{
"success": false,
"error": "api_key_invalid"
}
```