PEPTIDES·INDEX
72 peptides417 listingsBuild My Stack
Peptides·Index · integration spec v2.0

Vendor catalog API

Two read-only endpoints, behind one key we issue you — your catalog, which gets your prices, pack deals, shipping and lab certificates listed accurately, and the orders our readers place with you, which is how commission is reconciled.

The entire requirement

skunameurlpricecompounds

Five fields per product, plus your vendor name, URL and currency — that is the catalog. And an orders endpoint behind the same key, with no customer data. Both are required to be listed; everything else on this page is optional. compounds names what is in one unit using our slug, an amount and its unit — bpc-157 · 10 · mg. Products with no compounds (water, syringes, merch) are simply left out.

Building this with an AI assistant? Copy the onboarding prompt — it walks your assistant through detecting your platform, mapping your products to our compounds and building the endpoint.

peptides-index.com/vendors/api-guide/prompt.md

peptides-index.com/vendors/api-guide.md (spec only)

Why bother — what it fixes, from real cases

What happens without itWhat this gives you
Your pack deal is printed on the page but not anywhere we can read, so we publish your single-vial price as your best price. One vendor's BPC-157 showed as $3.50/mg when their real floor was $2.45/mg — they looked 30% more expensive than they are.packs[] and volume_discount — every deal, in the comparison.
A product's size lives only in prose ("a full 10mg per vial"). No size means no cost per mg, which means the listing cannot be ranked at all.compounds[].amount — explicit.
A blend titled 10/3mg gets read as 3 mg, pricing it at $26.66/mg instead of $6.15.One compounds entry per component — no parsing, no guess.
A vial dosed in international units has no milligram figure, so it ranks against nothing — or a number meant as IU is read as mg.unit: "iu" — the unit travels with the number, and IU is compared only with IU.
A spray labelled 50mg/30mL gets read as 30.amount: 50 — the peptide, not the solvent.
Your product is called "RT-3" or "Reta 10". To you that is obviously retatrutide; to a scraper it is a guess, and a wrong guess puts your listing in someone else's comparison.compounds[].slug — you name the compound from our list, so nobody guesses.
A reader outside the US sees your US shipping cost, or none at all, and finds out at checkout that delivery is $40 and three weeks.shipping[] — cost and duration, per destination.
You re-slug a product; our old row keeps serving its last price with a link to a 404.sku — stable, so a rename is a rename, not a phantom.
Your COA is a PDF we must OCR, or one we never find — or, worse, one we find by name that belongs to someone else. We once attributed 46 certificates to two vendors because a similarly-named company shared a word with them.coa_url — a link to the lab's own copy. We read the certificate from the lab, so it is yours and nobody else's.

We do not charge for a listing and we do not rank by commission. Accurate data is the entire benefit: a vendor whose prices and deals are right shows up correctly in every comparison a reader runs.

The API

Two required endpoints, both behind the same key. Put them wherever they fit your stack and send us the full URLs. The catalog:

https://yourdomain.com/api/peptides-index/v2/catalog
EndpointRequiredReturns
GET/catalogYesYour vendor block and every product you want listed, in one response.
GET/ordersYesThe orders our readers placed with you, without any customer data — see "Orders" at the end.

HTTPS only. The catalog has no pagination, no incremental sync and no webhooks: we fetch the whole catalog every time, and a few hundred products is a few hundred kilobytes. A product that is in one response and missing from the next has been delisted, and we act on that — so a full catalog is self-describing in a way a partial one never is.

The key

Both endpoints are gated with a read-only key that we issue you — one unique key per vendor, in the vendor portal's Integration kit. We send it on every request as a header:

Authorization: Bearer <your key>
  1. Store it as an environment variable named PEPTIDES_INDEX_KEY on your server, and read it from there — or, on WordPress, in the Peptides·Index plugin's settings page (Settings → Peptides Index; a PEPTIDES_INDEX_KEY constant in wp-config.php overrides it). Keep it out of your source code and never commit it to a public repository: it is what keeps other sites out of your feed. A new value usually takes effect only after a redeploy.
  2. Check it on every request. Missing or wrong key: answer 401 (a 403 is accepted too) and nothing else. Compare in constant time if your language offers it.
  3. Refuse to run without it. If PEPTIDES_INDEX_KEY is missing or empty on your server, answer 503 with the body {"error":"key_not_configured"} — never 200, and never compare our header against an empty string. Our checker reads that answer and tells you, in words, that the key is not set yet, instead of reporting a rejected key.
  4. Send us the two endpoint URLs, catalog and orders, through the vendor portal. We already hold the key.

