Overview

This document for LLMs: llms.txt (~17K tokens)

T.REX ARMS makes custom holsters. Our holster lines, combined with the pistols, weapon lights, hand, barrel and color options we support, come to hundreds of thousands of possible configurations. Stocking them all isn't realistic, so we make every holster to order.

The partner program puts our holster configurator on your product pages and drop-ships the orders. The customer configures and buys on your site; we make the holster and ship it to them in our box. This API is everything that takes, from listing the products to returns.

What the customer sees

Your product page, with our configurator in an iframe. The iframe takes the place of the product gallery and description, everything beside your add-to-cart block, and is styled to match your site. Our only branding is a small T.REX skull in the corner of the main image. The review count opens our reviews in a popup over the configurator, and the warranty link opens our warranty page in a new tab. The price shown in your buy box comes from the iframe, through the selection_changed message.

In your cart, the configuration is one line: the holster and the attachments chosen with it, with a render of exactly what was configured, one price and one quantity.

See it working in the demo: a stand-in product page with the configurator in its iframe, a buy box and a cart driven by its messages, and a log of every message in both directions.

Process assumptions

  • Every holster is made to order. Each product and cart item carries its lead time: how soon it ships out.
  • We make, pack and ship every order straight to your customer, with a T.REX ARMS packing slip in the box listing the customer's configuration.
  • We ship to the address exactly as sent and don't verify it; your checkout already has. Our holsters ship anywhere, but some attachments made by others ship to the US only (Shipping restrictions).
  • Our prices are our storefront's. Every price you show or charge comes from us: the customer gets whatever public sale we're running, with color upcharges and their discounts handled correctly. A promo is our term for any discount, scheduled or unscheduled; it's the promo in promo_discounts and promo_explanation.
  • Coupons on your site are yours. We don't do anything with them: a discount you give at your checkout never reaches us, and the order we make carries our own prices.
  • Sales tax is yours. You're the merchant of record: you collect and remit the tax, and our orders carry none.
  • Shipping charges are yours. We bill you the cost of the shipping label. We use USPS Ground Advantage, and we recommend charging a flat rate for shipping. Upgradeable shipping is not available yet.
  • You own every conversation with the customer. We never email, call or message your customer. When we need something from them (a fit problem found in production, a carrier that can't deliver), we put the order on hold and get in touch with you, and you take it from there.
  • Until the first box ships, an order can change: a new address, lines deleted, or cancelled outright, even once we've started making it. We absorb the cost of a cancellation, as we do for our own customers: a cancelled order is never invoiced, however far production got. A configured holster can't be edited; the customer deletes the line and orders a new configuration (Changing an order).
  • Returns come back to us. You open a return (an RMA) for what the customer is sending back, and we receive the box at our warehouse. Accepted returns are credited toward your next invoice.
  • Our lifetime warranty applies. The configurator links to our warranty page, and a warranty or support contact is tied back to its order by your order id.

Products and attachments

Each of our holster lines is one product on your site, with one product page. GET /products is the live list.

Attachments are not products of their own. The customer chooses them inside the holster's configurator, on that holster's product page, and they're sold only as part of a holster configuration. No attachment gets its own product page, listing or search entry. In the cart and on the order, a holster and the attachments configured with it are one line, with one price and one quantity (Cart). Which attachments a holster offers is decided by its configurator, and can change without any change to this API.

Technical details

For the developers.

Guiding principles

  1. As much complexity as possible is handled by T.REX. Pistol and light compatibility, pricing, stock and shipping restrictions are all complicated, and flattening them into a simple attribute model doesn't work. So the configurator works them out, every price you show or charge comes from us, computed the way our own storefront computes it, and we check each cart against the shipping address for you (Shipping restrictions).
  2. That includes cart items. The customer's browser hands you only cart item refs, and a ref reads nothing without your bearer token. When you place an order, you send only cart item refs and an address. Line contents, quantities and prices come from our records.
  3. Speed. There are no rate limits on partner traffic, and we aim to answer every request in under 100ms of server time.

Data flow overview

Your systems and ours meet in two places: the customer's browser, where your product page and our configurator iframe share nothing but postMessage, and this API, between your server and ours.

  1. You sync our products (GET /products) and keep one product of your own for each.
  2. Each of those product pages loads our configurator in an iframe from the product's embed_url.
  3. The iframe gets fits, availability and prices from our servers directly.
  4. As the customer configures, the iframe reports the selection and its price to your page (selection_changed).
  5. When the customer clicks your "Add to cart", your page tells the iframe, with the quantity (add_to_cart_clicked).
  6. The iframe registers the configuration with us. We create the cart item at that quantity and return its cart_item_ref.
  7. The iframe hands the ref to your page (cart_item_created). This is the only place a cart item crosses from our side to yours.
  8. Your page sends the ref to your cart.
  9. Your server reads the cart item from us (GET /cart_items/{cart_item_ref}) to build its cart line, and validates the cart with us (POST /cart/validate) whenever it's viewed and, with the shipping address, immediately before creating the order.
  10. When the customer changes a quantity, you send it to us (PATCH /cart_items/{cart_item_ref}), and we refuse a quantity we can't supply.
  11. The customer checks out on your site.
  12. You create the order with the cart item refs, your own order id and line ids, and the address (POST /orders). We take it at once and create it moments later, and an order_created event says it's there. From then on, you name the order and its lines by your own ids, so you have none of ours to store.
  13. Ordering uses the cart items up: we delete them, so they can't be ordered again.
  14. Until the order ships, you can change the address, delete lines, put the order on hold, or cancel it. You can read the order at any time (GET /orders/{partner_order_id}).
  15. We post an event to your webhook when the order's status changes or a box ships.

Base URL and versioning

https://www.trex-arms.com/drop_manufacturing_api/v1

All paths below are relative to this base. Breaking changes get a new version prefix. Additive changes (new fields, new optional parameters, new error codes) ship under v1 without notice, so ignore any field you don't recognize.

Sandbox and live traffic use the same base URL; only the bearer token differs.

Authentication

Every endpoint takes a bearer token:

Authorization: Bearer <token>

We set you up with two accounts, one live and one sandbox. Each has its own bearer token and its own embed_key. A missing, unknown or revoked token gets 401.

  • Live or sandbox. Orders created with the sandbox token are never made, shipped or invoiced, and never reach our warehouse. In every other way they behave like live orders, so you can exercise the whole flow (products, the embed, cart items, orders, edits, holds, cancellation, webhooks, returns) against production data before going live. A sandbox order ships itself about five minutes after it's created (or, if it's on hold then, soon after it's released), in one box with a fake tracking number, and becomes Completed, so you can test the shipment webhook end to end.
  • An account sees only what it created. Cart items, orders and returns belong to the account that created them. Anything created by another account answers 404, as if it didn't exist. So the sandbox account can't see live orders, the live account can't see sandbox ones, and neither can see anything of another partner's.
  • The embed names the account by its embed_key. The key isn't a secret: it's in the embed_url your product pages load. Cart items created through an embed belong to the account the key names, and only that account's token can read them.

To rotate a token, we issue a new one for the same account. Everything the account created stays with it, and the old token keeps working for a grace period, so you can switch over without a coordinated cutover.

Requests and responses

  • JSON both ways, UTF-8, Content-Type: application/json.
  • Field names are snake_case and, wherever possible, match the column names in our own database.
  • *_ref fields are our integer ids. They're stable and safe to store.
  • Money is a string in USD with two decimals ("135.00"), never a JSON number. That matches our database exactly and avoids floating point.
  • Datetimes are ISO 8601 in UTC with a Z suffix ("2026-09-22T14:14:06Z"). Dates are YYYY-MM-DD.
  • Booleans are JSON booleans.

Errors

Any response other than a 2xx has an ErrorResponse body.

Status Meaning
400 The request is malformed: a missing or mistyped field. Sending it again unchanged won't help.
401 The bearer token is missing, unknown or revoked.
404 The cart item, order, line item or return doesn't exist, or belongs to another account. A cart item that has been ordered no longer exists.
409 The request is valid, but the state of what it names doesn't allow it: a quantity we can't supply right now, an order already shipped or cancelled, a status change we don't allow, or a return of something that hasn't shipped or has already come back.
418 I'm a teapot.
422 An address change on an order would send a line somewhere it can't ship (shipping_restricted). The error names the line.
500 Our fault. Safe to retry; creating an order is idempotent.

Products

The products we offer you. Most product information rarely changes, but prices, sales, review counts and lead times do, and these endpoints keep your listings current.

GET /products

Every product offered to you, sorted by name, as a ProductsResponse.

  • Sales apply per product, so promo_discounts differs from product to product.
  • Only public sales appear: what any visitor to our site gets.
Response
TypeScript
{products: Product[]}
StatusCodeMeaning
200Success.

GET /products/{product_ref}

One product.

Response
TypeScript
Product
StatusCodeMeaning
200Success.
404not_foundThe product isn't a part of the program.

The configurator iframe

The configurator section of each product page is an iframe loading the product's embed_url. It's our own configurator page without the site around it: no header, no cart and no price. It's styled to sit on your page, in a skin we set up to match your site. It offers only what can be ordered right now: a pistol or light fit we don't make yet, or a color that's out of stock or not yet released, isn't shown at all. The iframe talks to our servers directly and reports its state to your page with window.postMessage().

In order, as the diagram numbers them:

  1. The iframe gets the product from our servers: its fits, availability and prices.
  2. Our servers answer with the product.
  3. The iframe posts ready to your page, with the product_ref.
  4. The iframe posts resize with its height, and again whenever the height changes; size the frame to it.
  5. As the customer configures, the iframe posts selection_changed: whether the selection is complete, its state, its price and lead time, and the most we can supply. Show the price, cap your quantity selector, and keep the state.
  6. When the customer clicks your "Add to cart", outside the iframe, your page posts add_to_cart_clicked with the quantity.
  7. The iframe sends our servers the selection, the quantity and a snapshot of the configuration, and we create the cart item.
  8. Our servers answer with the cart item.
  9. The iframe posts cart_item_created to your page with the cart_item_ref and the cart item, or add_to_cart_failed with a code and a message.
  10. Your page sends the cart_item_ref to your server with its add-to-cart request. Your server reads the cart item from us (GET /cart_items/{cart_item_ref}) and builds its cart line from it, trusting nothing else that came from the browser.

The demo hosts the iframe from a site other than ours, the way your product page does, and logs every message in both directions as it happens. It can also simulate our site erroring (500) or being unreachable (522), to show how your page should handle it. Its add to cart is simulated: it shows the cart item you'd get, but stores nothing.

The sample is a complete product page in plain JavaScript, with no framework, to start from: it frames the configurator, sizes it, drives a buy box, adds to the cart, and takes the frame down when the configurator can't load. It shows its own code under it, so copy it from there.

URL

https://www.trex-arms.com/embed/product/{product_ref}?partner=<embed_key>[&state=<configurator state>]
  • partner is your account's embed_key; the embed_url from GET /products already carries it. It decides which account owns the cart items the embed creates (the sandbox account's embed creates sandbox cart items), and which sites may frame the iframe and receive its messages: only the origins registered for that account.
  • state restores an earlier selection. Pass back, unchanged, the state string from the last selection_changed message.

