Overview
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
promoinpromo_discountsandpromo_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
- 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).
- 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.
- 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.
- You sync our products (
GET /products) and keep one product of your own for each. - Each of those product pages loads our configurator in an iframe from the product's
embed_url. - The iframe gets fits, availability and prices from our servers directly.
- As the customer configures, the iframe reports the selection and its price to your page (
selection_changed). - When the customer clicks your "Add to cart", your page tells the iframe, with the quantity (
add_to_cart_clicked). - The iframe registers the configuration with us. We create the cart item at that quantity and return its
cart_item_ref. - 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. - Your page sends the ref to your cart.
- 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. - 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. - The customer checks out on your site.
- 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 anorder_createdevent 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. - Ordering uses the cart items up: we delete them, so they can't be ordered again.
- 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}). - 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 theembed_urlyour 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_caseand, wherever possible, match the column names in our own database. *_reffields 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
Zsuffix ("2026-09-22T14:14:06Z"). Dates areYYYY-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_discountsdiffers from product to product. - Only public sales appear: what any visitor to our site gets.
{products: Product[]}| Status | Code | Meaning |
|---|---|---|
200 | Success. |
GET / products/ {product_ref}
One product.
Product| Status | Code | Meaning |
|---|---|---|
200 | Success. | |
404 | not_found | The 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:
- The iframe gets the product from our servers: its fits, availability and prices.
- Our servers answer with the product.
- The iframe posts
readyto your page, with theproduct_ref. - The iframe posts
resizewith its height, and again whenever the height changes; size the frame to it. - As the customer configures, the iframe posts
selection_changed: whether the selection is complete, itsstate, its price and lead time, and the most we can supply. Show the price, cap your quantity selector, and keep thestate. - When the customer clicks your "Add to cart", outside the iframe, your page posts
add_to_cart_clickedwith the quantity. - The iframe sends our servers the selection, the quantity and a snapshot of the configuration, and we create the cart item.
- Our servers answer with the cart item.
- The iframe posts
cart_item_createdto your page with thecart_item_refand the cart item, oradd_to_cart_failedwith a code and a message. - Your page sends the
cart_item_refto 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>]
partneris your account'sembed_key; theembed_urlfromGET /productsalready 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.staterestores an earlier selection. Pass back, unchanged, thestatestring from the lastselection_changedmessage.
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.
CartItem| Status | Code | Meaning |
|---|---|---|
200 | Success. | |
404 | not_found | No 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).
{// A positive whole number.quantity: number}CartItem| Status | Code | Meaning |
|---|---|---|
200 | Success. | |
400 | invalid_request | quantity isn't a positive whole number. |
404 | not_found | No such cart item, it's another account's, or it's been ordered. |
409 | cart_item_unavailable | The cart item is unavailable (available: false) for a reason other than its quantity, so its quantity can't go up. |
409 | quantity_unavailable | More 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 (itsavailability_issuesays why).quantity_exceeded: stock ran short; lowering the quantity to the issue'savailable_quantityor 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.
Partial<ShippingAddress> & {// Up to 100 per request.cart_item_refs: number[]}{// 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[]}| Status | Code | Meaning |
|---|---|---|
200 | Success. | |
400 | invalid_request | A 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.
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}{partner_order_id: string// Quote this when reporting a problem to us.request_id: string}| Status | Code | Meaning |
|---|---|---|
202 | Success; the order is created moments later. A repeat with the same partner_order_id is answered the same, and changes nothing. | |
400 | invalid_request | A 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.
Order| Status | Code | Meaning |
|---|---|---|
200 | Success. | |
404 | not_found | No 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.
// Any of the contact and address fields; the ones you leave out are unchanged.Partial<ShippingAddress & Contact>Order| Status | Code | Meaning |
|---|---|---|
200 | Success. | |
400 | invalid_request | A field that can't be changed, a malformed address, or nothing to change. |
404 | not_found | No such order of yours. |
409 | order_already_shipped | A box has shipped. |
409 | order_already_cancelled | The order is cancelled. |
422 | shipping_restricted | A 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.
Order| Status | Code | Meaning |
|---|---|---|
200 | Success. | |
404 | not_found | No such order of yours, or no such line on it (a repeat of this request included). |
409 | order_already_shipped | A box has shipped. |
409 | order_already_cancelled | The order is cancelled. |
409 | order_edit_not_allowed | It'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.
{sales_order_status: 'Processing' | 'On Hold'}Order| Status | Code | Meaning |
|---|---|---|
200 | Success, also when the order already had that status. | |
400 | invalid_request | sales_order_status isn't Processing or On Hold. |
404 | not_found | No such order of yours. |
409 | order_already_shipped | A box has shipped. |
409 | order_already_cancelled | The order is cancelled. |
409 | status_transition_not_allowed | Anything 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).
Order| Status | Code | Meaning |
|---|---|---|
200 | Success, also when the order was already cancelled. | |
404 | not_found | No such order of yours. |
409 | order_already_shipped | A box has shipped; the recourse is a return. |
409 | order_edit_not_allowed | The 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/jsonandAuthorization: Bearer <token>, with the token you give us. - At least once. A
2xxwithin 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 /orderscatches you up. A retry carries the sameevent_id, so de-duplicate on it. - Events can arrive out of order, an order's included. When two events disagree,
GET /ordersis 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_shippedcomes 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.
{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.
{// 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[]}Return & {// True when partner_return_id was already used on this order, and this is that earlier return.deduplicated: boolean}| Status | Code | Meaning |
|---|---|---|
201 | Success: a new return. | |
200 | Success: a repeat on the same order, answered with that earlier return and deduplicated: true. | |
400 | invalid_request | A malformed body, or a return_reason_ref that isn't a ReturnReason. |
404 | not_found | No such order of yours, or no such line on it. |
409 | order_not_shipped | Nothing on the order has shipped, or this line hasn't. |
409 | return_quantity_exceeded | More than is left to return; available_quantity says how many are. |
409 | partner_return_id_in_use | The id is already a return on another of your orders. |