Don't let a cache serve it. Send Cache-Control: private, no-store and Vary: Authorization on every response from both endpoints, including 401 and 503. A page cache or CDN in front of your site can otherwise store the keyed response and hand it to the next visitor with no key at all — our checker requests the same URL without the key to catch exactly that, and fails the endpoint if a cache answers.

Never put the key in the URL. We only ever send it as a header, and a ?key= query string is written into your access logs, your CDN's logs, analytics and every proxy in between.

The catalog endpoint should serve your catalog and nothing else — not orders, not customers, not your store's admin API. Orders get their own endpoint, described at the end. We can rotate or revoke the key at any time: when we issue a new one, swap it into your environment variable, and the old one stops working the moment you do.

HTTP behaviour we rely on

Mostly things your web server already does.

  • We fetch daily and always fetch the full catalog. Rate limit us however you like; a handful of requests a day is the whole load.
  • ETag and/or Last-Modified — welcome, not required. Return either and we send back If-None-Match / If-Modified-Since; a 304 Not Modified with an empty body is a perfect answer when nothing changed.
  • Status codes that mean what they say. 200 with data, 304 unchanged, 401 (or 403) bad or missing key, 429 when we are asking too often, 503 during maintenance — and 503 with {"error":"key_not_configured"} when your own PEPTIDES_INDEX_KEY is not set. Please send Retry-After with 429 and 503; we will back off rather than hammer you or, worse, conclude your catalog is empty.

Never return 200 with an empty or partial products[] during an outage. A successful-looking empty catalog reads as "this vendor delisted everything". We guard against it on our side — a vendor yielding zero products is skipped, never pruned — but a 503 is unambiguous and safer for both of us.

A pre-generated file is fine

If it is simpler, build the JSON on a schedule and serve that file from the endpoint — as long as the same Bearer check guards it. A public file at a guessable URL is not gated, and anyone can scrape your whole catalog from it. A WordPress/WooCommerce plugin, a route in a headless Shopify app, a small serverless function — anything that answers the URL with the right JSON, behind the key, is a correct implementation. A Shopify store on a theme builds none of this: the vendor portal connects the store directly instead (its Shopify steps).

What we actually need

Per product: sku, name, url, price and compounds. At the top: your vendor name, url and currency. Without compounds we do not know what the product is or how much of it you sell, so we cannot compare it with anything.

That is the whole catalog requirement; the other required half is the orders endpoint (see "Orders"). Every catalog field below this is optional and improves what we can show; send what you have and omit the rest. A missing field is read as unknown and handled. A wrong one gets published.

A complete, working response can be this short:

{
  "schema_version": "2.0",
  "vendor": { "name": "Your Company", "url": "https://yourdomain.com", "currency": "USD" },
  "products": [
    { "sku": "BPC10", "name": "BPC-157 10mg", "url": "https://yourdomain.com/product/bpc-157/",
      "price": 39.00, "compounds": [ { "slug": "bpc-157", "amount": 10, "unit": "mg" } ] }
  ]
}

A blend, with its composition — the shape that is easy to get wrong. blend names it as one of our listed blends; compounds still lists every component with its amount, because that is what the price per mg is computed from:

{ "sku": "KLOW80", "name": "KLOW 80mg",
  "url": "https://yourdomain.com/product/klow/",
  "price": 120.00, "form": "vial",
  "blend": "klow",
  "compounds": [
    { "slug": "ghk-cu",  "amount": 50, "unit": "mg" },
    { "slug": "bpc-157", "amount": 10, "unit": "mg" },
    { "slug": "tb-500",  "amount": 10, "unit": "mg" },
    { "slug": "kpv",     "amount": 10, "unit": "mg" }
  ] }

Ship that first if you like — it already gets your prices right. The rest of this document is what you can add afterwards, in any order.

Everything you can add

