API Documentation

egful API

Push orders into our factory and get production status and tracking back. REST over HTTPS, JSON in and out, API-key auth, signed webhooks.

Quickstart

  1. 1 · Create a test key

    Sign in, open Settings → API keys, and press Generate test key. Copy it now; it is shown once.

  2. 2 · Send your first test call

    Put your key in the header and call Validate key:

    curl https://api.egful.store/api/v1/ping \
      -H "X-API-Key: egk_test_..."

    You should get "ok": true and "mode": "test". Then try Quote an order and Create order with the sample body.

  3. 3 · Add a webhook for order updates

    Register an https URL for order.shipped to get the tracking number without polling. See Webhooks.

  4. 4 · Create a live key when your test calls work

    Same panel, switch on LIVE, and swap the key. Nothing else changes. Orders are then real and billed to your balance.

Authentication

Every request carries an API key in the X-API-Key header. Generate one in your dashboard under Settings → API keys.

curl https://api.egful.store/api/v1/ping \
  -H "X-API-Key: egk_test_..."

The prefix tells you which mode a key is in.

  • egk_test_… — the sandbox. Priced and validated like live, nothing produced or billed. Webhooks fire with test: true.
  • egk_live_… — real. Orders enter the factory queue and are billed.

The full key is shown once. We store only a hash.

Scopes

Limit a key to what the integration needs.

ScopeAllows
orders.writeCreate and cancel orders.
orders.readRead an order's status and tracking, and price a basket.
products.readList the blanks you can order.
webhooks.readList endpoints and their delivery history.
webhooks.writeAdd, remove and test endpoints.
billing.readRead your account balance.

A call outside a key's scopes returns 403 with insufficient_scope. A key with no scopes has full access.

Rate limits

ScopeLimit
All endpoints600 requests / minute / key
Order creation60 requests / minute / key

Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds). Over the limit returns 429 with Retry-After.

Endpoints

Base URL https://api.egful.store. All paths take and return application/json.

GET/api/v1/pingValidate key

Checks your key and tells you which mode it is in.

Scope any key

Try it
curl -X GET https://api.egful.store/api/v1/ping \
  -H "X-API-Key: egk_test_..."
Response
{
  "ok": true,
  "mode": "test",
  "live": false,
  "seller_id": "8f3c…",
  "time": "2026-07-21T16:04:11.882Z",
  "message": "Sandbox reachable — your test key is valid."
}
GET/api/v1/productsList products

The blanks you can print on — your real catalogue, in test and live mode.

  • Order lines must use a sku from this list.
  • sizes and colors are what it is made in. Empty means it does not vary.
  • prices: one unit per size and method, one position, before plan discount. price is the lowest. null means not priced yet.
  • print_areas: each position's largest size per method, with placement and a template_url. A null size is not published yet — ask us first.

Scope products.read

ParameterInTypeDescription
sku
optional
querystringReturn one product.
limit
optional
queryinteger1 to 1000. Defaults to 500.
offset
optional
queryintegerSkip this many. Use with has_more to page.
Try it
curl -X GET https://api.egful.store/api/v1/products \
  -H "X-API-Key: egk_test_..."
Response
{
  "object": "list",
  "mode": "test",
  "count": 1,
  "has_more": false,
  "data": [
    { "sku": "EG-5000", "name": "Heavy Cotton Tee", "type": "T-shirts", "method": "DTG / Embroidery",
      "price": 7.08, "base_price": 7.08,
      "prices": [
        { "method": "dtg", "sizes": { "S": 7.08, "M": 7.08, "L": 7.08, "XL": 7.08, "2XL": 9.05 } },
        { "method": "embroidery", "sizes": { "S": 11.33, "M": 11.33, "L": 11.33, "XL": 11.33, "2XL": 13.30 } }
      ],
      "sizes": ["S", "M", "L", "XL", "2XL"],
      "colors": ["Black", "White", "Navy"],
      "print_areas": [
        { "position": "front", "name": "Front", "method": "dtg",
          "width_in": 14, "height_in": 18, "dpi": 300, "width_px": 4200, "height_px": 5400,
          "placement": { "measured_from": "collar", "top_in": 2, "horizontal": "centered", "offset_in": 0 },
          "default_placement": { "top_in_below_collar": 3, "max_width_in": 12,
                                 "lower_by_size": { "XL": 0.5, "2XL": 0.5, "3XL": 1, "4XL": 1, "5XL": 1, "6XL": 1 } },
          "template_url": "https://api.egful.store/api/v1/templates/t-shirts/front/dtg.png" },
        { "position": "leftchest", "name": "Left chest", "method": "embroidery",
          "width_in": 4, "height_in": 4,
          "placement": { "measured_from": "collar", "top_in": 2, "horizontal": "right", "offset_in": 5.5 },
          "template_url": "https://api.egful.store/api/v1/templates/t-shirts/leftchest/emb.png" },
        { "position": "back", "name": "Back", "method": "dtg",
          "width_in": null, "height_in": null }
      ] }
  ]
}
POST/api/v1/ordersCreate order

