15% summer discount applied automatically at checkoutFree USA shipping over $100Free worldwide shipping over $200Credit/debit checkout disabled for updates
Sign in 0
Partner API v1

Order intake, status and tracking, by API and webhook.

Base URL https://prymalab.net/wp-json/prymalab/v1/. JSON in, JSON out. Authenticate with your API key from the partner dashboard.

Authentication

Create a key under Partner dashboard → API & webhooks. Send it on every request as a bearer token. Keys start with plk_ and can be revoked at any time.

curl https://prymalab.net/wp-json/prymalab/v1/me \
  -H "Authorization: Bearer plk_your_key_here"
{ "partner_id": 12, "company": "Acme Research", "brand": "Acme", "platform": "woocommerce",
  "webhook_url": "https://acme.com/hooks/prymalab", "statuses": ["received","processing","fulfilled","complete","on_hold","cancelled"] }

Create an order

POST /orders. Idempotent on external_id: sending the same ID twice returns the existing order with "duplicate": true and HTTP 200 instead of 201.

curl -X POST https://prymalab.net/wp-json/prymalab/v1/orders \
  -H "Authorization: Bearer plk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "1042",
    "order_number": "#1042",
    "customer": { "name": "Jordan Lee", "email": "jordan@example.com", "phone": "+1 555 010 0100" },
    "ship_to": { "name": "Jordan Lee", "address_1": "12 Main St", "address_2": "Apt 4",
                 "city": "Lexington", "state": "KY", "postcode": "40507", "country": "US" },
    "items": [
      { "sku": "BC5",  "name": "BPC-157 5mg",   "quantity": 2, "price": 34.99 },
      { "sku": "TB5",  "name": "TB-500 5mg",    "quantity": 1, "price": 49.99 }
    ],
    "shipping_method": "USPS Priority",
    "notes": "Leave at front desk"
  }'

Response 201 Created:

{ "duplicate": false,
  "order": { "id": 318, "external_id": "1042", "order_number": "#1042", "status": "received",
             "status_label": "Received", "carrier": "", "tracking_number": "", "tracking_url": "",
             "items": [ { "sku": "BC5", "name": "BPC-157 5mg", "quantity": 2, "lot": "" }, ... ],
             "shipped_at": null, "updated_at": "2026-09-03 14:02:11" } }
FieldRequiredNotes
external_idyesYour order ID. Used for de-duplication and shown in your dashboard.
order_numbernoDisplay number if different from the ID.
customernoname, email, phone. Used for the packing slip only.
ship_toyesaddress_1, city, state, postcode required. country defaults to US.
items[]yessku (our catalog number, preferred) or name; quantity; price (your retail, optional, for your records).
shipping_methodnoFree text; we map it to a carrier service.
notesnoShown to the picker.

List and read orders

GET /orders?status=fulfilled&since=2026-09-01&page=1&per_page=50
GET /orders/318

The single-order response adds an events array with every status change, timestamp and note.

Cancel an order

POST /orders/318/cancel
{ "reason": "Customer changed their mind" }

Allowed while the order is received, processing or on_hold. Returns 409 once shipped.

Payment and invoices

Every order creates an invoice on your PrymaLab account for the white-label cost, payable with any payment method on prymalab.net. Prepay accounts ship once the order invoice is paid; net-terms accounts are invoiced but never held. Every order object carries a payment block:

"partner_cost": 57.00,
"shipping_charge": 0,
"payment": {
  "status": "unpaid",            // unpaid | paid | partial | none | terms | terms_due
  "amount_due": 57.00,
  "amount_paid": 0,
  "pay_url": "https://prymalab.net/checkout/order-pay/4821/?pay_for_order=true&key=wc_order_…",
  "invoices": [ { "id": 4821, "type": "order", "amount": 57.00, "status": "unpaid",
                  "paid_at": null, "created_at": "2026-09-05 10:02:11" } ]
}

pay_url is the WooCommerce pay-for-order page for the oldest unpaid invoice; it works signed in or not. partial means the order invoice is paid and only a carrier or extra charge is open, which never holds the shipment. Invoice type is order (product cost, holds the shipment on prepay accounts), shipping (carrier at cost, issued after payment; never holds) or manual (an extra charge agreed with your account contact). When payment clears you receive an order.paid webhook and the block flips to paid. Cancelling an unshipped order cancels its unpaid invoice.