{
  "schema_version": "2.0",                // string, REQUIRED — the version of THIS SPEC, not of your catalog
  "updated_at": "2026-09-22T21:05:17Z",   // string | null — ISO 8601, when your catalog last changed
  "vendor": {
    "name": "Example Research",           // string, REQUIRED
    "url": "https://example.com",         // string, REQUIRED — your storefront
    "currency": "USD",                    // string, REQUIRED — ISO 4217, uppercase.
                                          // Applies to EVERY price in the feed.
    "ships_to": ["US", "CA"],             // array of strings — ISO 3166-1 alpha-2, uppercase
    // array of objects — what delivery costs and how long it takes, PER DESTINATION.
    // A country MUST NOT appear in more than one entry.
    //   countries : array of strings, REQUIRED — ISO alpha-2
    //   cost      : number — in `currency`; 0 means free
    //   free_over : number | null — order subtotal that makes it free; null = never
    //   days_min  : number | null — business days, low end
    //   days_max  : number | null — business days, high end
    "shipping": [
      { "countries": ["US"], "cost": 12, "free_over": 150, "days_min": 2, "days_max": 4 },
      { "countries": ["CA"], "cost": 25, "free_over": null, "days_min": 7, "days_max": 14 }
    ],
    // array of objects — EVERY method you accept, or omit the field entirely.
    //   method  : string, REQUIRED — "card" | "crypto" | "zelle" | "venmo" | "cashapp" | anything else you take
    //   pct_off : number | null — ONLY where paying that way costs less
    "payment_methods": [
      { "method": "card" },
      { "method": "crypto", "pct_off": 5 }
    ],
    // object | null — a percentage off for buying more. Omit or null if you have none.
    //   scope             : enum, REQUIRED — "cart" (mix and match across products)
    //                                      | "product" (N of the same item)
    //   tiers             : array of objects, REQUIRED — { min_qty: number, pct_off: number }
    //   stacks_with_packs : boolean — default false: we take whichever of this and
    //                       a product's `packs` is cheaper
    "volume_discount": {
      "scope": "cart",
      "tiers": [ { "min_qty": 3, "pct_off": 10 }, { "min_qty": 5, "pct_off": 20 } ],
      "stacks_with_packs": false
    }
  },
  "products": [
    {
      "sku": "BPC10",                        // string, REQUIRED — unique, and never changes
      "name": "BPC-157 10mg",                // string, REQUIRED — exactly as listed on your site
      "url": "https://example.com/p/bpc-157", // string, REQUIRED — absolute, on your own site
      // array of objects, REQUIRED — what is in ONE unit. One entry per compound.
      //   slug   : string | null, REQUIRED — from OUR compound list; null if it is not on it
      //   name   : string — REQUIRED when slug is null; what you call the compound
      //   amount : number, REQUIRED — per unit: per vial, per capsule, per bottle of spray
      //   unit   : enum, REQUIRED — "mg" | "mcg" | "iu"
      "compounds": [ { "slug": "bpc-157", "amount": 10, "unit": "mg" } ],
      // string | null — ONLY for a blend we list: the slug of that named blend,
      // one flagged "blend": true in /api/compounds. Omit it for a single
      // compound and for a blend we do not list. Never inside `compounds`.
      "blend": null,
      "form": "vial",                        // enum — "vial" | "capsule" | "tablet" | "spray" | "nasal" | "topical" | "kit" | "other"; default "vial"
      "unit_count": 1,                       // number — vials per unit sold, capsules per bottle; default 1
      "price": 39.00,                        // number, REQUIRED — ONE unit at quantity 1, in `currency`
      "min_qty": 1,                          // number | null — smallest quantity you sell; default 1
      "in_stock": true,                      // boolean | null — null means your platform cannot tell
      // array of objects | null — this product's own bundles.
      //   qty   : number, REQUIRED — units in the pack
      //   price : number, REQUIRED — the TOTAL for the pack, as your checkout shows it
      "packs": [ { "qty": 10, "price": 350.00 } ],
      // string | null — the LAB's own copy of the current batch's certificate:
      // its verify page or a lab-hosted PDF. Never a copy on your own site.
      "coa_url": "https://portal.ils-lab.com/verify/atZrY5XXDL3dPY0B"
    }
  ]
}

Our compound list

Every compounds[].slug comes from one public endpoint — no key needed:

GET https://peptides-index.com/api/compounds
{
  "updated_at": "2026-09-22T21:05:17Z",
  "count": 74,
  "compounds": [
    { "slug": "bpc-157", "name": "BPC-157", "aliases": ["BPC-157", "Oral BPC-157"] },
    { "slug": "klow", "name": "KLOW", "aliases": ["KLOW"], "blend": true }
  ]
}
  • Match on name and aliases, then send the slug. Aliases are the names vendors actually use, so your product name is often already in there.
  • Named blends carry "blend": true; every other entry omits the key. A blend slug goes in a product's blend field, never in compounds.
  • Slugs are permanent. Each one is the last part of that compound's public URL on our site (/peptide/bpc-157), so it does not change. Map once and the mapping holds.
  • Not on the list? Send "slug": null with your "name" for the compound. We review it and either map it to an existing slug or add a new one; until then that product is not ranked. Never invent a slug — one we do not have matches nothing.

