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
- Open Vendor settings and click Generate new key.
- Copy the key (shown once, starting with
ack_live_) into your secret manager. - 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 productsproducts:write— create, update, and delete products (stock, price, URL, notes, quantity options)reviews:read— list and read reviews left for your vendoranalytics:read— read traffic, click, and review-rate analyticscoas:read— list and read your Certificates of Analysiscoas: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"
}
}| Status | Code | Meaning |
|---|---|---|
| 400 | validation | Request body or query string is malformed. |
| 401 | unauthorized | Missing, malformed, revoked, or expired API key. |
| 403 | insufficient_scope | Key is missing the scope required by this endpoint. |
| 403 | vendor_not_approved | Vendor status is not approved; API access is paused. |
| 404 | — | Resource does not exist or does not belong to your vendor. |
| 429 | rate_limited | Slow down — see Retry-After header. |
Endpoints
Every successful response includes { "ok": true, ...payload }.
/api/v1/merequires scope products:readReturns 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"]
}
}/api/v1/productsrequires scope products:readList your vendor's products. Supports filtering and pagination.
Query parameters
status— one ofin_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=25Response 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 }
}/api/v1/productsrequires scope products:writeAdd 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 peptidequantityOptions— array of{ size, price_usd, currency? }, up to 20 entriesbasePriceUsd— number ≥ 0stockStatus— defaults toin_stockpublicNotes— 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"
}
}/api/v1/products/:idrequires scope products:readFetch a single product by id. 404 if the product does not belong to your vendor.
/api/v1/products/:idrequires scope products:writeUpdate 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
stockStatus—in_stock|low_stock|out_of_stock|discontinuedbasePriceUsd— number ≥ 0, ornullto clearpublicNotes— string up to 500 chars, ornullquantityOptions— 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"
}
}/api/v1/products/stockrequires scope products:writeBulk 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" }
]
}/api/v1/products/:idrequires scope products:writeRemove 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 }/api/v1/reviewsrequires scope reviews:readList reviews against your vendor. Defaults to published reviews only.
Query parameters
status—published(default) |pending|rejected|flaggedrating— 1..5peptideId— UUID filterhasResponse—true|falsesince— ISO datetime, returns reviews withcreated_at >= sincelimit— 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 }
}/api/v1/reviews/:idrequires scope reviews:readSingle-review detail. 404 if the review is not against your vendor.
/api/v1/analyticsrequires scope analytics:readVendor 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
}
]
}/api/v1/coasrequires scope coas:readList your vendor's uploaded Certificates of Analysis.
Query parameters
peptideId— filter to one peptidestatus—published|hidden|rejectedsince— ISO date (YYYY-MM-DD); returns COAs withtestDate >= sincelimit— 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:
- pdf-text — fast, free path. Pulls text out of the PDF and matches labeled keywords like Purity, Total purity, Assay.
- 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
purityPcton a high-confidence match.
The result lives in four response fields:
extractedPurityPct— the value we found, ornullextractedTextExcerpt— short slice of surrounding text (or the model's quoted reasoning) for auditextractionStatus— one ofpending,extracted,no_purity,no_text,skipped, orerrorextractionMethod— one ofpdf-text,ai-vision, ornone(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.
/api/v1/coasrequires scope coas:writeUpload 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 MBpeptideId(required) — UUID of the peptide; must bepublishedlabName(required) — e.g. "Janoshik Analytical"testDate(required) — YYYY-MM-DDvendorProductId— UUID, optional; must belong to the same vendor and peptidebatchNumber— string up to 80 chars, optionalissuedDate— YYYY-MM-DD, must be ≥ testDate, optionalpurityPct— 0..100, optionalnotes— 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"
}
}/api/v1/coas/:idrequires scope coas:readSingle COA detail. 404 if the COA does not belong to your vendor.
/api/v1/coas/:idrequires scope coas:writeDelete 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?
- Manage your API keys →
- Contact support → for integration questions or to report a bug.