Home / Platforms / API Documentation

API Reference

FramesOnDemand API

Turn our shop floor into an endpoint. Send a paid order as JSON, hand us a print file, and we produce and ship the framed piece under your brand. This is the full reference for the REST API.

Introduction

The FramesOnDemand API is a REST interface for custom-framed print fulfillment. You send us an order and a print file. We print it, frame it, and ship it from our shop in New Jersey. Nothing in the box points back to us, so the package arrives under your brand. This is blind, white-label fulfillment, run as an API.

The flow is simple. You post an order, we produce it, and we call your endpoint back with the tracking number when the label prints. You never manage carriers, labels, or production yourself.

Who this is for

  • Platforms. Storefront builders, headless carts, and marketplaces that want to offer real framed art without running a frame shop.
  • Institutions. Museum gift shops, galleries, and university stores that connect their own storefront to our endpoints and ship finished, gallery-grade pieces to buyers.
  • Direct sellers. Sellers on a custom stack who want framed prints in their own checkout, from Magento to a bespoke internal tool.

Access is by requestThe API is scoped to partners and institutions. Request credentials at partners@framesondemand.app and our team issues a key for your account. This is standard B2B access control for a fulfillment API.

Base URL and versioning

All API requests go to a single host over HTTPS. The version is part of the path, so a new version never breaks a running integration. Every path in this reference is relative to the base URL below.

BASE https://api.framesondemand.app/v1

The current version is v1. A request to create an order is a POST to https://api.framesondemand.app/v1/orders. When we ship a new major version, it lives under a new prefix such as /v2, and /v1 keeps working. All requests and responses use JSON with UTF-8 encoding.

Authentication

The API uses bearer tokens. You get an API key for your account when access is granted. Send it in the Authorization header on every request. Keep the key on your server. Never ship it in a browser, a mobile app, or a public repo.

# every request carries your key in the Authorization header
curl https://api.framesondemand.app/v1/orders \
  -H "Authorization: Bearer $FOD_API_KEY" \
  -H "Content-Type: application/json"

A missing or bad key returns 401 Unauthorized. Keys are scoped to one account, so orders you create are only visible to you. If a key is ever exposed, email us and we roll it for a new one.

Orders: create an order

Create an order to send us a piece to produce and ship. You pass the buyer ship-to address and one or more line items. Each line item picks a size and a frame and points at a print file. We validate the file, produce the piece, and ship it.

POST /v1/orders

Paid orders onlySend us orders the buyer has already paid for. We produce paid work. This keeps you from being billed for a cart that was never checked out or a sale that was canceled.

Request body

FieldTypeDescription
external_id requiredstringYour own order id. We echo it back and use it to keep requests idempotent.
ship_to requiredobjectThe buyer address. Holds name, address1, address2, city, state, zip, and country.
items requiredarrayOne or more line items. Each maps to a size, a frame, an optional mat, and a print file.
items[].print_file_idstringThe id of a print file you uploaded first. See Print files.
items[].print_file_urlstringA signed URL to the source file, used in place of print_file_id.
items[].width_in requirednumberFinished print width in inches.
items[].height_in requirednumberFinished print height in inches.
items[].framestringFrame slug from the catalog, such as museum-black. Leave it out for an unframed print.
items[].matstringMat slug, such as antique-white. Optional.
items[].quantityintegerHow many of this item to make. Defaults to 1.
callback_urlstringYour endpoint. We POST tracking to it when the label prints.

Example request

# create a framed print order
curl -X POST https://api.framesondemand.app/v1/orders \
  -H "Authorization: Bearer $FOD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "order-10482",
    "ship_to": {
      "name": "A. Buyer",
      "address1": "12 Main St",
      "city": "Austin",
      "state": "TX",
      "zip": "78701",
      "country": "US"
    },
    "items": [{
      "print_file_id": "pf_9m2k1a7",
      "width_in": 18,
      "height_in": 24,
      "frame": "museum-black",
      "mat": "antique-white",
      "quantity": 1
    }],
    "callback_url": "https://you.com/fod/hook"
  }'

Example response

// 201 Created
{
  "id": "ord_7Qf3xB2k",
  "external_id": "order-10482",
  "status": "received",
  "created_at": "2026-07-09T14:22:05Z",
  "items": [{
    "id": "itm_1a2b3c",
    "width_in": 18,
    "height_in": 24,
    "frame": "museum-black",
    "mat": "antique-white",
    "quantity": 1
  }],
  "tracking": null
}

The id is our order id. Save it. You can poll it for status or wait for the tracking callback. Because external_id is idempotent, sending the same one twice returns the first order instead of making a duplicate.

Order status

Read an order at any time to see where it is in production and shipping. You can look it up by our order id or by your own external_id.

GET /v1/orders/{id}
# read one order
curl https://api.framesondemand.app/v1/orders/ord_7Qf3xB2k \
  -H "Authorization: Bearer $FOD_API_KEY"