The list is also embedded in the AI onboarding prompt on this page, for assistants that cannot make web requests.

Field notes, in order of how often they go wrong

price is ONE unit, at quantity 1

price is what one unit costs when a customer adds it to the cart at quantity 1. Never a pack total, never a derived per-mg figure. Ten vials for $350 is a packs entry, not price: 350.

This is the single easiest field to get wrong, because most ecommerce systems have exactly one "price" and for a bundle product it is the bundle's. Exporting that straight through multiplies your cost per mg by the bundle size and drops you to the bottom of every comparison you should be winning.

"One unit" is whatever your cart counts as one:

You sellSend
One 10 mg vial for $39price: 39, unit_count: 1
A box of 5 vials as a single item, $175price: 175, unit_count: 5 — your cart counts one box
Single vials, but only in fives, $35 eachprice: 35, min_qty: 5 — your cart counts vials, and five is the minimum
A bottle of 60 capsules for $60price: 60, form: "capsule", unit_count: 60

price is always present. If you cannot price a product, leave the product out rather than sending 0 or null — a zero price is the cheapest listing on the site.

compounds — what is in it, in our words

compounds is an array of objects, one per compound in the product. Each has a slug, an amount and a unit.

  • slug comes from our compound list, not from your product names — see "Our compound list" above. A slug names the compound, not the form: BPC-157 capsules are bpc-157 with form: "capsule".
  • amount is per unit — per vial, per capsule, per bottle of spray. Not the package total (we multiply by unit_count), not a concentration, and not a dose. A 10 mL vial holding 600 mg is amount: 600. A bottle of 60 × 0.5 mg capsules is amount: 0.5 with unit_count: 60.
  • unit is "mg", "mcg" or "iu". IU and milligrams measure different things and there is no conversion between them. An IU listing is compared with other IU listings, never with a milligram one.
  • A blend is one entry per component, with the amounts you already print on the label. A 10/3mg Tesamorelin/Ipamorelin is two entries, tesamorelin at 10 and ipamorelin at 3. Do not send the sum, and do not send percentages — you know the vial holds 10 mg and 3 mg, and round-tripping that through 76.9% / 23.1% loses precision in both directions. Mixed units are fine (5 mg and 500 mcg); we normalise them.
  • A compound that is not on our list gets "slug": null and your "name" for it. We review it and either map it to an existing compound or add it; until then it is not ranked.

blend — naming one of our blends

Some blends are compounds on our list in their own right — klow, wolverine, tesamorelin-ipamorelin — flagged "blend": true in /api/compounds. If your product is one of them, set "blend" to that slug and still list every component in compounds with its amount. The blend field decides which blend page you appear on; the components decide your price per mg.

  • A blend we do not list: omit blend and just list the components. Any product with two or more compounds is treated as an unnamed blend.
  • Never put a blend slug inside compounds. { "slug": "klow", "amount": 80 } hides the composition and gives us nothing to compute from.
  • We do not infer blend from components: two vendors' "KLOW" can differ, so the named blend is yours to state.

A size you cannot state honestly is better left out — send the product without it and tell us. A missing size costs you a ranking; a wrong one costs a reader's trust.

form decides what you are compared against

Cost per mg is only meaningful within a route: capsules and vials are never ranked against each other, because oral use needs far more material and mg-for-mg would make capsules look 2–37× better. Getting form right protects you from a flattering comparison a reader would rightly distrust.

We group the forms into routes — injectable (vial), oral (capsule, tablet), spray (spray, and nasal for nasal drops, which is compared with spray), and other — and compare only within one. A wrong form is not a small error: it is your listing in someone else's table.

Topical: accepted, price comparison coming soon. Serums, creams and gels are form: "topical". We list them, but do not yet compare them on cost per mg; when topical comparison arrives, your listings join it with no change on your side.

Ten identical vials sold as one item is not a kit. Many shops call that a "kit"; for us it is form: "vial" with unit_count: 10, or a packs entry. form: "kit" is for a bundle that is not one thing — different compounds in one box, or a vial sold with its water and syringes. We list a kit, but we do not rank it on cost per mg.

url must be on your own site

Every product url has to be on the store you applied with: the same host, a subdomain of it, or its parent (shop.example.com for example.com, or the other way round). A sibling subdomain (store.example.com when you applied with shop.example.com) or any other site is refused: the checker reports it as product-domain and that product is not imported. Our Buy button sends shoppers to this link with our referral code added, so it must be the product page itself.

sku must be unique and stable

It is how we know your renamed product is the same product. A changed SKU is a new product: the old listing looks delisted, the new one looks new, and its price history starts again from zero. Two products sharing a SKU is worse — one of them silently disappears.