Messages from the iframe to your page

Every message is a JSON object with a type and source: "trex", one of IframeToContainerMessage.

If the iframe says unavailable, or hasn't said ready within about 15 seconds (it may never load at all), take it off the page, disable your "Add to cart", and show a short message instead of a blank box. The demo and the sample both do this.

Messages from your page to the iframe

Post a ContainerToIframeMessage with iframe.contentWindow.postMessage(message, 'https://www.trex-arms.com').

The quantity is checked against stock the same way a quantity change is (PATCH /cart_items/{cart_item_ref}). Cap your quantity selector at the latest selection_changed's max_available_quantity and a refusal will be rare, though stock can still move between the two. If we can't supply the quantity, the iframe answers add_to_cart_failed with code unavailable and available_quantity, the most we can supply right now. It then sends a selection_changed whose max_available_quantity is that number, so a selector capped from selection_changed catches up on its own.

When adding fails for any reason, nothing is added: show the message from add_to_cart_failed and keep the customer on the page. When it succeeds, send the cart_item_ref from cart_item_created to your server (Cart).

Every add to cart creates a new cart item. Adding the same configuration twice gives two cart items, not one with a larger quantity.

Cart

A cart item is one configuration: a holster and the attachments configured with it. To you it's one cart line, with one price, one quantity, one discount and one image. The holster and each attachment are listed inside it (lines) so you can show the customer what they're buying, but they have no price or quantity of their own to manage. We keep our own record of every cart item in your carts, quantity included, and its cart_item_ref is your handle to it. The iframe hands your page the ref when the customer adds to the cart (The configurator iframe), and your server reads the cart item from us (GET /cart_items/{cart_item_ref}) to build its cart line, trusting nothing else that came from the browser.