Creates a fulfilment order.

  • Test key: priced, nothing created. Live key: created as New, charged and made once submitted.
  • The same external_id returns the same order. totals.total is what submitting charges; breakdown lists what it is made of.
  • Any file size is accepted. print_files[].placed is what we will make: size, top_in_below_collar, centered and decided_by.
  • Who sets the size, first match wins: your width_in/top_in (you) · your mockup (your_mockup) · an embroidery file's own size at 300 dpi, front or back (your_file) · the product's default_placement (us).
  • An order is never refused for placement. If it does not fit, we make the closest fit and say so under warnings (placement_adjusted). Under 150 DPI is low_resolution.
  • Bigger than the template, or over 8 thread colours, is made as sent and charged extra: see print_files[].oversize and the Extra row. Send oversize: "fit" (now, or later with Answer an oversize) to keep it inside at no extra.
  • A file at the area's pixel size or proportions fills it exactly (placed.fills_area). Embroidery measures the visible design, not empty margin.
  • placed.preview_url is a picture of the design on the garment. It needs no key, so keep it private. A test order's picture lasts a day.

Scope orders.write

ParameterInTypeDescription
external_id
optional
bodystringYour order number. Always send it: a repeat returns the first order, not a duplicate.
order_number
optional
bodystringThe marketplace's order number (Etsy, TikTok…). Shown and searchable.
items
required
bodyarrayOne entry per line.
items[].product_id
required
bodystringA sku from List products. Unknown: 400 unpriceable_lines.
items[].quantity
optional
bodyintegerDefaults to 1.
items[].size
optional
bodystringOne of the product's sizes. Optional if one-size.
items[].color
optional
bodystringOne of the product's colours. Else 400 unavailable_size.
items[].method
optional
bodystringPrint method, e.g. DTG, EMB. Affects the price.
items[].positions
optional
bodyarrayPositions to price, e.g. ["front", "back"], or { position, method }. Defaults to front. Unknown: 400 unknown_position.
items[].print_files
optional
bodyarrayArtwork, one per position, 6 at most: [{ "position": "front", "url": "https://…" }]. Unreachable: 400 print_files_refused.
items[].print_files[].url
optional
bodystringPublic https PNG, JPEG or WebP, ≤25 MB. Drive/Dropbox links work. In a Drive folder: PNG = artwork, JPEG = mockup, name PNGs by position (front.png). .dst/.pes/.emb are attached too.
items[].print_files[].position
optional
bodystringFrom the product's print_areas, e.g. front, back, left_shoulder. Defaults to front.
items[].print_files[].oversize
optional
bodystringIf larger than the area: "accept" (default) makes it as sent and charges extra; "fit" shrinks it to fit, no extra.
items[].print_files[].width_in
optional
bodynumberDesign width in inches. Left out, we decide. Too wide is shrunk to fit (placed.adjusted).
items[].print_files[].top_in
optional
bodynumberInches from collar to the top of the design; always centred. Left out, we use default_placement. Outside the area is moved in (placed.adjusted).
items[].mockups
optional
bodyarrayHow each side should look. Same shape as print_files, ≤5 MB, needs a print file for that position.
items[].print_file_url
optional
bodystringShorthand for one front file.
shipping_address
required
bodyobjectname, street1, street2, city, state, zip, country, phone. street2 and phone optional.
shipping_label
optional
bodyobjectYour own label, e.g. TikTok Shop: { url, tracking_number, carrier }. We buy no postage; the own-label fee applies. Unreachable, or not an object (text, or a link under another key like label_url): 400 shipping_label_refused, nothing created.
shipping_label.url
optional
bodystringPublic https PDF, PNG or JPEG, ≤10 MB.
shipping_label.tracking_number
optional
bodystringRequired with shipping_label.
shipping_label.carrier
optional
bodystringe.g. USPS, UPS, FedEx. Optional.
Try it
curl -X POST https://api.egful.store/api/v1/orders \
  -H "X-API-Key: egk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
  "external_id": "my-store-1001",
  "items": [
    {
      "product_id": "16468",
      "quantity": 2,
      "color": "Black",
      "size": "L",
      "method": "DTG"
    }
  ],
  "shipping_address": {
    "name": "Ava Brodeur",
    "street1": "43 Calumet Rd",
    "city": "Fairhaven",
    "state": "MA",
    "zip": "02719",
    "country": "US"
  }
}'
Response
{
  "object": "order",
  "mode": "live",
  "id": "API-9F2C1A",
  "status": "received",
  "items": [
    { "line": 1, "sku": "16468", "quantity": 2, "size": "L",
      "unit_price": 8.50, "line_total": 17.00 }
  ],
  "print_files": [
    { "line": 1, "position": "front", "stored": true,
      "placed": { "width_in": 12, "height_in": 5.75, "top_in_below_collar": 3,
                  "centered": true, "decided_by": "us",
                  "preview_url": "https://api.egful.store/api/v1/previews/API-9F2C1A/ln-1/front/6f2b0c9e1a7d4e3b5c8a2f10.png" } }
  ],
  "shipping_address": { "name": "Ava Brodeur", "street1": "43 Calumet Rd",
    "city": "Fairhaven", "state": "MA", "zip": "02719", "country": "US" },
  "totals": { "items": 17.00, "currency": "USD", "total": 24.99 },
  "breakdown": [
    { "line": 1, "label": "Blank", "amount": 17.00 },
    { "line": 1, "label": "DTG · Front", "position": "front", "method": "dtg", "amount": 0 },
    { "label": "Shipping", "amount": 7.99 }
  ],
  "created": "2026-07-21T16:04:11.882Z"
}
POST/api/v1/orders/quoteQuote an order

