Skip to main content

Vendor API

AminoCatalog Vendor API

Programmatic access to your catalog. Update stock, prices, and product details from your inventory system, ERP, or any script that can make HTTPS requests.

Quick start

  1. Open Vendor settings and click Generate new key.
  2. Copy the key (shown once, starting with ack_live_) into your secret manager.
  3. Pass it as a bearer token in the Authorization header on every request.

cURL

curl https://www.aminocatalog.com/api/v1/me \
  -H "Authorization: Bearer ack_live_YOUR_KEY_HERE"

Base URL & versioning

All endpoints live under https://www.aminocatalog.com/api/v1. Breaking changes will ship as /api/v2 with at least 90 days of overlap.

Authentication

Each request must include Authorization: Bearer <key>. Keys begin with ack_live_ and are 41 characters total. Treat them like a password — never commit them to source control, never paste them into shared docs.

A key is bound to a single vendor account and one or more scopes:

  • products:read — list and read products
  • products:write — create, update, and delete products (stock, price, URL, notes, quantity options)
  • reviews:read — list and read reviews left for your vendor
  • analytics:read — read traffic, click, and review-rate analytics
  • coas:read — list and read your Certificates of Analysis
  • coas:write — upload and delete COAs

If a key is missing the required scope for an endpoint, the response is 403 insufficient_scope. Revoke and regenerate at any time from the dashboard — the old key stops working immediately.

Rate limits

  • 60 requests / minute / key across all v1 endpoints.
  • 10 write requests / minute / IP as a global backstop (applies to all API traffic).

Exceeding either limit returns 429 rate_limited with a Retry-After header (seconds to wait). For bulk catalog work, prefer the bulk endpoints below to keep your request count low.

Errors

All errors return a JSON body with at least error. Validation failures also include code: "validation" and a fieldErrors map.

Example error response

{
  "error": "stockStatus must be one of in_stock, low_stock, out_of_stock, discontinued",
  "code": "validation",
  "fieldErrors": {
    "stockStatus": "Invalid enum value"
  }
}
StatusCodeMeaning
400validationRequest body or query string is malformed.
401unauthorizedMissing, malformed, revoked, or expired API key.
403insufficient_scopeKey is missing the scope required by this endpoint.
403vendor_not_approvedVendor status is not approved; API access is paused.
404Resource does not exist or does not belong to your vendor.
429rate_limitedSlow down — see Retry-After header.

Endpoints

Every successful response includes { "ok": true, ...payload }.

GET/api/v1/merequires scope products:read

Returns the vendor associated with the API key. Useful for confirming a key is wired up correctly during integration.

Response 200

{
  "ok": true,
  "vendor": {
    "id": "uuid",
    "slug": "acme-peptides",
    "name": "Acme Peptides",
    "status": "approved",
    "subscriptionTier": "verified",
    "websiteUrl": "https://acme.example",
    "country": "US",
    "averageRating": 4.51,
    "reviewCount": 142,
    "compositeScore": 87.42
  },
  "apiKey": {
    "id": "uuid",
    "scopes": ["products:read", "products:write"]
  }
}
GET/api/v1/productsrequires scope products:read

List your vendor's products. Supports filtering and pagination.

Query parameters

  • status — one of in_stock, low_stock, out_of_stock, discontinued (optional)
  • limit — 1..100 (default 50)
  • offset — ≥0 (default 0)

Request

GET /api/v1/products?status=in_stock&limit=25

Response 200

{
  "ok": true,
  "products": [
    {
      "id": "uuid",
      "stockStatus": "in_stock",
      "basePriceUsd": 35.0,
      "quantityOptions": [
        { "size": "5mg", "price_usd": 35.0 },
        { "size": "10mg", "price_usd": 60.0 }
      ],
      "publicNotes": "Ships in vacuum-sealed pouch.",
      "peptide": { "id": "uuid", "slug": "bpc-157", "name": "BPC-157" },
      "lastStockChangeAt": "2026-05-10T12:00:00Z",
      "updatedAt": "2026-05-15T08:32:00Z"
    }
  ],
  "pagination": { "limit": 25, "offset": 0, "total": 12 }
}
POST/api/v1/productsrequires scope products:write