Quantity lives on our side: when the customer changes it, send the change to us, and apply it in your cart only once we accept it. Removing a line from your cart needs no call to us: a cart item you never order is simply never ordered. Cart items never expire; ordering is what uses them up.

A cart item's contents are recomputed on every read, so its price, availability and lead time are current, not what they were when it was created.

GET /cart_items/{cart_item_ref}

One cart item.

Response
TypeScript
CartItem
StatusCodeMeaning
200Success.
404not_foundNo such cart item, it's another account's, or it's been ordered.

PATCH /cart_items/{cart_item_ref}

Sets a cart item's quantity.

We refuse a quantity we can't supply right now, so your cart never holds more than we can fill. A refused change leaves the quantity as it was. Lowering a quantity is always accepted, since it never asks for more than before. Cap your quantity selector at max_available_quantity and these refusals will be rare.

Stock can still run short after a quantity is accepted, when other orders take it. That shows up as a quantity_exceeded availability issue (POST /cart/validate).

Request
TypeScript
{// A positive whole number.quantity: number}
Response
TypeScript
CartItem
StatusCodeMeaning
200Success.
400invalid_requestquantity isn't a positive whole number.
404not_foundNo such cart item, it's another account's, or it's been ordered.
409cart_item_unavailableThe cart item is unavailable (available: false) for a reason other than its quantity, so its quantity can't go up.
409quantity_unavailableMore than max_available_quantity; the error's available_quantity is the most we can supply.

POST /cart/validate

A cart item can become unavailable after it's in a cart. Usually a color has gone out of stock. Rarely, we've found a problem with how a model or light fits a holster and disabled that combination while we fix it. Prices change too, when a sale starts or ends, and nothing locks them.

So validate the cart whenever it's viewed, and again with the shipping address immediately before creating the order. Send all of the address or none of it; it's checked by the same rules as an order's.

Reads every cart item as GET /cart_items/{cart_item_ref} does, and says whether an order of them, to that address, would go through as is. If it wouldn't, problems names each cart item that stops it and why, so you can tell the customer while they can still act:

  • cart_item_unavailable: the cart item can't be ordered right now (its availability_issue says why).
  • quantity_exceeded: stock ran short; lowering the quantity to the issue's available_quantity or below makes it orderable again.
  • shipping_restricted: the cart item can't ship to that address.
  • not_found: no such cart item, or it's already been ordered.

We take an order regardless (POST /orders), so this check is yours. A cart item whose unit_price_usd has changed has a new price; show it. The order is charged the price current when it's created, so the last read before you create it is what to charge the customer.

Request
TypeScript
Partial<ShippingAddress> & {// Up to 100 per request.cart_item_refs: number[]}
Response
TypeScript
{// True when an order of these cart items, to this address, would go through as is.  Without an address, nothing is checked against one.orderable: boolean// In the order requested.  A ref that doesn't exist or belongs to another account is an error entry rather than failing the whole request.cart_items: (CartItem | { cart_item_ref: number, error: 'not_found' })[]// Empty when orderable.problems: CartProblem[]}
StatusCodeMeaning
200Success.
400invalid_requestA malformed body, more than 100 refs, or a partial or malformed address.

Orders

POST /orders

An order always goes through. We take it at our edge, so it's accepted even if the rest of our site is slow or down: we check only your bearer token (401) and the request's shape (400), and answer 202 at once. The order is created moments later, and we post an order_created event (Webhooks); until then GET /orders/{partner_order_id} answers 404.

It's idempotent on partner_order_id: sending the same order twice creates it once. So when you're unsure whether a request got through (a timeout, a 500), send it again.

Nothing about the order is refused after that, since your validation just before creating it (POST /cart/validate) has already had its say. A cart item that has gone unavailable is still ordered, and made when it can be. If something can't be resolved (a cart item that doesn't exist, belongs to another account or was already ordered, or a line that can't ship to the address), the order is still created, without any line we couldn't build, and put On Hold, and we get in touch with you to sort it out. Nothing holds stock while the customer checks out.

The request carries no prices, quantities, names, attributes or SKUs. We build the order from our own record of each cart item, at its quantity on our side and our own price at that moment. The order shows the prices we charged, so you can reconcile them against what you collected. The order has no sales tax and no shipping charge.

Ordering uses the cart items up: once the order is created they're deleted, and reading one again answers 404. The order's line_items come in the order sent, each with your partner_line_item_id.

Request
TypeScript
ShippingAddress & Contact & {// Your own order id.  Unique across your orders; the idempotency key for this request.  Up to 20 characters.partner_order_id: string// One entry per cart item.line_items: {// Your own id for the order line.  Unique within the order; every later call and every shipment names the line by it.  Up to 50 characters.partner_line_item_id: stringcart_item_ref: number}[]// A note from the customer to print on the packing slip, if you collect one.note_from_customer?: string | null}
Response
TypeScript
{partner_order_id: string// Quote this when reporting a problem to us.request_id: string}
StatusCodeMeaning
202Success; the order is created moments later. A repeat with the same partner_order_id is answered the same, and changes nothing.
400invalid_requestA malformed body or address, a repeated line id or cart item, or a line item with any field but its id and cart item ref (a quantity included).

GET /orders/{partner_order_id}

The order as it stands now, looked up by your order id.

Response
TypeScript
Order
StatusCodeMeaning
200Success.
404not_foundNo such order of yours, or it hasn't been created yet.

Shipping restrictions

Some parts can't ship everywhere: some attachments made by others ship to the US only, for example. POST /cart/validate checks the cart against the shipping address and names any cart item that can't go there; that's the place to catch it, before the customer pays. An address change on an existing order is checked the same way (PATCH /orders/{partner_order_id}). Our holsters themselves ship anywhere.

Changing an order

When a customer calls to change an order, you can change the shipping address, delete line items, put the order on hold or release it, or cancel it. All of these work only until the first box ships. An order on hold still accepts address changes and line deletions.

A line can't be edited. The valid edits to a configured holster are hard to describe and hard to pass between our systems, so the customer deletes the line and orders a new configuration.

PATCH /orders/{partner_order_id}

Changes the order's contact and shipping details (send any of them; the rest are unchanged). A new address is stored as sent and checked against the lines' shipping restrictions. The rules for a US address apply to the address as it will be, so changing only shipping_country to US is refused unless the state and postal code already on the order are a USPS state and a ZIP.

Request
TypeScript
// Any of the contact and address fields; the ones you leave out are unchanged.Partial<ShippingAddress & Contact>
Response
TypeScript
Order
StatusCodeMeaning
200Success.
400invalid_requestA field that can't be changed, a malformed address, or nothing to change.
404not_foundNo such order of yours.
409order_already_shippedA box has shipped.
409order_already_cancelledThe order is cancelled.
422shipping_restrictedA line can't ship to the new address; partner_line_item_id names it.

DELETE /orders/{partner_order_id}/line_items/{partner_line_item_id}

Removes the line: the holster and its attachments together. The line drops out of line_items, and the order's total_usd is recomputed.

Response
TypeScript
Order
StatusCodeMeaning
200Success.
404not_foundNo such order of yours, or no such line on it (a repeat of this request included).
409order_already_shippedA box has shipped.
409order_already_cancelledThe order is cancelled.
409order_edit_not_allowedIt's the order's last line; cancel the order instead.

POST /orders/{partner_order_id}/status

Puts the order on hold or releases it. The only allowed changes are Processing to On Hold and back. Setting the status the order already has does nothing.

While an order is On Hold we don't pick, make or ship it, and its promised ship date moves out by the time it spent on hold. Use it while the customer decides whether to change or cancel, or while you confirm something with them. Releasing it puts the order back in the production queue.

We put orders On Hold too, for problems only the customer can resolve, and get in touch with you about them. We never contact the customer, so you do. Once the customer answers, fix the order (change the address, delete a line or cancel) or release it.

Request
TypeScript
{sales_order_status: 'Processing' | 'On Hold'}
Response
TypeScript
Order
StatusCodeMeaning
200Success, also when the order already had that status.
400invalid_requestsales_order_status isn't Processing or On Hold.
404not_foundNo such order of yours.
409order_already_shippedA box has shipped.
409order_already_cancelledThe order is cancelled.
409status_transition_not_allowedAnything but Processing to On Hold and back.

POST /orders/{partner_order_id}/cancel

Any order that hasn't shipped can be cancelled, even if we've started making it (Process assumptions).

Response
TypeScript
Order
StatusCodeMeaning
200Success, also when the order was already cancelled.
404not_foundNo such order of yours.
409order_already_shippedA box has shipped; the recourse is a return.
409order_edit_not_allowedThe order can't be cancelled right now.

Webhooks

When something happens to an order, we POST an event to a URL you give us, with a bearer token you give us. Webhooks are optional: an account with no webhook URL is sent nothing. Every event describes state that GET /orders/{partner_order_id} also returns, so you can take the webhook, poll, or both, and polling is always enough to catch up on a missed event.

POST {your_webhook_url}

  • A JSON body with Content-Type: application/json and Authorization: Bearer <token>, with the token you give us.
  • At least once. A 2xx within 10 seconds acknowledges the event. Anything else, a timeout included, is retried a minute or so later, a limited number of times; if an event never gets through, GET /orders catches you up. A retry carries the same event_id, so de-duplicate on it.
  • Events can arrive out of order, an order's included. When two events disagree, GET /orders is the truth.
  • Each account has its own webhook URL, so sandbox events can go to your test system. The sandbox account gets the same events for its orders, the simulated shipment included.
  • shipment_shipped comes once per box, at its first label: an order that ships in two boxes sends two. A label voided and replaced sends no second event; reading the order shows the replacement.
Request
TypeScript
{event_id: stringevent_datetime: stringsales_order_ref: numberpartner_order_id: string} & (| {// A box left our warehouse.event_type: 'shipment_shipped'shipment: Shipment// True when this shipment completes the order.order_complete: boolean}| {// The order went On Hold or came off it, on either side, or was cancelled.event_type: 'order_status_changed'sales_order_status: 'Processing' | 'On Hold' | 'Cancelled'}| {// An order you sent has been created, "On Hold" if something needs sorting out with you.event_type: 'order_created'sales_order_status: 'Processing' | 'On Hold'})

Returns

When a customer wants to send something back, open a return (an RMA) for it. It starts initiated. When the box reaches us we receive it, recording what actually came back, and it becomes received. Accepted returns are credited toward your next invoice (Process assumptions).

POST /orders/{partner_order_id}/returns

Opens a return.

Only what has shipped can come back, and a line can't come back more times than it was ordered, counting every earlier return.

Every return you open is also listed on the order, in returns. A sandbox return stays initiated, since no box ever arrives.

Request
TypeScript
{// Your own id for the return.  Unique across all your returns, as your order ids are; the idempotency key for this request.  Up to 50 characters.partner_return_id: string// Why it's coming back.return_reason_ref: ReturnReason// Anything the customer said about it.  Up to 1000 characters.note?: string | null// What's coming back.  A line comes back whole: the holster and its attachments together.line_items: ReturnLine[]}
Response
TypeScript
Return & {// True when partner_return_id was already used on this order, and this is that earlier return.deduplicated: boolean}
StatusCodeMeaning
201Success: a new return.
200Success: a repeat on the same order, answered with that earlier return and deduplicated: true.
400invalid_requestA malformed body, or a return_reason_ref that isn't a ReturnReason.
404not_foundNo such order of yours, or no such line on it.
409order_not_shippedNothing on the order has shipped, or this line hasn't.
409return_quantity_exceededMore than is left to return; available_quantity says how many are.
409partner_return_id_in_useThe id is already a return on another of your orders.