Shipping is per destination, and duration counts as much as cost

shipping is an array of objects, and a country must not appear in more than one entry. We could have said "the first matching entry wins", which would have made the order of a JSON array into business logic — reordering your entries would change the price a reader sees. An overlap is a validation error instead: we tell you, you fix it.

Two vendors at the same cost per mg are not the same offer to someone in Toronto. One ships for $12 in two days; the other charges $40 and takes three weeks through customs. days_min / days_max are business days, and a range is fine — most readers want to know whether it is "this week" or "next month".

A country in ships_to with no shipping entry is shown as "ships here, cost unknown". That is honest, but a real number is better.

free_over is worth more than it looks

A $12 flat fee on a $39 vial is almost a third of the price again, and a reader comparing two vendors at $3.90/mg is really choosing between very different totals. Free-over thresholds also decide whether buying three is cheaper than buying one somewhere else.

payment_methods changes the real price

Several vendors take 5–10% off for crypto or a bank transfer and say so only at checkout — by which point a reader has already chosen someone else on a headline number that was never the number. If you do this, tell us: it is one of the few things that genuinely moves where you land.

Send the whole list or none of it. A partial list reads as the complete set and sends a reader to a checkout that wanted something else. We tried reading payment methods off storefronts and a scan of one vendor's page found "crypto, Venmo, Cash App" and missed "card" — from a single sentence that listed all four. So we publish nothing unless we have the lot.

packs vs volume_discount

packs is this product: "10 vials of BPC-157 for $350". Its price is the total for the pack, exactly as your checkout shows it — not a per-vial figure; we divide. volume_discount is a percentage off for quantity: with scope: "cart" it is mix-and-match ("any 3 vials, 10% off"); with scope: "product" it counts only the same item ("3 of the same, 10% off"). Both are welcome; they display differently because they are different offers.

How the discounts combine

You can express three price reductions — packs, volume_discount and a payment_methods percentage — and we compute a best achievable price from them. The rule we apply:

  1. Quantity discounts do not stack with each other. packs and volume_discount are alternatives and we take whichever is cheaper at that quantity. Set volume_discount.stacks_with_packs: true only if your checkout genuinely applies both — if it is on and your checkout does not honour it, we publish a price nobody can buy at.
  2. A payment-method discount applies on top, because it is nearly always a percentage off the final total. If yours is either/or with a quantity discount, tell us when you send the endpoint.

So ten vials on a pack, paid in crypto at 5% off, is the pack price less 5%.

min_qty when you do not sell singles

Some vendors' smallest purchase is several of an item. Keep price as the one-unit figure and set min_qty to the number that has to be bought; we multiply. Omit the field if you sell singles. A spec with nowhere to say "you cannot buy one of these" invites a per-vial price nobody can actually pay, which is what we once published for one vendor until we caught it.

in_stock

true or false if you know. null if your platform genuinely cannot tell — some cannot. null is shown as "unknown", never as out of stock, so it costs you nothing; true on something you cannot ship costs a reader a wasted checkout.

Lab certificates: send the link, not the numbers

We do not want purity figures, test panels or lab names from you. We read them from the lab. Send coa_url — a link to the laboratory's own copy of the certificate for the batch you are selling now — and we identify the lab from the domain, parse the certificate reference out of the link, and fetch the result from the lab directly.

LabWhat a coa_url looks like
ILShttps://portal.ils-lab.com/verify/<id>
Freedom Diagnosticshttps://coas.freedomdiagnosticstesting.com/<code>.pdf
Janoshikhttps://verify.janoshik.com/tests/<task>_<key>
Vanguardhttps://verifiedbyvanguard.com/verify/<code>

Another lab is fine — send its verify page or lab-hosted PDF and we will add it.

Why the lab's copy and not yours:

  • It is a stronger claim on your behalf. A certificate we fetched from the lab is one a reader does not have to take your word for. A PDF on your own server could be anyone's.
  • It cannot be attributed to the wrong vendor. Matching certificates by name is how 46 of them once ended up on two vendors that had never commissioned them. A link you give us, on the lab's domain, is yours by construction.
  • It carries the whole panel. Purity barely separates vendors — across everyone we list it runs from about 99.3% to 99.8%. Endotoxin, sterility, heavy metals and fentanyl screening do, and only about one certificate in five we hold carries a fentanyl screen. Reading the lab's record means everything you paid to have tested gets shown.