Add a peptide to your catalog. The (vendor, peptide) pair must be unique: attempting to list the same peptide twice returns 409 duplicate_listing. The peptide must be in published status; otherwise returns 409 peptide_unavailable.

Body

  • peptideId (required) — UUID of the peptide
  • quantityOptions — array of { size, price_usd, currency? }, up to 20 entries
  • basePriceUsd — number ≥ 0
  • stockStatus — defaults to in_stock
  • publicNotes — string up to 500 chars

Request

POST /api/v1/products
Authorization: Bearer ack_live_xxx
Content-Type: application/json

{
  "peptideId": "3ce7e2a4-1234-4abc-8def-9999aaaa0000",
  "basePriceUsd": 35.0,
  "stockStatus": "in_stock",
  "quantityOptions": [
    { "size": "5mg",  "price_usd": 35.0 },
    { "size": "10mg", "price_usd": 60.0 }
  ]
}

Response 201

{
  "ok": true,
  "product": {
    "id": "uuid",
    "stockStatus": "in_stock",
    "basePriceUsd": 35.0,
    "quantityOptions": [...],
    "publicNotes": null,
    "peptide": { "id": "uuid", "slug": null, "name": null },
    "lastStockChangeAt": "2026-05-17T14:02:00Z",
    "updatedAt": "2026-05-17T14:02:00Z"
  }
}
GET/api/v1/products/:idrequires scope products:read

Fetch a single product by id. 404 if the product does not belong to your vendor.

PATCH/api/v1/products/:idrequires scope products:write

Update one or more fields on a product. Every field is optional — send only what you want to change. Sending an empty body returns 400.

Body

  • stockStatusin_stock | low_stock | out_of_stock | discontinued
  • basePriceUsd — number ≥ 0, or null to clear
  • publicNotes — string up to 500 chars, or null
  • quantityOptions — array of { size, price_usd, currency? }, max 20 items

Request

PATCH /api/v1/products/3ce7e2a4-1234-4abc-8def-9999aaaa0000
Authorization: Bearer ack_live_xxx
Content-Type: application/json

{
  "stockStatus": "low_stock",
  "basePriceUsd": 39.99
}

Response 200

{
  "ok": true,
  "product": {
    "id": "3ce7e2a4-1234-4abc-8def-9999aaaa0000",
    "stockStatus": "low_stock",
    "basePriceUsd": 39.99,
    "quantityOptions": [...],
    "publicNotes": null,
    "lastStockChangeAt": "2026-05-17T14:02:00Z",
    "updatedAt": "2026-05-17T14:02:00Z"
  }
}
POST/api/v1/products/stockrequires scope products:write

Bulk stock-status update. Up to 50 rows per call. Each row is applied independently and the response reports per-row outcome so a partial-success batch is still useful.

Request

POST /api/v1/products/stock
Authorization: Bearer ack_live_xxx
Content-Type: application/json

{
  "updates": [
    { "productId": "uuid-a", "stockStatus": "out_of_stock" },
    { "productId": "uuid-b", "stockStatus": "in_stock" },
    { "productId": "uuid-not-mine", "stockStatus": "in_stock" }
  ]
}

Response 200

{
  "ok": true,
  "requested": 3,
  "updated": 2,
  "results": [
    { "productId": "uuid-a", "status": "updated", "stockStatus": "out_of_stock" },
    { "productId": "uuid-b", "status": "updated", "stockStatus": "in_stock" },
    { "productId": "uuid-not-mine", "status": "not_found" }
  ]
}
DELETE/api/v1/products/:idrequires scope products:write

Remove the product listing from your catalog. This is a hard delete — historical analytics rows that referenced this product set their vendor_product_id to null but otherwise survive. If you want to keep the row visible-as-archived, PATCH with stockStatus: "discontinued" instead.

Response 200

{ "ok": true, "id": "uuid", "deleted": true }
GET/api/v1/reviewsrequires scope reviews:read

List reviews against your vendor. Defaults to published reviews only.

Query parameters

  • statuspublished (default) | pending | rejected | flagged
  • rating — 1..5
  • peptideId — UUID filter
  • hasResponsetrue | false
  • since — ISO datetime, returns reviews with created_at >= since
  • limit — 1..100 (default 50), offset — ≥0