Prices a basket without creating an order.

  • Same pricing as the real charge, postage and discount included. Nothing is created or charged.
  • The breakdown rows add up to totals.total.
  • Shipping is our fulfilment charge, not a live carrier rate.

Scope orders.read

ParameterInTypeDescription
items
required
bodyarrayOne entry per line.
items[].product_id
required
bodystringA sku from List products. Unknown: 400 unpriceable_lines.
items[].quantity
optional
bodyintegerDefaults to 1.
items[].size
optional
bodystringOne of the product's sizes. Optional if one-size.
items[].color
optional
bodystringOne of the product's colours. Else 400 unavailable_size.
items[].method
optional
bodystringPrint method, e.g. DTG, EMB. Affects the price.
items[].positions
optional
bodyarrayPositions to price, e.g. ["front", "back"], or { position, method }. Defaults to front. Unknown: 400 unknown_position.
Try it
curl -X POST https://api.egful.store/api/v1/orders/quote \
  -H "X-API-Key: egk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
  "items": [
    {
      "product_id": "16468",
      "quantity": 2,
      "color": "Black",
      "size": "L",
      "method": "DTG"
    }
  ]
}'
Response
{
  "object": "quote",
  "mode": "live",
  "currency": "USD",
  "lines": [
    { "line": 1, "sku": "16468", "product": "Heavy Cotton Tee", "size": "L",
      "method": "DTG", "quantity": 2, "unit_price": 8.50, "line_total": 17.00 }
  ],
  "totals": { "items": 17.00, "shipping": 7.99, "discount": { "percent": 20, "amount": 3.40 },
    "total": 21.59, "units": 2 },
  "breakdown": [
    { "line": 1, "label": "Blank", "amount": 17.00 },
    { "line": 1, "label": "DTG · Front", "position": "front", "method": "dtg", "amount": 0 },
    { "label": "Shipping", "amount": 7.99 },
    { "label": "Discount", "percent": 20, "amount": -3.40 }
  ]
}
POST/api/v1/orders/:id/oversizeAnswer an oversize

