Dropship API
Place production orders programmatically from your own store. We make the parts and ship them directly to your customers.
Using Shopify? Start here — no code needed
Install the RMFG app from the Shopify App Store, connect it to your dropship account, and publish your approved parts as products in one click. The app wires up ordering and fulfillment for you — when a customer buys, we make the part, ship it to them, and push tracking back to Shopify automatically. You never touch an API key, SKU, or webhook by hand.
- Install the RMFG app and open it in your Shopify admin.
- Paste your API key on the app's Settings tab (from your dropship dashboard) and click Test connection.
- On the Catalog tab, set a retail price and click Publish for each approved part.
The reference below is for building your own integration (Squarespace, a custom storefront, etc.) — you only need it if you're not using the Shopify app.
Authentication
All endpoints live under /v1 on the RMFG API. Authenticate every request with your API key, sent in the rmfg_key header. Once your dropship account is approved, generate your key — along with managing your account, card on file, and webhooks — from the dropship dashboard.
curl https://<api-base>/v1/items \
-H "rmfg_key: <your API key>"Every error is JSON with a stable machine-readable code:
{"error": {"code": "unknown_sku", "message": "No dropship item with sku 'BRKT-99'."}}| Status | Code | When |
|---|---|---|
| 401 | missing_api_key / invalid_api_key | rmfg_key header is missing or not recognized. |
| 403 | dropship_account_not_approved | Your account is not approved (requested, suspended, or denied). |
GET /v1/items
Lists your dropship items — both approved (orderable) and still under review. Unit prices are locked when we approve an item and are integer cents.
{
"items": [
{
"id": "6e1f9a2b-7c3d-4e5f-8a9b-0c1d2e3f4a5b",
"sku": "BRKT-01",
"name": "Mounting Bracket",
"status": "approved",
"unit_price_cents": 1850,
"kind": "part",
"part": {
"id": "aB3dE5fG7hJ9",
"material": "aluminum",
"material_type": "5052-H32",
"thickness": 3.175
},
"assembly": null
},
{
"id": "3c2b1a0f-9e8d-4c7b-a6f5-e4d3c2b1a0f9",
"sku": "FRAME-03",
"name": "Welded Frame",
"status": "approved",
"unit_price_cents": 24900,
"kind": "assembly",
"part": null,
"assembly": { "id": "xY7zW9vU3tR1", "part_count": 4, "has_welds": true, "file_name": "frame.step" }
},
{
"id": "0d9c8b7a-6e5f-4d3c-2b1a-0e9f8d7c6b5a",
"sku": "PLATE-02",
"name": "Base Plate",
"status": "requested",
"unit_price_cents": null,
"kind": "part",
"part": { "id": "kL2mN4pQ6rS8", "material": "steel", "material_type": "cold rolled", "thickness": 1.897 },
"assembly": null
}
]
}Items are single parts (kind: "part") or assemblies (kind: "assembly" — multi-part units, optionally welded, reproduced from your sample order). Ordering is identical either way: one SKU, one locked unit price per part or assembly unit.
Only status: "approved" items can be ordered. Request new items from any part on one of your past orders (order detail page → "Request for dropship").
POST /v1/orders
Creates a production order and charges your card on file off-session for the parts (unit_price_cents × quantity). Shipping is charged separately to the same card at fulfillment — after we ship your order, once the real parcel and label cost are known. Only US destinations are supported.
If the shipping charge fails, we still ship your order. You're notified via the order.payment_failed webhook event, and the outstanding balance can be settled from the dropship dashboard — outstanding charges are also retried automatically when you replace your card.
POST /v1/orders
rmfg_key: <your API key>
Content-Type: application/json
{
"sku": "BRKT-01",
"quantity": 2,
"external_ref": "shopify-1001",
"retail_unit_price_cents": 2999,
"ship_to": {
"name": "Jane Doe",
"street1": "500 Market St",
"street2": null,
"city": "San Francisco",
"state": "CA",
"zip": "94107",
"country": "US",
"phone": "415-555-0100",
"email": "jane@example.com"
}
}{
"order": {
"id": "k3x9mQ2pL7vD",
"status": "paid",
"external_ref": "shopify-1001",
"sku": "BRKT-01",
"quantity": 2,
"parts_total_cents": 3700,
"shipping": { "status": "charged_at_fulfillment" },
"created_at": "2026-07-05T12:00:00+00:00"
}
}Idempotency
Pass your own order id (e.g. the Shopify order number) as external_ref. If we already have an order with that external_ref on your account, we return the existing order with status 200 instead of 201 — no duplicate order, no double charge. Safe to retry on timeouts.
Contact phone (strongly recommended)
Include ship_to.phonewhenever you have it: carriers require a contact number on the shipment; if omitted we use your account's contact phone.
Retail price (optional)
You can optionally tell us what your customer paid, per unit, in retail_unit_price_cents (integer cents). You never have to send it — our store integrations (like Shopify) fill it in automatically from the order.
It never affects your bill: you always pay the locked wholesale price. We only use it to unlock better pricing for you over time — for example, spotting parts you sell often so we can offer volume discounts on the next production run.
Errors
| Status | Code | When |
|---|---|---|
| 402 | card_declined | The off-session charge was declined. No order is created. The message contains Stripe's decline reason. |
| 402 | payment_not_completed | The payment could not complete off-session (e.g. the card requires authentication). No order is created. |
| 403 | item_not_approved | The SKU exists but is not approved for ordering. |
| 404 | unknown_sku | No dropship item with that SKU on your account. |
| 409 | no_payment_method | No card on file. Add one in the dropship dashboard. |
| 422 | unsupported_country | ship_to.country must be "US". |
GET /v1/orders and /v1/orders/{id}
GET /v1/orders lists your API orders, filterable with ?external_ref=shopify-1001. GET /v1/orders/{id} returns one order including tracking (once shipped) and the charge breakdown.
{
"order": {
"id": "k3x9mQ2pL7vD",
"status": "shipped",
"external_ref": "shopify-1001",
"sku": "BRKT-01",
"quantity": 2,
"parts_total_cents": 3700,
"shipping": { "status": "charged_at_fulfillment" },
"tracking": {
"carrier": "FedEx",
"number": "781234567890",
"url": "https://www.fedex.com/fedextrack/?trknbr=781234567890"
},
"charges": [
{ "kind": "parts", "amount_cents": 3700, "status": "succeeded", "stripe_payment_intent_id": "pi_...", "created_at": "2026-07-05T12:00:00+00:00" },
{ "kind": "shipping", "amount_cents": 1450, "status": "succeeded", "stripe_payment_intent_id": "pi_...", "created_at": "2026-07-08T16:20:00+00:00" }
],
"created_at": "2026-07-05T12:00:00+00:00"
}
}Webhook endpoints
Manage endpoints via the API or the dashboard. Available events: order.created, order.in_review, order.in_production, order.shipped, order.delivered, order.cancelled, order.payment_failed.
GET /v1/webhook-endpoints— list active endpoints (secrets are never returned here)POST /v1/webhook-endpointswith{"url": "...", "events": ["order.shipped"]}(events optional — defaults to all events)DELETE /v1/webhook-endpoints/{id}— deactivates the endpoint, returns 204
{
"id": "3c2b1a0e-9f8d-4c6b-a5e4-d3c2b1a0e9f8",
"url": "https://yourstore.com/webhooks/rmfg",
"events": ["order.created", "order.in_review", "order.in_production", "order.shipped", "order.delivered", "order.cancelled", "order.payment_failed"],
"active": true,
"created_at": "2026-07-05T12:00:00+00:00",
"secret": "whsec_2f9d1c4b8a6e3f7d0c5b9a8e7f6d5c4b3a2e1f0d9c8b7a6e5f4d3c2b1a0e9f8d"
}The whsec_ secret is returned only in this response. Store it immediately — it cannot be retrieved again.
Webhook delivery & signature verification
We POST each event to your endpoint with a 10 second timeout. Any 2xx response marks it delivered; otherwise we retry with backoff (1m, 5m, 30m, 2h, 8h — 6 attempts total over roughly 10.5 hours). Deliveries can arrive out of order and more than once — use the delivery id to de-duplicate.
Content-Type: application/json
X-RMFG-Event: order.shipped
X-RMFG-Timestamp: 1751712000
X-RMFG-Signature: v1=fb8156e2ce94d2c1ccb20ef218633f689cf4a10aff0cb775a3a1d8b04ed764aa{
"id": "8f14e45f-ceea-4f30-aa8f-08bd10e2f011",
"type": "order.shipped",
"created_at": "2026-07-05T12:00:00+00:00",
"data": {
"order": {
"id": "k3x9mQ2pL7vD",
"external_ref": "shopify-1001",
"status": "shipped",
"sku": "BRKT-01",
"quantity": 2,
"tracking": {
"carrier": "FedEx",
"number": "781234567890",
"url": "https://www.fedex.com/fedextrack/?trknbr=781234567890"
},
"shipping_cost_cents": 1450
}
}
}tracking and shipping_cost_cents are null until the order ships.
order.payment_failed fires when a charge to your card fails — the order still ships. Update your card in the dropship dashboard (which retries outstanding charges automatically) or retry them from there directly. Its data carries the order and the failed charge:
{
"id": "c1a2b3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"type": "order.payment_failed",
"created_at": "2026-07-08T16:20:00+00:00",
"data": {
"order": {
"id": "k3x9mQ2pL7vD",
"external_ref": "shopify-1001",
"status": "shipped"
},
"charge": {
"kind": "shipping",
"amount_cents": 1450,
"error": "Your card was declined."
}
}
}Verifying signatures
The signature is HMAC-SHA256(secret, timestamp + "." + rawBody) hex-encoded, sent as X-RMFG-Signature: v1=<hex>. Sign the raw request body bytes exactly as received — do not re-serialize the parsed JSON, since key order and whitespace would change the digest. Reject deliveries whose timestamp is more than a few minutes old to prevent replays.
const crypto = require("crypto");
// rawBody must be the exact request body string/bytes, not re-serialized JSON.
function verifyRmfgWebhook(secret, headers, rawBody) {
const timestamp = headers["x-rmfg-timestamp"];
const signature = headers["x-rmfg-signature"]; // "v1=<hex>"
if (!timestamp || !signature || !signature.startsWith("v1=")) return false;
// Reject stale deliveries (replay protection).
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const provided = signature.slice("v1=".length);
const expectedBuf = Buffer.from(expected, "hex");
const providedBuf = Buffer.from(provided, "hex");
return (
expectedBuf.length === providedBuf.length &&
crypto.timingSafeEqual(expectedBuf, providedBuf)
);
}
// Express example — capture the raw body before JSON parsing:
// app.post("/webhooks/rmfg", express.raw({ type: "application/json" }), (req, res) => {
// if (!verifyRmfgWebhook(process.env.RMFG_WEBHOOK_SECRET, req.headers, req.body.toString("utf8"))) {
// return res.status(400).send("bad signature");
// }
// const event = JSON.parse(req.body);
// // handle event.type / event.data.order ...
// res.sendStatus(200);
// });Worked example
With secret whsec_2f9d1c4b8a6e3f7d0c5b9a8e7f6d5c4b3a2e1f0d9c8b7a6e5f4d3c2b1a0e9f8d, timestamp 1751712000, and this exact raw body (one line, no spaces):
{"id":"8f14e45f-ceea-4f30-aa8f-08bd10e2f011","type":"order.shipped","created_at":"2026-07-05T12:00:00+00:00","data":{"order":{"id":"k3x9mQ2pL7vD","external_ref":"shopify-1001","status":"shipped","sku":"BRKT-01","quantity":2,"tracking":{"carrier":"FedEx","number":"781234567890","url":"https://www.fedex.com/fedextrack/?trknbr=781234567890"},"shipping_cost_cents":1450}}}the string to sign is 1751712000.{...body...} and the resulting header is:
X-RMFG-Signature: v1=fb8156e2ce94d2c1ccb20ef218633f689cf4a10aff0cb775a3a1d8b04ed764aa