Response 200

{
  "ok": true,
  "reviews": [
    {
      "id": "uuid",
      "rating": 5,
      "body": "Shipped fast, packaging was solid. ...",
      "tags": ["fast_shipping", "good_packaging"],
      "status": "published",
      "helpfulCount": 12,
      "author": { "username": "researcher42" },
      "peptide": { "id": "uuid", "slug": "bpc-157", "name": "BPC-157" },
      "response": {
        "body": "Thanks for the kind words — glad it arrived well.",
        "createdAt": "2026-05-10T12:00:00Z"
      },
      "publishedAt": "2026-05-09T18:30:00Z",
      "editedAt": null,
      "createdAt": "2026-05-09T17:14:00Z"
    }
  ],
  "pagination": { "limit": 50, "offset": 0, "total": 142 }
}
GET/api/v1/reviews/:idrequires scope reviews:read

Single-review detail. 404 if the review is not against your vendor.

GET/api/v1/analyticsrequires scope analytics:read

Vendor traffic and engagement metrics over a configurable window. Returns totals, a per-day series (zero-padded so every day in the window has an entry), and a per-product click breakdown.

Query parameters

  • days — 1..365, defaults to 30

Response 200

{
  "ok": true,
  "window": {
    "days": 30,
    "startIso": "2026-04-17T00:00:00.000Z",
    "endIso":   "2026-05-17T00:00:00.000Z"
  },
  "totals": {
    "pageViews": 1284,
    "outboundClicks": 217,
    "newReviews": 9
  },
  "daily": [
    { "date": "2026-04-17", "pageViews": 41, "outboundClicks": 8 },
    { "date": "2026-04-18", "pageViews": 39, "outboundClicks": 6 }
  ],
  "products": [
    {
      "productId": "uuid",
      "peptide": { "id": "uuid", "slug": "bpc-157", "name": "BPC-157" },
      "lifetimeClicks": 412,
      "windowClicks": 88
    }
  ]
}
GET/api/v1/coasrequires scope coas:read

List your vendor's uploaded Certificates of Analysis.

Query parameters

  • peptideId — filter to one peptide
  • statuspublished | hidden | rejected
  • since — ISO date (YYYY-MM-DD); returns COAs with testDate >= since
  • limit — 1..100 (default 50), offset — ≥0

Response 200

{
  "ok": true,
  "coas": [
    {
      "id": "uuid",
      "peptide": { "id": "uuid", "slug": "bpc-157", "name": "BPC-157" },
      "vendorProductId": "uuid-or-null",
      "batchNumber": "BPC-2026-04A",
      "labName": "Janoshik Analytical",
      "testDate": "2026-04-12",
      "issuedDate": "2026-04-15",
      "purityPct": 99.2,
      "extractedPurityPct": 99.2,
      "extractedTextExcerpt": "...Purity: 99.2% by HPLC...",
      "extractionStatus": "extracted",
      "extractionMethod": "pdf-text",
      "notes": "HPLC-MS, full peptide identity confirmed.",
      "fileUrl": "https://...supabase.co/storage/v1/object/public/vendor-coas/...",
      "filePath": "uuid/uuid.pdf",
      "fileMimeType": "application/pdf",
      "fileSizeBytes": 348201,
      "status": "published",
      "createdAt": "2026-04-15T17:00:00Z"
    }
  ],
  "pagination": { "limit": 50, "offset": 0, "total": 12 }
}

Automatic purity extraction. On upload, AminoCatalog runs a two-stage extraction pipeline:

  1. pdf-text — fast, free path. Pulls text out of the PDF and matches labeled keywords like Purity, Total purity, Assay.
  2. ai-vision — fallback for image-based PDFs and direct image uploads (PNG / JPEG / WebP). Uses Anthropic Claude with vision and structured JSON outputs to read the file. Only auto-fills purityPct on a high-confidence match.

