Quickstart
- Once your access is approved, sign in to your account and create a key — start with a
dtf_test_…key. Each key is shown once. POST https://api.dtfcenter.com/api/v1/orderswith your order payload.- Poll
GET /orders/{externalOrderId}for status, or subscribe to webhooks.
For each design, send a link, the print width, and a quantity. Width is required — we resize the design to it. Height is optional: omit it to keep the design's aspect ratio, or set it to force an exact size. You don't pick a "type" — orders are laid out by size automatically. (Already arranged a full 22" sheet yourself? Send the advanced workflow field — see the reference.)
Every order is either shipped (shipMode: "SHIP", the default — send a shipTo address) or held for pickup (shipMode: "PICKUP"). See Shipping & pickup.
Create an order
cURL
curl -X POST https://api.dtfcenter.com/api/v1/orders \
-H "Authorization: Bearer dtf_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"externalOrderId": "PO-10044",
"customerName": "Acme Co",
"shipMode": "SHIP",
"shippingService": "ups_ground",
"shipTo": {
"name": "Jane Doe", "address1": "100 Main St",
"city": "Dallas", "province": "TX", "zip": "75201", "country": "US",
"phone": "2145550100"
},
"lineItems": [
{ "title": "Custom DTF Transfer", "quantity": 3,
"assetUrl": "https://cdn.acme.com/art/123.png",
"widthInch": 11, "heightInch": 14 }
]
}'JavaScript
const res = await fetch("https://api.dtfcenter.com/api/v1/orders", {
method: "POST",
headers: {
"Authorization": "Bearer dtf_live_xxxxxxxx",
"Content-Type": "application/json",
},
body: JSON.stringify({
externalOrderId: "PO-10044",
shipTo: {
name: "Jane Doe", address1: "100 Main St",
city: "Dallas", province: "TX", zip: "75201", country: "US",
},
lineItems: [
{ title: "Custom DTF Transfer", quantity: 3,
assetUrl: "https://cdn.acme.com/art/123.png", widthInch: 11 },
],
}),
});
const { order } = await res.json();Python
import requests
res = requests.post(
"https://api.dtfcenter.com/api/v1/orders",
headers={"Authorization": "Bearer dtf_live_xxxxxxxx"},
json={
"externalOrderId": "PO-10044",
"shipMode": "PICKUP", # held for pickup — no shipTo needed
"lineItems": [
{"title": "Custom DTF Transfer", "quantity": 3,
"assetUrl": "https://cdn.acme.com/art/123.png", "widthInch": 11},
],
},
)
order = res.json()["order"]Shipping & pickup
Ship orders (shipMode: "SHIP", the default) need a shipTo object — an order without one is rejected with 400 validation_error. We buy the label, and order.shipped carries the tracking number.
| shipTo field | Notes |
|---|---|
name | Required. |
address1, city, zip | Required. address2 and company optional. |
province | State code — required for US addresses (alias state). |
country | ISO code, defaults to US. |
phone | Recommended; some carriers require it. |
residential | Defaults to true; set false for a business. |
Pickup orders (shipMode: "PICKUP") need no address; when the package is ready we send order.ready_for_pickup. You can change shipTo or shipMode with PATCH until the label is bought.
Shipping speed & price
Pick the service with shippingService on a ship order — the same options and prices our own checkout offers. Omit it for the cheapest. Like at checkout, the price that applies depends on the order total (your print total), so it's settled when the order is charged and added to that charge; order.shipping and billing.shippingCents report it. The service's prices are locked when you create the order.
| shippingService | Service | Price |
|---|---|---|
ups_ground | UPS Ground (1-3 Days Delivery) | $9.95 up to a $100.00 order · Free from $100.00 |
ups_2day | UPS 2nd Day Air (1-2 Days Delivery) | $16.95 |
ups_overnight | UPS Standard Overnight (Next Day Delivery) | $39.95 up to a $249.00 order · Free from $249.00 |
This list is live: fetch it any time with GET https://api.dtfcenter.com/api/v1/shipping-options rather than hard-coding prices.
Blind shipping
Dropshipping to your own customers? Put your name on the label. Set a default sender in your account (Shipping sender), or send sender on an order to override it:
"sender": {
"company": "Peachy Pixels",
"phone": "2145550100",
"returnAddress": {
"address1": "12 Peach Ln", "city": "Plano",
"province": "TX", "zip": "75024", "country": "US"
}
}| sender field | Notes |
|---|---|
name / company | At least one. Max 35 characters each. |
phone | Optional, max 20. Carriers print a phone; without yours, ours is used. |
returnAddress | Optional. address1, city, zip (and a 2-letter province in the US). Address lines max 35, city max 30. Undeliverable parcels go here. |
Standard letters, digits and punctuation only — carrier label fonts can't print other characters. The parcel is still collected from DTF Center, so the rate is based on our location. Nothing inside the package carries DTF Center branding or prices. You can change the sender with PATCH until the label is bought.
Custom t-shirts
Beyond film transfers, you can order printed t-shirts. Send the apparel (brand, color, size) and one design per placement — we print each placement as its own DTF transfer and press it onto the shirt.
Add a line item with productType: "CUSTOM_TSHIRT", an apparel object (brand, color, size — all required), and a placements array. Valid positions: front, back, pocket_left, pocket_right, sleeve_left, sleeve_right. Each placement needs a widthInch (height optional — same rule as a transfer).
curl -X POST https://api.dtfcenter.com/api/v1/orders \
-H "Authorization: Bearer dtf_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"externalOrderId": "PO-5001",
"shipMode": "PICKUP",
"lineItems": [
{
"title": "Team Event Tee",
"quantity": 10,
"productType": "CUSTOM_TSHIRT",
"apparel": { "brand": "Gildan 5000", "color": "Black", "size": "L" },
"placements": [
{ "position": "front", "assetUrl": "https://cdn.acme.com/front.png",
"widthInch": 11, "heightInch": 14 },
{ "position": "sleeve_left", "assetUrl": "https://cdn.acme.com/sleeve.png",
"widthInch": 3 }
]
}
]
}'When you read the order back, the shirt returns as a single custom_tshirt line item with its apparel and a placements array — each placement carrying its own status and gangsheetCreated.
Authentication
Pass your key as a bearer token on every request. Keys look like dtf_live_…; you create and revoke them yourself in your account (up to 5 active keys).
Authorization: Bearer dtf_live_xxxxxxxxCalls are server-to-server — never expose a key in a browser or a public repo. Lost or leaked a key? Create a new one in your account and revoke the old one — it stops working immediately.
Test mode
A dtf_test_… key behaves exactly like a live key, but its orders never enter production or billing — they're hidden from our operators and auto-purged after a week. Test and live orders are kept completely apart: a test key can't see or change a live order, even with the same externalOrderId. Use it freely while you build your integration, then switch to your dtf_live_… key to go live.
Base URL & versioning
The current base URL is:
https://api.dtfcenter.com/api/v1The version is in the path (/v1). We'll announce a new version before changing anything breaking.
Rate limits
Each response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (seconds). Over the limit you get 429 with a Retry-After header — back off and retry.
Billing
If billing is enabled for your account, we price each order from the actual print area once its gangsheets are generated — so the amount reflects what we produced, and height stays optional. When your order shares a sheet with other orders, only your part of the sheet counts. An order is shipped (or released for pickup) once it is paid.
The order.billing object reports the charge:
| billing.status | Meaning |
|---|---|
PAID | Charged, or debited from your prepaid balance. |
PENDING | Awaiting confirmation — finalized via webhook. |
PAYMENT_FAILED | Charge failed — the order won't ship until resolved. |
INVOICED | Accrued to your monthly invoice. |
Prepaid accounts with an exhausted balance get 402 insufficient_balance when creating an order — top up, then retry. Test-mode orders are never charged.
Errors
Every error is { "ok": false, "error": { "code", "message" } }. Branch on error.code.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | Malformed body or field. |
| 401 | unauthorized | Missing / invalid / revoked key. |
| 402 | insufficient_balance | Prepaid balance exhausted — top up. |
| 403 | insufficient_scope | Key lacks the required scope. |
| 404 | order_not_found | No such order for your client. |
| 409 | order_locked | Order already in production or shipped — its items can't change. |
| 409 | order_cancelled | Order was cancelled — use a new externalOrderId. |
| 409 | already_shipped | Can't cancel an order that has shipped. |
| 409 | idempotency_key_reused | Same key, different body. |
| 413 | payload_too_large | Body exceeds the size limit. |
| 429 | rate_limited | Too many requests. |
| 503 | api_disabled | Public API is turned off. |
Webhooks
We POST order-lifecycle events to your callback URL: order.received, order.approved, order.gangsheet_ready, order.shipped, order.ready_for_pickup, order.cancelled.
Add your https endpoint in your account → Webhooks (up to 3). The signing secret is shown once, right there — store it with your endpoint. Send test posts a signed ping event so you can check your verification; New secret replaces a leaked one. Answer with any 2xx; other responses are retried with backoff for about a day. Redirects are not followed, so register the final URL.
Each delivery carries an X-DTF-Signature header (t=<unix>,v1=<hmac>) and an X-DTF-Event-Id for deduplication. Verify it:
const crypto = require("crypto");
// rawBody: the exact request body string, before any JSON parsing.
function verifyDtfSignature(rawBody, header, signingSecret, toleranceSec = 300) {
const parts = Object.fromEntries(
String(header || "").split(",").map((p) => p.split("="))
); // { t, v1 }
const t = Number(parts.t);
if (!t || !parts.v1) return false;
// Reject stale deliveries (replay protection).
if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto
.createHmac("sha256", signingSecret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(parts.v1, "hex");
const b = Buffer.from(expected, "hex");
// timingSafeEqual throws on different lengths — compare lengths first.
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Changelog
- v1.3.1 — Live orders get a DTF Center order name (
orderName, e.g.API-100023) used on our production sheets. Keep using yourexternalOrderIdfor every API call and in webhooks — it doesn't change. - v1.3.0 — Shipping speed & price (
shippingService,GET /shipping-options, shipping added to the charge), blind shipping (sender+ account default) and self-serve webhooks in your account, with a signed test event. - v1.2.0 —
shipToaddress (required for ship orders), theorder.ready_for_pickupevent, custom t-shirts,409 order_cancelled, shared-sheet flag on gangsheets, and input limits (quantity ≤ 10,000, width ≤ 60", height ≤ 1,200"). Test and live orders are fully separated. - v1.1.0 — Billing: per-order pricing, the
order.billingfield, and test mode. - v1.0.0 — Initial release: orders, status, gangsheets, webhooks.
Support
No access yet? Request access. Questions? Email support@neuralmatrix.io or open the interactive reference for a built-in console.
Your account: sign in to track your orders, view tracking, and save or update a payment method.