DTF Center API

API documentation

Endpoints, authentication, errors, and webhooks — everything you need to integrate.

Quickstart

  1. 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.
  2. POST https://api.dtfcenter.com/api/v1/orders with your order payload.
  3. 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 fieldNotes
nameRequired.
address1, city, zipRequired. address2 and company optional.
provinceState code — required for US addresses (alias state).
countryISO code, defaults to US.
phoneRecommended; some carriers require it.
residentialDefaults 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.

shippingServiceServicePrice
ups_groundUPS Ground (1-3 Days Delivery)$9.95 up to a $100.00 order · Free from $100.00
ups_2dayUPS 2nd Day Air (1-2 Days Delivery)$16.95
ups_overnightUPS 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 fieldNotes
name / companyAt least one. Max 35 characters each.
phoneOptional, max 20. Carriers print a phone; without yours, ours is used.
returnAddressOptional. 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_xxxxxxxx

Calls 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/v1

The 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.statusMeaning
PAIDCharged, or debited from your prepaid balance.
PENDINGAwaiting confirmation — finalized via webhook.
PAYMENT_FAILEDCharge failed — the order won't ship until resolved.
INVOICEDAccrued 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.

StatusCodeMeaning
400validation_errorMalformed body or field.
401unauthorizedMissing / invalid / revoked key.
402insufficient_balancePrepaid balance exhausted — top up.
403insufficient_scopeKey lacks the required scope.
404order_not_foundNo such order for your client.
409order_lockedOrder already in production or shipped — its items can't change.
409order_cancelledOrder was cancelled — use a new externalOrderId.
409already_shippedCan't cancel an order that has shipped.
409idempotency_key_reusedSame key, different body.
413payload_too_largeBody exceeds the size limit.
429rate_limitedToo many requests.
503api_disabledPublic 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 your externalOrderId for 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 — shipTo address (required for ship orders), the order.ready_for_pickup event, 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.billing field, 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.