A coa_url on your own domain is treated as unverified. We will link it, labelled as vendor-hosted, but we do not read results from it. If your lab has no public verify page or hosted PDF, send what you have and name the lab when you contact us.

Keep it current: when a new batch is tested, update the link. An old batch's certificate on a new batch's listing is the one thing in this section that can actually hurt you.

What we compute ourselves

Do not send derived numbers. From price, unit_count, packs and compounds we compute:

  • per-vial price (or per capsule) — price ÷ unit_count, and the same for every pack;
  • package total — amount × unit_count, summed across components for a blend;
  • cost per mg (or per IU) — within one form only;
  • best achievable price — packs, volume discount and payment discount combined as described above.

If you send your own versions of these, they will disagree with ours on rounding at the very least, and a reader will see two numbers.

What we do with it

  1. Fetch on schedule — daily, with your key, respecting Retry-After. We fetch only the URL you give us. It is not permission to crawl the rest of your site, and your robots.txt does not apply to it.
  2. Publish price, cost per mg, stock, packs, shipping and the lab's certificate on each listing, matched to our compound pages by slug.
  3. Never alter your prices, and never rank by what we earn.

Check it yourself before you send it

Every feed is validated on our side, and a failure is a conversation rather than a silent drop. These are the same checks — run them first and there will be nothing to talk about:

CheckWhy it exists
Every product has sku, name, url, price and compoundsThey are the whole requirement.
sku is unique within the responseTwo products on one SKU and one of them disappears.
price is present and greater than 0 on every productA zero price is the cheapest listing on the site. Omit what you cannot price.
vendor.currency is an ISO 4217 code (USD, EUR, GBP…)Every price in the feed is read in it.
Every compounds[].amount is positive, and unit is mg, mcg or iuA zero or missing amount means no cost per mg.
Every compounds[].slug is on our list, or null with a nameAn invented slug matches nothing.
blend, if present, is a slug flagged "blend": true in /api/compounds, and that product lists 2 or more compoundsA named blend without its components cannot be priced per mg.
No blend slug appears inside compoundsIt hides the composition.
Every pack's price per vial (price ÷ qty, then ÷ unit_count) is below the single priceA pack that costs more per vial is almost always a per-vial figure sent as a total, or the reverse.
No country appears in two shipping entriesOtherwise the rate a reader sees depends on array order.
coa_url is on the lab's domain, not yoursOnly a lab-hosted certificate is read and shown as verified.
stacks_with_packs is true only if your checkout really applies bothOtherwise we publish a total nobody can buy at.
Diluents, syringes and merch are left outAnything with no compounds is ignored anyway.
No Authorization header → 401 or 403; wrong key → 401 or 403; the key we issued → 200The endpoint is gated, and our key is the only way in.
With PEPTIDES_INDEX_KEY unset or empty on the server → 503 {"error":"key_not_configured"}, never 200A missing key must not open the endpoint, or match an empty header. The 503 tells both of us exactly what is wrong.
Right after a keyed 200, the same URL with no header still gets 401 or 403 — not a cached 200Our checker makes exactly that request. A page cache or CDN that replays the keyed answer opens your feed to anyone; Cache-Control: private, no-store and Vary: Authorization prevent it.

Validating it

Send both endpoint URLs, catalog and orders, through the vendor portal — we already hold the key, so there is nothing else to send. We run it and reply with exactly what we parsed — every product, compound, pack and certificate we read — so you can see your listing before it is live. If a field is wrong, we would rather find it in the diff than after publication.

Orders

The second endpoint: the orders our readers placed with you, with no customer data in them. It is required to be listed, alongside the catalog. It does not change how you rank — ranking is on price and data, never on what we earn.

Why it exists

We watch what we send you. Clicks alone say which links people follow, not which of them became an order, a refund or nothing at all — and the only other place to find that out is your affiliate portal, which somebody has to log into and read by hand. An orders endpoint replaces that login: we read the same figures on a schedule, and it is how commission is reconciled — when your numbers and ours disagree, we both have one record to point at.

The request

GET <your orders URL>?since=<ISO-8601 UTC>&cursor=<opaque>
Authorization: Bearer <your key>
  • The same key as the catalog, checked the same way: missing or wrong key → 401 or 403 and nothing else; PEPTIDES_INDEX_KEY unset on your server → 503 {"error":"key_not_configured"}. There is no second key to issue or store.
  • since means orders UPDATED since then, not created. A refund or a cancellation changes an order you already sent us, and it only reaches us if its updated_at moves and since catches it. Example: since=2026-09-22T00:00:00Z.
  • cursor is yours: an opaque string from your last page's next_cursor, passed back unchanged with the same since. The first request has no cursor. Build it from the last order's updated_at and id, and return the orders strictly after that pair — not a page number or an offset, which skips an order that changes during the pull.
  • The status codes are the catalog's: 200 with data, 401 or 403, 429 and 503 with Retry-After. Never 200 with an empty list during an outage — it reads as "no orders".