// 200 OK
{
  "id": "ord_7Qf3xB2k",
  "external_id": "order-10482",
  "status": "shipped",
  "tracking": {
    "carrier": "usps",
    "number": "9400100000000000000000",
    "url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400100000000000000000"
  },
  "shipped_at": "2026-07-12T17:04:00Z"
}

Status values

StatusMeaning
receivedWe have the order and are checking it.
validatingWe are checking the print file resolution and mapping the line items to real catalog parts.
in_productionThe piece is on our shop floor being printed and framed.
shippedThe label printed and the piece is with the carrier. Tracking is set.
deliveredThe carrier marked the package delivered.
on_holdWe paused the order and need input, such as a better file. See the hold_reason field.
canceledThe order was canceled before it shipped.

Tracking and webhooks

You do not have to poll. When a label prints, we POST a webhook to the callback_url you set on the order. Your system reads it and sends the shipping email to your buyer, or writes tracking back to your own marketplace. This is the write-back that closes the loop.

Webhook payload

// POST to your callback_url
{
  "event": "order.shipped",
  "order_id": "ord_7Qf3xB2k",
  "external_id": "order-10482",
  "status": "shipped",
  "tracking": {
    "carrier": "usps",
    "number": "9400100000000000000000",
    "url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400100000000000000000"
  },
  "shipped_at": "2026-07-12T17:04:00Z"
}

Verify the signature

Every webhook carries a FOD-Signature header. It is an HMAC of the raw request body, signed with your webhook secret. Recompute the HMAC on your side and compare it to the header before you trust the payload. Reject the request if they do not match.

# header on every webhook
FOD-Signature: t=1752338640,v1=6a3f0c...b2

# verify (Node)
const expected = crypto
  .createHmac("sha256", process.env.FOD_WEBHOOK_SECRET)
  .update(rawBody)
  .digest("hex");
if (!crypto.timingSafeEqual(sig, expected)) return reject();

Respond with a 2xx status to confirm you got the webhook. If your endpoint is down or returns an error, we retry with backoff for up to 24 hours, so build your handler to be idempotent on order_id.

Products and catalog

Line items map to real parts in our catalog. That catalog holds more than 40 mouldings, 46 mats, and custom sizing, all cut and joined in our own shop. You reference a frame or mat by its slug and a size by width and height in inches.

GET /v1/catalog
// 200 OK
{
  "frames": [
    { "slug": "museum-black", "name": "Museum Black", "width_in": 0.875 },
    { "slug": "antique-gold", "name": "Antique Gold", "width_in": 1.25 }
  ],
  "mats": [
    { "slug": "antique-white", "name": "Antique White" },
    { "slug": "black", "name": "Black" }
  ],
  "sizes": {
    "min_in": 4,
    "max_in": 40,
    "custom": true
  }
}

Sizing is custom to the inch inside the supported range, so you are not stuck with a handful of stock sizes. Pull the catalog once at build time and cache it, then refresh it when you want the newest frames and mats.

Errors

The API uses standard HTTP status codes. A 2xx means success. A 4xx means the request had a problem you can fix. A 5xx means something failed on our side. Every error returns the same JSON shape.

// 422 Unprocessable Entity
{
  "error": {
    "type": "validation_error",
    "message": "print file resolution is below the minimum for this size",
    "field": "items[0].print_file_id"
  }
}
StatusTypeWhen it happens
400bad_requestThe JSON was malformed or a required field is missing.
401unauthorizedThe API key is missing or wrong.
403forbiddenThe key is valid but not allowed to do this.
404not_foundNo order or resource matches that id.
409conflictAn order with that external_id already exists.
422validation_errorA field failed a rule, such as a file below the resolution minimum.
429rate_limitedToo many requests. Slow down and retry.
500server_errorSomething failed on our side. Retry the request.

Rate limits

Each API key can make up to 120 requests per minute. Order creation has its own steadier limit so a large batch stays smooth. Every response carries headers that tell you where you stand.

HeaderMeaning
X-RateLimit-LimitRequests allowed in the current window.
X-RateLimit-RemainingRequests you have left in this window.
X-RateLimit-ResetUnix time when the window resets.
Retry-AfterSeconds to wait, sent with a 429.

If you hit the limit you get a 429. Back off for the number of seconds in Retry-After and try again. If you run high-volume batches, tell us and we raise the limit for your account.

Support and access

To get credentials, email partners@framesondemand.app and tell us what you are building. We issue an API key and a webhook secret for your account and share the sandbox details so you can test before you go live.

White-glove for institutions

Museums, galleries, universities, and larger catalogs do not have to self-serve. Our team scopes the integration with you, maps your catalog to our frames and mats, and stands up the connection alongside your developers. You get museum-grade materials and one shop accountable for every piece. Read more on the institutions page.