Changes a design placed from your mockup to fit the print area, or back. Before submit only.

  • Submitting accepts the extra. Send fit before submitting to avoid it.
  • 409 once submitted — message us in chat.
  • Test key: same shape, nothing changes.

Scope orders.write

ParameterInTypeDescription
id
required
pathstringThe order id returned by Create order.
answer
required
bodystring"fit": inside the area, no extra. "keep": mockup size, with the extra.
line
optional
bodyintegerOnly this item (1 = first). Left out: all.
position
optional
bodystringOnly this position, e.g. front. Left out: all.
Try it
curl -X POST https://api.egful.store/api/v1/orders/ord_test123/oversize \
  -H "X-API-Key: egk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
  "answer": "fit"
}'
Response
{
  "object": "order",
  "mode": "live",
  "id": "API-9F2C1A",
  "answer": "fit",
  "positions": [ { "line": 1, "position": "front" } ],
  "totals": { "total": 24.99, "currency": "USD" },
  "breakdown": [ { "line": 1, "label": "Blank", "amount": 17.00 }, { "label": "Shipping", "amount": 7.99 } ]
}
POST/api/v1/orders/:id/submitSubmit order

Charges the order to your wallet and sends it to production.

  • Nothing is charged or made until submitted. Submitting twice charges once.
  • 402: the wallet cannot cover it. 409: a line is not ready. The message says which.
  • A 402 for a short wallet carries required, balance and shortfall. The order keeps its place and is submitted automatically when a top-up covers it; you will get order.payment_failed too.
  • breakdown lists what charged was for. Test key: charged: 0.

Scope orders.write

ParameterInTypeDescription
id
required
pathstringThe order id returned by Create order.
Try it
curl -X POST https://api.egful.store/api/v1/orders/ord_test123/submit \
  -H "X-API-Key: egk_test_..."
Response
{
  "object": "order",
  "mode": "live",
  "id": "API-9F2C1A",
  "status": "in_review",
  "charged": 21.59,
  "breakdown": [
    { "line": 1, "label": "Blank", "amount": 17.00 },
    { "line": 1, "label": "DTG · Front", "position": "front", "method": "dtg", "amount": 0 },
    { "label": "Shipping", "amount": 7.99 },
    { "label": "Discount", "percent": 20, "amount": -3.40 }
  ]
}
GET/api/v1/ordersList orders

Your orders, newest first.

  • external_id is the id you sent with Create order.
  • Test key: an empty list.

Scope orders.read

ParameterInTypeDescription
limit
optional
queryinteger1 to 100. Defaults to 25.
created_after
optional
querystringCreated after this time, ISO 8601.
created_before
optional
querystringCreated before this time. Pass the last order's created for the next page.
status
optional
querystringe.g. new, in_review, shipped, cancelled.
Try it
curl -X GET https://api.egful.store/api/v1/orders \
  -H "X-API-Key: egk_test_..."
Response
{
  "object": "list",
  "mode": "live",
  "has_more": false,
  "data": [
    { "object": "order", "id": "API-9F2C1A", "external_id": "my-store-1001", "status": "shipped",
      "tracking": { "carrier": "USPS", "code": "9400100000000000000000" },
      "total": 17.00, "created": "2026-07-21T16:04:11.882Z" }
  ]
}
POST/api/v1/orders/:id/cancelCancel order

Cancels an order that has not reached production and refunds it.

  • 409 once approved, labelled, or with a designer.
  • Reserved units are released. Cancelling twice is safe.

Scope orders.write

ParameterInTypeDescription
id
required
pathstringThe order id returned by Create order.
Try it
curl -X POST https://api.egful.store/api/v1/orders/ord_test123/cancel \
  -H "X-API-Key: egk_test_..."
Response
{
  "object": "order",
  "mode": "live",
  "id": "API-9F2C1A",
  "status": "cancelled",
  "refunded": 21.59
}
GET/api/v1/orders/:idRetrieve order