Send us the URL with the catalog's. It can be any path; /api/peptides-index/v2/orders beside the catalog is a sensible default.

The response

{
  "schema_version": "2.0",                  // string, REQUIRED — the version of THIS SPEC, as in the catalog
  "orders": [                               // array, REQUIRED — at most 500 per page; [] when there are none
    {
      "id": "10482",                        // string, REQUIRED — your order number; stable, never reused
      "created_at": "2026-09-20T14:02:11Z", // string, REQUIRED — ISO 8601, UTC; a real date, 2000 or later
      "updated_at": "2026-09-21T09:30:00Z", // string, REQUIRED — ISO 8601, UTC; moves on every change
      // enum, REQUIRED — "pending" | "paid" | "fulfilled" | "cancelled" | "refunded" | "partially_refunded"
      "status": "paid",
      "currency": "USD",                    // string, REQUIRED — ISO 4217, uppercase
      "subtotal": 120.00,                   // number, REQUIRED — the items, before discounts; no shipping, no tax
      "discount": 12.00,                    // number, REQUIRED — the amount taken off the items; 0 if none
      "commissionable": 108.00,             // number, REQUIRED — what the agreed commission applies to
      // enum, REQUIRED — how the order reached you from us: "coupon" | "ref" | "both"
      "attribution": "both",
      "coupon_code": "PEPTIDES",            // string | null, OPTIONAL — the code as entered; null or left out when none
      "click_id": "k3v7q2mz5xab",           // string | null — our pi_click value from the visit; null only when the order came without one
      // array of objects, REQUIRED — what was bought
      //   sku : string, REQUIRED — the same sku as in your catalog
      //   qty : integer, REQUIRED — greater than 0
      "items": [ { "sku": "BPC10", "qty": 3 } ]
    }
  ],
  "next_cursor": null                       // string | null — pass it back for the next page; null on the last one
}
  • Only orders attributed to us — placed with our referral code, arriving through our referral link, or both. Both are shown in the vendor portal (usually PEPTIDES and ?ref=PEPTIDES). Nobody else's orders, not even with the customer removed.
  • At most 500 orders per page. Past that, return a next_cursor and we ask again.
  • At most 20 pages per pull. We follow next_cursor for 20 pages — 10,000 orders — and stop; the rest come on the next pull, which asks from the latest updated_at we hold, less a two-day overlap.
  • Sorted by updated_at, oldest first, across every page. That is what makes stopping at page 20 safe: everything up to the last order we read has arrived, so the next pull resumes from there without a gap. In any other order, a pull that stops early holds a late updated_at while earlier orders it never reached fall behind since, and never arrive.
  • An order can reach us more than once — on a later page, or again after it changes. We key on id, and the copy with the later updated_at wins, so re-sending is always safe.
  • Amounts are JSON numbers, 0 or more, and under 10 billion. 108.00, not "108.00". A negative subtotal, discount or commissionable fails the order, and so does one that does not round to less than 9999999999.99. We store cents: amounts are compared after rounding to two decimals.
  • Timestamps are real dates. created_at and updated_at must exist on the calendar (2026-02-30 fails, it is not rolled into March), be no earlier than 2000-01-01, and be no more than a day ahead of our clock. A date in the far future would otherwise hold the window of every later pull.
  • One field is optional: coupon_code. Leave it out or send null — absent reads as null. Our code is the one shown in the vendor portal (usually PEPTIDES), and attribution already says whether it was used.
  • click_id: build the capture wherever your platform can carry it (below). It is null on an order that arrived without a pi_click, such as one where the customer typed our code, or on a platform that cannot carry it. When you send either field, it is a string within its limit below.
  • Field limits. Longer values are rejected, not truncated — a cut-off order number would match the wrong order, or none. Lengths are counted in bytes, so a character outside plain ASCII counts as two or more:
    • id — at most 100 characters, and not empty
    • coupon_code — at most 100 characters
    • click_id — at most 64 characters
    • items[].sku — at most 120 characters, and not empty
  • A bad order is skipped, not half-stored. An order that breaks any rule here is left out and the rest of the page is kept; the checker names it. A page that is not in the orders shape, or a next_cursor we have already been given, ends the pull — nothing from it is stored, and we try again next time.

