egful API
Quickstart
1 · Create a test key
Sign in, open Settings → API keys, and press Generate test key. Copy it now; it is shown once.
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": trueand"mode": "test". Then try Quote an order and Create order with the sample body.3 · Add a webhook for order updates
Register an https URL for
order.shippedto get the tracking number without polling. See Webhooks.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 withtest: 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.
| Scope | Allows |
|---|---|
| orders.write | Create and cancel orders. |
| orders.read | Read an order's status and tracking, and price a basket. |
| products.read | List the blanks you can order. |
| webhooks.read | List endpoints and their delivery history. |
| webhooks.write | Add, remove and test endpoints. |
| billing.read | Read 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
| Scope | Limit |
|---|---|
| All endpoints | 600 requests / minute / key |
| Order creation | 60 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.
Print areas
Each product in List products says where it can be decorated and how big.
"print_areas": [
{ "position": "front", "name": "Front", "method": "dtg",
"width_in": 12, "height_in": 20, "dpi": 300, "width_px": 3600, "height_px": 6000,
"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/apparel/front/dtg.png" },
{ "position": "leftchest", "name": "Left chest", "method": "embroidery",
"width_in": 4, "height_in": 4,
"placement": { "measured_from": "collar", "top_in": 1.5, "horizontal": "right", "offset_in": 4 },
"template_url": "https://api.egful.store/api/v1/templates/apparel/leftchest/emb.png" }
]| Field | Means |
|---|---|
| position | e.g. front, back, leftchest. Send it as print_files[].position. |
| name | The position in words, e.g. Left chest. |
| method | dtg, dtf, embroidery, applique, laser or screen. One entry per position and method. |
| width_in · height_in | Largest size there, in inches. null = not published yet. |
| dpi · width_px · height_px | File size to build at 300 DPI. Printed methods only. |
| default_placement | Printed fronts and backs: where a design goes with no top_in/width_in — top_in_below_collar, max_width_in, lower_by_size (e.g. 3XL: 1). |
| placement | Where the area sits: measured_from (collar), top_in, horizontal, offset_in. null on sleeves and caps. |
| template_url | Picture of the area with its measurements. No key needed. null if none. |
Endpoints
Base URL https://api.egful.store. All paths take and return application/json.
/api/v1/pingValidate keyChecks your key and tells you which mode it is in.
Scope any key
curl -X GET https://api.egful.store/api/v1/ping \ -H "X-API-Key: egk_test_..."
{
"ok": true,
"mode": "test",
"live": false,
"seller_id": "8f3c…",
"time": "2026-07-21T16:04:11.882Z",
"message": "Sandbox reachable — your test key is valid."
}/api/v1/productsList productsThe blanks you can print on — your real catalogue, in test and live mode.
- Order lines must use a
skufrom this list. sizesandcolorsare what it is made in. Empty means it does not vary.prices: one unit per size and method, one position, before plan discount.priceis the lowest.nullmeans not priced yet.print_areas: each position's largest size per method, withplacementand atemplate_url. Anullsize is not published yet — ask us first.
Scope products.read
| Parameter | In | Type | Description |
|---|---|---|---|
skuoptional | query | string | Return one product. |
limitoptional | query | integer | 1 to 1000. Defaults to 500. |
offsetoptional | query | integer | Skip this many. Use with has_more to page. |
curl -X GET https://api.egful.store/api/v1/products \ -H "X-API-Key: egk_test_..."
{
"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 }
] }
]
}/api/v1/ordersCreate orderCreates a fulfilment order.
- Test key: priced, nothing created. Live key: created as New, charged and made once submitted.
- The same
external_idreturns the same order.totals.totalis what submitting charges;breakdownlists what it is made of. - Any file size is accepted.
print_files[].placedis what we will make: size,top_in_below_collar,centeredanddecided_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'sdefault_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 islow_resolution. - Bigger than the template, or over 8 thread colours, is made as sent and charged extra: see
print_files[].oversizeand theExtrarow. Sendoversize: "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_urlis 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
| Parameter | In | Type | Description |
|---|---|---|---|
external_idoptional | body | string | Your order number. Always send it: a repeat returns the first order, not a duplicate. |
order_numberoptional | body | string | The marketplace's order number (Etsy, TikTok…). Shown and searchable. |
itemsrequired | body | array | One entry per line. |
items[].product_idrequired | body | string | A sku from List products. Unknown: 400 unpriceable_lines. |
items[].quantityoptional | body | integer | Defaults to 1. |
items[].sizeoptional | body | string | One of the product's sizes. Optional if one-size. |
items[].coloroptional | body | string | One of the product's colours. Else 400 unavailable_size. |
items[].methodoptional | body | string | Print method, e.g. DTG, EMB. Affects the price. |
items[].positionsoptional | body | array | Positions to price, e.g. ["front", "back"], or { position, method }. Defaults to front. Unknown: 400 unknown_position. |
items[].print_filesoptional | body | array | Artwork, one per position, 6 at most: [{ "position": "front", "url": "https://…" }]. Unreachable: 400 print_files_refused. |
items[].print_files[].urloptional | body | string | Public 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[].positionoptional | body | string | From the product's print_areas, e.g. front, back, left_shoulder. Defaults to front. |
items[].print_files[].oversizeoptional | body | string | If larger than the area: "accept" (default) makes it as sent and charges extra; "fit" shrinks it to fit, no extra. |
items[].print_files[].width_inoptional | body | number | Design width in inches. Left out, we decide. Too wide is shrunk to fit (placed.adjusted). |
items[].print_files[].top_inoptional | body | number | Inches 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[].mockupsoptional | body | array | How each side should look. Same shape as print_files, ≤5 MB, needs a print file for that position. |
items[].print_file_urloptional | body | string | Shorthand for one front file. |
shipping_addressrequired | body | object | name, street1, street2, city, state, zip, country, phone. street2 and phone optional. |
shipping_labeloptional | body | object | Your 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.urloptional | body | string | Public https PDF, PNG or JPEG, ≤10 MB. |
shipping_label.tracking_numberoptional | body | string | Required with shipping_label. |
shipping_label.carrieroptional | body | string | e.g. USPS, UPS, FedEx. Optional. |
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"
}
}'{
"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"
}/api/v1/orders/quoteQuote an orderPrices a basket without creating an order.
- Same pricing as the real charge, postage and discount included. Nothing is created or charged.
- The
breakdownrows add up tototals.total. - Shipping is our fulfilment charge, not a live carrier rate.
Scope orders.read
| Parameter | In | Type | Description |
|---|---|---|---|
itemsrequired | body | array | One entry per line. |
items[].product_idrequired | body | string | A sku from List products. Unknown: 400 unpriceable_lines. |
items[].quantityoptional | body | integer | Defaults to 1. |
items[].sizeoptional | body | string | One of the product's sizes. Optional if one-size. |
items[].coloroptional | body | string | One of the product's colours. Else 400 unavailable_size. |
items[].methodoptional | body | string | Print method, e.g. DTG, EMB. Affects the price. |
items[].positionsoptional | body | array | Positions to price, e.g. ["front", "back"], or { position, method }. Defaults to front. Unknown: 400 unknown_position. |
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"
}
]
}'{
"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 }
]
}/api/v1/orders/:id/oversizeAnswer an oversizeChanges a design placed from your mockup to fit the print area, or back. Before submit only.
- Submitting accepts the extra. Send
fitbefore submitting to avoid it. - 409 once submitted — message us in chat.
- Test key: same shape, nothing changes.
Scope orders.write
| Parameter | In | Type | Description |
|---|---|---|---|
idrequired | path | string | The order id returned by Create order. |
answerrequired | body | string | "fit": inside the area, no extra. "keep": mockup size, with the extra. |
lineoptional | body | integer | Only this item (1 = first). Left out: all. |
positionoptional | body | string | Only this position, e.g. front. Left out: all. |
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"
}'{
"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 } ]
}/api/v1/orders/:id/submitSubmit orderCharges 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,balanceandshortfall. The order keeps its place and is submitted automatically when a top-up covers it; you will getorder.payment_failedtoo. breakdownlists whatchargedwas for. Test key:charged: 0.
Scope orders.write
| Parameter | In | Type | Description |
|---|---|---|---|
idrequired | path | string | The order id returned by Create order. |
curl -X POST https://api.egful.store/api/v1/orders/ord_test123/submit \ -H "X-API-Key: egk_test_..."
{
"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 }
]
}/api/v1/ordersList ordersYour orders, newest first.
external_idis the id you sent with Create order.- Test key: an empty list.
Scope orders.read
| Parameter | In | Type | Description |
|---|---|---|---|
limitoptional | query | integer | 1 to 100. Defaults to 25. |
created_afteroptional | query | string | Created after this time, ISO 8601. |
created_beforeoptional | query | string | Created before this time. Pass the last order's created for the next page. |
statusoptional | query | string | e.g. new, in_review, shipped, cancelled. |
curl -X GET https://api.egful.store/api/v1/orders \ -H "X-API-Key: egk_test_..."
{
"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" }
]
}/api/v1/orders/:id/cancelCancel orderCancels 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
| Parameter | In | Type | Description |
|---|---|---|---|
idrequired | path | string | The order id returned by Create order. |
curl -X POST https://api.egful.store/api/v1/orders/ord_test123/cancel \ -H "X-API-Key: egk_test_..."
{
"object": "order",
"mode": "live",
"id": "API-9F2C1A",
"status": "cancelled",
"refunded": 21.59
}/api/v1/orders/:idRetrieve orderLooks up one order by id.
print_filesis the stored artwork, per line and position.shipping_addresscomes back on API orders.- A cancelled order also carries
reason,rejected_byandrejected_at. - Test key: a simulated order with no lines;
totalis null.
Scope orders.read
| Parameter | In | Type | Description |
|---|---|---|---|
idrequired | path | string | The order id returned by Create order. |
curl -X GET https://api.egful.store/api/v1/orders/ord_test123 \ -H "X-API-Key: egk_test_..."
{
"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" }
}/api/v1/stockCheck stockWhat we can make right now: available quantity per blank sku.
statusis one of in_stock, low, out_of_stock.availableis on-hand minus what is already committed. A print-method suffix (-EMB, -DTG, …) is ignored.
Scope products.read
| Parameter | In | Type | Description |
|---|---|---|---|
skuoptional | query | string | One blank. Omit for all (up to 1,000). |
curl -X GET https://api.egful.store/api/v1/stock \ -H "X-API-Key: egk_test_..."
{
"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" }
]
}/api/v1/balanceAccount balanceWhat is currently on your account.
- Negative means charges exceed funds.
awaiting_fundsis orders whose submit was refused for a short wallet. The next top-up submits them, oldest first, soavailableis what a new order can use.
Scope billing.read
curl -X GET https://api.egful.store/api/v1/balance \ -H "X-API-Key: egk_test_..."
{
"object": "balance",
"mode": "live",
"account": "8f3c…",
"balance": 90.80,
"awaiting_funds": { "orders": 1, "amount": 24.50 },
"available": 66.30,
"currency": "USD"
}/api/webhooksList webhooksYour registered webhook endpoints.
- Secrets are shown once, when the endpoint is created.
Scope webhooks.read
curl -X GET https://api.egful.store/api/webhooks \ -H "X-API-Key: egk_test_..."
[
{ "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" }
]/api/webhooksAdd a webhookRegisters an https endpoint to be told about order events.
- The signing secret is returned once. Store it.
- Each delivery carries
X-EG-EventandX-EG-Signature(sha256=<hex>), an HMAC-SHA256 of the raw body with that secret. - Up to ten endpoints per account.
Scope webhooks.write
| Parameter | In | Type | Description |
|---|---|---|---|
urlrequired | body | string | Public https URL. Redirects are not followed. |
eventsoptional | body | string[] | Any of order.received, order.status_changed, order.shipped, order.cancelled, order.payment_failed. Omit for all. |
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"
]
}'{
"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."
}/api/webhooks/:id/testSend a test deliverySends a sample event to one endpoint, signed like a real one.
- The payload carries
test: true.
Scope webhooks.write
| Parameter | In | Type | Description |
|---|---|---|---|
idrequired | path | integer | The webhook id from List webhooks. |
eventoptional | body | string | Defaults to order.shipped. |
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"
}'{
"ok": true,
"event": "order.shipped",
"url": "https://your-app.example.com/hooks/egful",
"status_code": 200,
"attempts": 1,
"error": null
}/api/webhooks/eventsList event namesThe events a webhook can subscribe to.
Scope any key
curl -X GET https://api.egful.store/api/webhooks/events \ -H "X-API-Key: egk_test_..."
{
"events": ["order.received", "order.status_changed", "order.shipped", "order.cancelled", "order.payment_failed"]
}/api/webhooks/:idRemove a webhookStops deliveries to one endpoint.
Scope webhooks.write
| Parameter | In | Type | Description |
|---|---|---|---|
idrequired | path | integer | The webhook id from List webhooks. |
curl -X DELETE https://api.egful.store/api/webhooks/1 \ -H "X-API-Key: egk_test_..."
{ "ok": true }/api/webhooks/:id/deliveriesDelivery attemptsThe last 100 delivery attempts for one endpoint.
- Deliveries retry three times and stop on a 4xx.
Scope webhooks.read
| Parameter | In | Type | Description |
|---|---|---|---|
idrequired | path | integer | The webhook id from List webhooks. |
curl -X GET https://api.egful.store/api/webhooks/1/deliveries \ -H "X-API-Key: egk_test_..."
[
{ "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.
| Event | Fires when |
|---|---|
| order.received | We accepted an order you pushed. |
| order.status_changed | It moved along the production pipeline. |
| order.shipped | Tracking exists. This is the one most integrations care about. |
| order.cancelled | It 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" }]
}| Status | Means | What to do |
|---|---|---|
400invalid_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. |
400unpriceable_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. |
401 | The key is missing, mistyped or revoked. | Send it in the X-API-Key header, exactly as copied from Settings → API keys. |
403insufficient_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. |
404 | No order with that id belongs to this key's account. | Use the `id` returned by Create order, not your own external_id. |
409 | The order is already in production and can no longer be cancelled. | Nothing to retry. Contact us if it must be stopped. |
429 | Too many requests for this key. | Wait the number of seconds in the Retry-After header, then retry. |
500internal_error | Something failed on our side. | Safe to retry reads. For Create order, retry with the same external_id so it cannot duplicate. |
501 | A 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 · 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 · 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 theX-API-Keyheader.3 · Ask it something
“Has order API-9F2C1A shipped?” or “What would 40 black tees in L cost?”
What it can do
| Tool | Does | Endpoint |
|---|---|---|
| list_products | List products | /api/v1/products |
| create_order | Create order · writes | /api/v1/orders |
| quote_order | Quote an order | /api/v1/orders/quote |
| cancel_order | Cancel order · writes | /api/v1/orders/:id/cancel |
| get_order | Retrieve order | /api/v1/orders/:id |
| check_stock | Check stock | /api/v1/stock |
| get_balance | Account 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.