Looks up one order by id.

  • print_files is the stored artwork, per line and position. shipping_address comes back on API orders.
  • A cancelled order also carries reason, rejected_by and rejected_at.
  • Test key: a simulated order with no lines; total is null.

Scope orders.read

ParameterInTypeDescription
id
required
pathstringThe order id returned by Create order.
Try it
curl -X GET https://api.egful.store/api/v1/orders/ord_test123 \
  -H "X-API-Key: egk_test_..."
Response
{
  "object": "order",
  "mode": "live",
  "id": "API-9F2C1A",
  "status": "shipped",
  "external_id": "my-store-1001",
  "tracking": { "carrier": "USPS", "code": "9400100000000000000000" },
  "total": 17.00,
  "created": "2026-07-21T16:04:11.882Z",
  "items": [
    { "line": 1, "sku": "16468", "product": "Heavy Cotton Tee", "quantity": 2,
      "color": "Black", "size": "L", "method": "DTG", "unit_price": 8.50 }
  ],
  "print_files": [ { "line": 1, "position": "front", "stored": true } ],
  "shipping_address": { "name": "Ava Brodeur", "street1": "43 Calumet Rd",
    "city": "Fairhaven", "state": "MA", "zip": "02719", "country": "US" }
}
GET/api/v1/stockCheck stock

What we can make right now: available quantity per blank sku.

  • status is one of in_stock, low, out_of_stock.
  • available is on-hand minus what is already committed. A print-method suffix (-EMB, -DTG, …) is ignored.

Scope products.read

ParameterInTypeDescription
sku
optional
querystringOne blank. Omit for all (up to 1,000).
Try it
curl -X GET https://api.egful.store/api/v1/stock \
  -H "X-API-Key: egk_test_..."
Response
{
  "object": "list",
  "mode": "test",
  "count": 2,
  "data": [
    { "sku": "16468", "name": "Heavy Cotton Tee", "variant": "Black / L",
      "category": "Apparel", "available": 90, "status": "in_stock" },
    { "sku": "LA6", "name": "Trucker Cap", "variant": "Navy",
      "category": "Headwear", "available": 12, "status": "low" }
  ]
}
GET/api/v1/balanceAccount balance

What is currently on your account.

  • Negative means charges exceed funds.
  • awaiting_funds is orders whose submit was refused for a short wallet. The next top-up submits them, oldest first, so available is what a new order can use.

Scope billing.read

Try it
curl -X GET https://api.egful.store/api/v1/balance \
  -H "X-API-Key: egk_test_..."
Response
{
  "object": "balance",
  "mode": "live",
  "account": "8f3c…",
  "balance": 90.80,
  "awaiting_funds": { "orders": 1, "amount": 24.50 },
  "available": 66.30,
  "currency": "USD"
}
GET/api/webhooksList webhooks

Your registered webhook endpoints.

  • Secrets are shown once, when the endpoint is created.

Scope webhooks.read

Try it
curl -X GET https://api.egful.store/api/webhooks \
  -H "X-API-Key: egk_test_..."
Response
[
  { "id": 3,
    "url": "https://your-app.example.com/hooks/egful",
    "events": ["order.received", "order.shipped"],
    "active": true,
    "created_at": "2026-07-21T14:00:00.000Z" }
]
POST/api/webhooksAdd a webhook

Registers an https endpoint to be told about order events.

  • The signing secret is returned once. Store it.
  • Each delivery carries X-EG-Event and X-EG-Signature (sha256=<hex>), an HMAC-SHA256 of the raw body with that secret.
  • Up to ten endpoints per account.

Scope webhooks.write

ParameterInTypeDescription
url
required
bodystringPublic https URL. Redirects are not followed.
events
optional
bodystring[]Any of order.received, order.status_changed, order.shipped, order.cancelled, order.payment_failed. Omit for all.
Try it
curl -X POST https://api.egful.store/api/webhooks \
  -H "X-API-Key: egk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://your-app.example.com/hooks/egful",
  "events": [
    "order.received",
    "order.status_changed",
    "order.shipped",
    "order.cancelled",
    "order.payment_failed"
  ]
}'
Response
{
  "id": 4,
  "url": "https://your-app.example.com/hooks/egful",
  "events": ["order.shipped"],
  "active": true,
  "secret": "egwh_2f9c1d4b…",
  "_note": "Store this secret now — it is not shown again."
}
POST/api/webhooks/:id/testSend a test delivery