No customer data, ever. Anywhere in the response: no names, emails, phone numbers, addresses, IP addresses, customer ids or user ids, order notes or client details — not in a field of their own, not inside another one, not "just the city", and not beside the orders either (a top-level "customers" list counts). We have no use for them and do not want to hold them. A response that contains any of them fails the check, whatever else in it is right; an order carrying one is never stored, and a page carrying one outside its orders ends the pull.

commissionable is the number the commission is worked out from

It is the amount your agreed commission applies to: after discounts, before shipping and tax. So it can never be more than subtotal − discount. It can be less — if some products earn no commission, leave them out of it — but never more. We send the figure rather than working it out ourselves because only your checkout knows what it excludes; if we derived it, the two sides would disagree on the first order that had an exception.

A refund lowers it: a partially_refunded order sends the amount that is still commissionable — commission is paid on that remaining amount — and a refunded or cancelled one sends 0.

click_id — build it with the endpoint, if your platform can

Our outbound links carry a pi_click parameter — ?pi_click=k3v7q2mz5xab on the URL a reader lands on. Keep it when the reader lands (a first-party cookie or the session, for about 30 days), attach it to the order they place, and send it back as click_id, unchanged. Store it only if it is 1–64 characters of printable ASCII, otherwise store nothing; a later landing with a valid value replaces the earlier one. A page cache (WP Rocket, Cloudflare APO) can skip PHP on the landing page, so capture it where code actually runs — a small script, or at add-to-cart or checkout — not only on page load. It lets both sides match an order to the exact visit that produced it, rather than to a code anyone can type — and it is far easier to build now, alongside the endpoint, than to come back for later. Most stores can: WooCommerce and most custom stores easily, Shopify through a cart attribute.

Treat it as opaque — today it is 12 characters, a–z and 2–7, but do not validate it against that. click_id: null is right for an order that arrived without one; coupon / ref attribution still works for those. If your platform cannot carry a value from the landing page to the order, send null — nothing fails, and it does not hold up your listing.

What the orders checker runs

The same idea as the catalog checks: run them first and there is nothing to discuss.

CheckWhy it exists
No Authorization header → 401 or 403; wrong key → 401 or 403; our key → 200Orders are more private than a catalog. Our key is the only way in — a check that only looks for the header is not a gate.
With PEPTIDES_INDEX_KEY unset or empty on the server → 503 {"error":"key_not_configured"}, never 200A missing key must never open your orders. The checker reports it as "not set yet", not as a rejected key.
The response is valid JSON in the orders shape aboveA shape we cannot read is a shape we cannot count.
No customer data anywhere in the responseNames, emails, phones, addresses, IPs, notes and customer ids — on an order or anywhere around the orders — fail it outright.
commissionable ≤ subtotal − discount on every orderA commission base larger than what the customer paid for the items is an error, whichever way it happened.
Every items[].sku is in your catalogA warning, not a failure: an item you have since delisted is still a real sale.
since set to the current time returns "orders": []Proves since is applied — otherwise every poll re-reads your whole order history.
next_cursor, passed back as cursor, returns the next page — none of the first page's orders againA cursor that is ignored sends the first page forever, and we never see the rest.
Orders are sorted by updated_at, oldest first, on the first page and into the nextA warning, not a failure: out of order, a pull that stops at page 20 can leave orders behind for good.
status, currency and attribution use the listed values; currency is three uppercase lettersA status we do not recognise is an order we cannot count.
A click_id is shaped like oursA warning, not a failure: something else in that field means the store is sending the wrong value.

Before you submit: one test order (required)

The checker proves the endpoint answers correctly; it cannot see a real sale. So before you submit — it is required — place one real test order: make a hidden test product priced low — $5–$10, or whatever your payment processor accepts (a free or near-free order is one many processors and affiliate plugins skip, and it must be attributed to us) — and give it a SKU, such as PI-TEST: an order line with no SKU fails the order. Open your referral link from the vendor portal with &pi_click=testorder234 added (the portal's Test order step shows it), go to the test product by its direct URL, check at checkout that your referral code is applied (enter it if not), and pay normally. Confirm it appears in your orders endpoint (with since set before the order), then use the portal's Test order step: enter the order id and press Check after placing it — while it is paid, within 30 days. Then refund it through your payment gateway, confirm it comes back with the new status, commissionable at 0 and a later updated_at, and press Check refund after refunding it. Both must pass before you can submit. Delete or unpublish the test product afterwards. The AI prompt walks you through it (Phase 8).

Peptides·Index · vendor catalog API, spec version 2.0 · September 2026.