Catalog and pricing

GET /catalog

Returns your white-label price list for vials, nasal sprays, pens, and capsules and SARMs with all three volume tiers, plus the effective date. Use the sku values here in your orders; sprays, pens and capsules have no catalogue number, so send the product name exactly as listed.

Outbound webhooks

Set an HTTPS URL in the dashboard. We POST on every status change, plus order.paid when an invoice is paid, and retry with backoff for 24 hours. Verify the signature: HMAC-SHA256 of the raw request body using your signing secret, hex encoded, in X-PrymaLab-Signature.

POST https://your-store.example/hooks/prymalab
X-PrymaLab-Event: order.fulfilled
X-PrymaLab-Signature: 3f1a…   (hex HMAC-SHA256 of body)
X-PrymaLab-Delivery: 9021

{ "event": "order.fulfilled",
  "order": { "id": 318, "external_id": "1042", "order_number": "#1042", "status": "fulfilled",
             "carrier": "USPS", "tracking_number": "9400 1000 0000 0000 0000 00",
             "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400...",
             "items": [ { "sku": "BC5", "name": "BPC-157 5mg", "quantity": 2, "lot": "PL-2609-114" } ],
             "shipped_at": "2026-09-03 16:40:02" },
  "sent_at": "2026-09-03T20:40:05+00:00" }

PHP verification:

$body = file_get_contents('php://input');
$ok = hash_equals(hash_hmac('sha256', $body, $secret), $_SERVER['HTTP_X_PRYMALAB_SIGNATURE'] ?? '');

Node verification:

const sig = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const ok  = crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(req.headers['x-prymalab-signature'] || ''));

WooCommerce adapter

No code needed. In your store go to WooCommerce → Settings → Advanced → Webhooks, add a webhook with topic Order updated, delivery URL https://prymalab.net/wp-json/prymalab/v1/webhooks/woocommerce/{your_partner_id}, and set the secret to the inbound secret from your dashboard. We create the order when yours reaches processing or completed, ignore other statuses, and cancel on cancelled or refunded if we have not shipped. Duplicate deliveries are safe.

To receive tracking back into WooCommerce, point your outbound webhook at a small endpoint that adds a note or tracking meta to the order. A reference receiver plugin is available in the partner dashboard under API & webhooks: install it on your store, paste the signing secret, and it writes carrier, tracking and lot numbers onto the matching order and marks it completed when shipped.

Shopify adapter

No code needed. In your partner dashboard open API & webhooks → Connect a Shopify store. Create a custom app in Shopify (Settings → Apps and sales channels → Develop apps) with scopes read_orders, read_fulfillments, write_fulfillments, read_merchant_managed_fulfillment_orders, write_merchant_managed_fulfillment_orders and write_webhooks, then paste the store domain, the Admin API access token and the API secret key. Test connection checks the token; Create webhooks registers orders/create, orders/updated and orders/cancelled on your store pointing at https://prymalab.net/wp-json/prymalab/v1/webhooks/shopify/{your_partner_id}.

We create the order once financial_status is paid (an unpaid order is picked up later by orders/updated), cancel it if you cancel on Shopify before we ship, and ignore duplicates. When we mark the order Fulfilled we create a Shopify fulfillment for the open fulfillment orders with the carrier, tracking number and tracking URL and set notify_customer, so Shopify sends your customer its shipping confirmation. If Shopify rejects the call (token revoked, order already fulfilled) the failure is written to the order timeline and retried hourly for 12 hours.

Statuses

StatusMeaningWebhook event
receivedOrder arrived from your store or the API. Not yet picked.order.received
processingBeing labeled, packed and QC checked.order.processing
fulfilledShipped. Tracking number attached.order.fulfilled
completeDelivered or closed.order.complete
on_holdWaiting on something. See the note.order.on_hold
cancelledCancelled before shipping.order.cancelled

Errors

HTTPCodeWhy
401unauthorizedMissing, revoked or wrong API key.
400bad_jsonBody is not valid JSON.
422no_items / no_addressOrder is missing items or a shippable address.
404not_foundOrder does not exist or belongs to another partner.
409shippedCancel requested after shipment.

Rate limit: 600 requests per minute per key. Questions: michael@prymalab.net.