Sends a sample event to one endpoint, signed like a real one.

  • The payload carries test: true.

Scope webhooks.write

ParameterInTypeDescription
id
required
pathintegerThe webhook id from List webhooks.
event
optional
bodystringDefaults to order.shipped.
Try it
curl -X POST https://api.egful.store/api/webhooks/1/test \
  -H "X-API-Key: egk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
  "event": "order.shipped"
}'
Response
{
  "ok": true,
  "event": "order.shipped",
  "url": "https://your-app.example.com/hooks/egful",
  "status_code": 200,
  "attempts": 1,
  "error": null
}
GET/api/webhooks/eventsList event names

The events a webhook can subscribe to.

Scope any key

Try it
curl -X GET https://api.egful.store/api/webhooks/events \
  -H "X-API-Key: egk_test_..."
Response
{
  "events": ["order.received", "order.status_changed", "order.shipped", "order.cancelled", "order.payment_failed"]
}
DELETE/api/webhooks/:idRemove a webhook

Stops deliveries to one endpoint.

Scope webhooks.write

ParameterInTypeDescription
id
required
pathintegerThe webhook id from List webhooks.
Try it
curl -X DELETE https://api.egful.store/api/webhooks/1 \
  -H "X-API-Key: egk_test_..."
Response
{ "ok": true }
GET/api/webhooks/:id/deliveriesDelivery attempts

The last 100 delivery attempts for one endpoint.

  • Deliveries retry three times and stop on a 4xx.

Scope webhooks.read

ParameterInTypeDescription
id
required
pathintegerThe webhook id from List webhooks.
Try it
curl -X GET https://api.egful.store/api/webhooks/1/deliveries \
  -H "X-API-Key: egk_test_..."
Response
[
  { "id": 11, "event": "order.shipped", "status_code": 200,
    "error": null, "attempts": 1, "created_at": "2026-07-21T15:16:00.000Z" },
  { "id": 10, "event": "order.received", "status_code": 500,
    "error": null, "attempts": 3, "created_at": "2026-07-21T15:02:00.000Z" }
]

Billing

GET /api/v1/balance returns what is currently on account. It needs billing.read. Negative means charges exceed funds.

{
  "object": "balance",
  "mode": "live",
  "account": "8f3c…",
  "balance": 90.80,
  "currency": "USD"
}

There is no statement endpoint. Every movement is itemised on your wallet page.

Webhooks

Register a URL and we POST to it when something happens. Add one in the app under API Documentation → Webhooks, where you can also send a test delivery.

EventFires when
order.receivedWe accepted an order you pushed.
order.status_changedIt moved along the production pipeline.
order.shippedTracking exists. This is the one most integrations care about.
order.cancelledIt was cancelled or refunded — or we refused it. Carries reason and rejected_by.
POST https://your-app.example.com/hooks/egful
X-EG-Event: order.shipped
X-EG-Signature: sha256=<hex>

{
  "event": "order.shipped",
  "created": "2026-07-21T14:56:43.584Z",
  "data": {
    "id": "API-9F2C1A",
    "status": "shipped",
    "tracking": { "carrier": "USPS", "code": "9400100000000000000000" },
    "total": 24.50
  }
}

Your endpoint must be public https and answer 2xx within 10 seconds. Acknowledge first, then do the slow work.

Redirects are not followed, so register the final URL. The host must resolve to a public address. An account may hold ten endpoints.

Failed deliveries retry three times. A 5xx, 429 or 408 is retried; any other 4xx stops. The same event can arrive twice and out of order, so treat each payload as the current state.

Verifying signatures

The signature proves a delivery came from us. It is an HMAC-SHA256 of the raw request body, keyed with the secret shown when you created the endpoint.

import crypto from "node:crypto"