The result lives in four response fields:

  • extractedPurityPct — the value we found, or null
  • extractedTextExcerpt — short slice of surrounding text (or the model's quoted reasoning) for audit
  • extractionStatus — one of pending, extracted, no_purity, no_text, skipped, or error
  • extractionMethod — one of pdf-text, ai-vision, or none (no extraction attempted)

Vendor-entered purityPct is never overwritten. If you upload a COA without providing purityPct and we extract one, we'll auto-fill the field for you.

POST/api/v1/coasrequires scope coas:write

Upload a new COA via multipart form-data. The request body is not JSON — set Content-Type: multipart/form-data.

Form fields

  • file (required) — PDF / PNG / JPEG / WebP, max 10 MB
  • peptideId (required) — UUID of the peptide; must be published
  • labName (required) — e.g. "Janoshik Analytical"
  • testDate (required) — YYYY-MM-DD
  • vendorProductId — UUID, optional; must belong to the same vendor and peptide
  • batchNumber — string up to 80 chars, optional
  • issuedDate — YYYY-MM-DD, must be ≥ testDate, optional
  • purityPct — 0..100, optional
  • notes — string up to 1000 chars, optional

Request (cURL)

curl -X POST "https://www.aminocatalog.com/api/v1/coas" \
  -H "Authorization: Bearer $ACK_KEY" \
  -F "file=@./coa-bpc157-2026-04.pdf;type=application/pdf" \
  -F "peptideId=3ce7e2a4-1234-4abc-8def-9999aaaa0000" \
  -F "labName=Janoshik Analytical" \
  -F "testDate=2026-04-12" \
  -F "issuedDate=2026-04-15" \
  -F "batchNumber=BPC-2026-04A" \
  -F "purityPct=99.2"

Response 201

{
  "ok": true,
  "coa": {
    "id": "uuid",
    "peptide": { "id": "uuid", "slug": null, "name": null },
    "vendorProductId": null,
    "batchNumber": "BPC-2026-04A",
    "labName": "Janoshik Analytical",
    "testDate": "2026-04-12",
    "issuedDate": "2026-04-15",
    "purityPct": 99.2,
    "notes": null,
    "fileUrl": "https://...supabase.co/storage/v1/object/public/vendor-coas/...",
    "filePath": "vendor-id/coa-id.pdf",
    "fileMimeType": "application/pdf",
    "fileSizeBytes": 348201,
    "status": "published"
  }
}
GET/api/v1/coas/:idrequires scope coas:read

Single COA detail. 404 if the COA does not belong to your vendor.

DELETE/api/v1/coas/:idrequires scope coas:write

Delete a COA. Removes the storage object first (idempotent — "object not found" is tolerated), then the metadata row.

Response 200

{ "ok": true, "id": "uuid", "deleted": true }

Recipes

Mark everything out of stock during a supplier outage

bash

# 1. List in-stock products to get ids.
PRODUCTS=$(curl -s "https://www.aminocatalog.com/api/v1/products?status=in_stock&limit=100" \
  -H "Authorization: Bearer $ACK_KEY" | jq -c '.products | map(.id)')

# 2. Build the bulk update body.
BODY=$(echo "$PRODUCTS" | jq -c '{ updates: map({ productId: ., stockStatus: "out_of_stock" }) }')

# 3. Send it.
curl -X POST "https://www.aminocatalog.com/api/v1/products/stock" \
  -H "Authorization: Bearer $ACK_KEY" \
  -H "Content-Type: application/json" \
  -d "$BODY"

Sync from your inventory system (Node.js)

Node.js

const key = process.env.AMINOCATALOG_API_KEY;
const base = 'https://www.aminocatalog.com/api/v1';

async function syncStock(rows /* [{ productId, qty }] */) {
  const updates = rows.map((r) => ({
    productId: r.productId,
    stockStatus:
      r.qty === 0 ? 'out_of_stock' : r.qty < 5 ? 'low_stock' : 'in_stock',
  }));

  // Chunk into batches of 50 to stay under the per-call limit.
  for (let i = 0; i < updates.length; i += 50) {
    const chunk = updates.slice(i, i + 50);
    const res = await fetch(`${base}/products/stock`, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${key}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ updates: chunk }),
    });
    if (!res.ok) {
      throw new Error(`Sync failed (${res.status}): ${await res.text()}`);
    }
  }
}

Need help?

Vendor API · Documentation · AminoCatalog