// express.raw() — NOT express.json(). Re-serialising the body
// changes whitespace and key order, and the digest will not match.
app.post("/hooks/egful", express.raw({ type: "application/json" }), (req, res) => {
  const presented = String(req.headers["x-eg-signature"] || "").replace("sha256=", "")
  const expected  = crypto.createHmac("sha256", SECRET).update(req.body).digest("hex")

  const a = Buffer.from(presented), b = Buffer.from(expected)
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401)

  res.sendStatus(200)                        // acknowledge first
  process(JSON.parse(req.body.toString()))   // then work
})

Compare in constant time, not with ===.

Errors

Every error is JSON with error in plain words and a code to switch on. Missing fields are listed under missing, unpriceable lines under unpriced.

{
  "error": "An order needs a non-empty "items" array.",
  "code": "invalid_request",
  "mode": "live",
  "missing": ["items"]
}
{
  "error": "Some lines have no catalogue match, so they cannot be priced or produced.",
  "code": "unpriceable_lines",
  "unpriced": [{ "line": 2, "sku": "NOT-OURS", "size": "L", "method": "DTG" }]
}
StatusMeansWhat to do
400
invalid_request
The request is missing something or has a value we can't use.Read `error` and `missing` in the body — they name the field. Fix it and send again.
400
unpriceable_lines
A line's product_id is not in the catalogue, so it can't be priced.Check each sku in `unpriced` against GET /api/v1/products. Nothing was created.
401The key is missing, mistyped or revoked.Send it in the X-API-Key header, exactly as copied from Settings → API keys.
403
insufficient_scope
The key is valid but not allowed to make this call.The body's `required` names the scope. Create a key that includes it.
404No order with that id belongs to this key's account.Use the `id` returned by Create order, not your own external_id.
409The order is already in production and can no longer be cancelled.Nothing to retry. Contact us if it must be stopped.
429Too many requests for this key.Wait the number of seconds in the Retry-After header, then retry.
500
internal_error
Something failed on our side.Safe to retry reads. For Create order, retry with the same external_id so it cannot duplicate.
501A capability we document as not offered (carrier label buying).Read tracking from the order instead — we buy the label when we ship.

Every line must match a product in our catalogue. Check your SKUs against GET /api/v1/products.

If we refuse an order after accepting it

Some problems only show on the floor, such as a blank out of stock. The order is then cancelled, and the reason is on the order.cancelled event and on the order.

GET /api/v1/orders/API-9F2C1A

{
  "object": "order",
  "id": "API-9F2C1A",
  "status": "cancelled",
  "reason": "Blank out of stock in Navy 2XL",
  "rejected_by": "factory",
  "rejected_at": "2026-07-21T17:04:11.882Z"
}

rejected_by is factory when we refused it and seller when you cancelled it.

Connect MCP

Connect an AI assistant to look up orders, price a basket, check stock and read your balance, and to place or cancel orders if you allow it. Included on Pro and Enterprise.

  1. 1 · Add it to your assistant

    In Claude open Settings → Connectors → Add custom connector, paste this address, then approve the sign-in.

    https://mcp.egful.store/mcp
  2. 2 · Sign in and approve

    Your assistant opens EGFUL in the browser. Choose Read only or Full access. Revoke it any time in Settings → API keys.

    A script or an agent that cannot sign in: the same address, https://mcp.egful.store/mcp, with an API key in the X-API-Key header.

  3. 3 · Ask it something

    “Has order API-9F2C1A shipped?” or “What would 40 black tees in L cost?”

What it can do

ToolDoesEndpoint
list_productsList products/api/v1/products
create_orderCreate order · writes/api/v1/orders
quote_orderQuote an order/api/v1/orders/quote
cancel_orderCancel order · writes/api/v1/orders/:id/cancel
get_orderRetrieve order/api/v1/orders/:id
check_stockCheck stock/api/v1/stock
get_balanceAccount balance/api/v1/balance

It can do what you allowed, and nothing more. Read only looks things up and prices work. Full access can also place and cancel orders, and your assistant asks before either. There is no tool that lists your orders. A retried order is safe: create_order always sends an external_id. Artwork is added in the app afterwards.

An API key limited to products.read is offered only the product tools. Nothing appears that the connection would be refused.