Skip to content

Developers

API reference

Read your reviews, send your orders, and list stores from your own server. Every example is ready to paste.

Getting started

Every endpoint lives under one base URL. Send a key, and you get JSON back.

Base URL
https://pepreviewpro.com/api/v1
  1. Create a key in the dashboard, under API, Keys. A key for one store's server is a store key, under Widgets, Integrations.
  2. Keep it on your server as PEPREVIEWPRO_API_KEY.
  3. Call GET /api/v1/me to check it. It answers with the key and how much of this month's quota is left.
Your first request
curl "https://pepreviewpro.com/api/v1/me" \
  -H "Authorization: Bearer $PEPREVIEWPRO_API_KEY"

Conventions

  • A success answers with the resource itself. A list answers with its items under their plural name, like stores, and next_cursor.
  • Stores are named by slug: store in a request, slug in a store object. Your own order and product ids always come back as strings.
  • Timestamps are ISO 8601 in UTC with milliseconds, like 2026-11-03T16:00:00.000Z.
  • The examples use a made-up store, example-store.

Authentication

Send your key as Authorization: Bearer pk_live_… or as X-Api-Key. Public keys start with pk_live_, store keys with sk_live_, and plugin keys with prp_. Every key is a secret: call the API from your server, never from a browser.

KeyStarts withReachesMonthly quotaWhere to get it
API keypk_live_Every store on your account, or the directory on a partner accountCounts toward 10,000 a monthAPI, Keys
Store keysk_live_One storeNo monthly quotaWidgets, Integrations
Plugin keyprp_One store, for the WordPress pluginCounts toward 10,000 a monthWidgets, Integrations, WordPress

Each endpoint below names the keys that can call it.

Scopes

A key holds only the scopes you give it. A key without an endpoint's scope gets a 403, insufficient_scope. The directory scopes are for partner accounts.

ScopeLets the keyDirectory
me:readRead accountNo
store:readRead your storesNo
store:reviewsRead your reviewsNo
store:ordersSend ordersNo
store:catalogSend your catalogNo
store:writeReport your integrationNo
stores:readRead the vendor directoryYes
reviews:readRead all reviewsYes
partner:writeSend partner reportsYes

Errors

A failure answers with a 4xx or 5xx status and this body.

Error response
{
  "error": {
    "code": "rate_limited",
    "message": "This key sent too many requests in one second. Retry after the seconds in Retry-After.",
    "request_id": "3b9f4c1e-7a2d-4e8b-9c5f-1d2e3f4a5b6c"
  }
}

Branch on code. message is for people and may change. request_id matches the X-Request-Id header on every response, so quote it when you contact us. An invalid_body error adds error.details, the problems per field.

Errors every keyed endpoint can return

Each endpoint lists only its own errors, and these come on top. {scope} and {method} stand for the endpoint's own.

StatusCodeMessageWhen
401missing_api_keySend an API key in the Authorization header.No Authorization or X-Api-Key header.
401invalid_api_keyThis API key is not valid.The key does not match any issued key.
401revoked_api_keyThis API key has been revoked.The key was revoked.
401expired_api_keyThis API key has expired.The key is past its expiry date.
403insufficient_scopeThis API key does not have the {scope} scope.The key does not hold {scope}.
429rate_limitedThis key sent too many requests in one second. Retry after the seconds in Retry-After.More than 20 requests in one second from this key.
429monthly_quota_exceededThis account has used its monthly request quota. It resets on the first of the month, UTC.The account's pk_live_ keys made 10,000 requests this calendar month (UTC). Retry-After counts the seconds to the reset. Store keys have no monthly quota.
405method_not_allowedThis endpoint takes {method} only.Any other HTTP method. Checked before the key.
500internal_errorSomething failed on our side. Retry with backoff, and quote the request id if it keeps failing.Something failed on our side.

The public endpoints keep their older error bodies, and each lists its own.

Rate limits

Each key can make 20 requests a second. Each account can make 10,000 requests a calendar month, in UTC.

Each key has its own per-second limit. The monthly quota is per account, across its pk_live_ keys, and resets on the first of the month, UTC. Store keys have no monthly quota and never count toward it. Every request with a known key counts, failed ones included. /me is never rate limited, but it counts toward the month.

A 429 carries Retry-After, in seconds, and the code rate_limited or monthly_quota_exceeded. A request with a missing or unknown key is not counted, since it has no account to count against.

Your usage is in the dashboard, under API, Requests.

Idempotency

Every write takes an Idempotency-Key header of 1 to 200 characters. Send a new key with each call and the same key when you retry it. The same key and body within 24 hours gets the first answer and changes nothing.

The same key with another body is a 422, idempotency_key_reused. A repeat while the first call is still running is a 409, idempotency_key_in_progress, and you retry it. A failed call stores nothing, so its retry runs again.

Keys belong to the store on store writes and to the account on partner writes. The SDK sends one with every write and reuses it on each retry.

Pagination

Lists take limit, 1 to 100 and 20 by default, and cursor. Each page answers next_cursor. Send it back as cursor, with the same other parameters, for the next page. It is null on the last one.

The next page
curl "https://pepreviewpro.com/api/v1/my-reviews?store=example-store&cursor=eyJvIjoyMH0" \
  -H "Authorization: Bearer $PEPREVIEWPRO_API_KEY"

/stores and /products also answer total. In the SDK, iterate and syncAll walk every page for you.

Webhooks

A webhook tells a store's server when its reviews or settings change. The owner adds the endpoint on the Integrations page. How do I keep my pages current with webhooks?

Deliveries follow Standard Webhooks, with three headers.

HeaderWhat it holds
webhook-idThe delivery's id. It stays the same on every retry, so drop repeats by it.
webhook-timestampWhen it was sent, in Unix seconds.
webhook-signaturev1, and an HMAC-SHA256 of the id, the timestamp, and the raw body, keyed with your whsec_ secret.

The body is { type, timestamp, data }. data always carries the store's slug as store, so one handler can serve several stores.

Webhook body
{
  "type": "aggregate.changed",
  "timestamp": "2026-11-03T16:00:00.000Z",
  "data": {
    "average": 4.78,
    "total": 128,
    "distribution": {
      "1": 0,
      "2": 1,
      "3": 2,
      "4": 21,
      "5": 104
    },
    "store": "example-store"
  }
}
EventtypeWhen
Review publishedreview.publishedA review goes live.
Review updatedreview.updatedA live review is cleaned up, confirmed, or changes.
Review deletedreview.deletedA review is deleted or taken out of public view.
Store updatedstore.updatedBranding or widget settings are saved.
Rating changedaggregate.changedYour rating or review count changes.
TestpingThe endpoint is saved, so you can check it answers.

Reject a delivery whose timestamp is more than 5 minutes off. Answer with a 2xx. A failed delivery is retried for 24 hours, and an endpoint that fails for 3 days is turned off and its owner emailed. Check deliveries with prp.webhooks.verify(rawBody, headers) from the SDK, or with any Standard Webhooks library.

SDKs

The TypeScript samples use the server SDK. It runs on Node 20 or later, Bun, Deno, and edge runtimes, and returns the same fields in camelCase.

Install
pnpm add @pepreviewpro/server
PackageWhat it does
@pepreviewpro/serverOrders, catalog, reviews, webhooks, and the partner directory, on your server. The samples here use it.
@pepreviewpro/nextNext.js App Router: cached reviews, the webhook route, and JSON-LD.
@pepreviewpro/reactThe review widgets and the review form as React components.
@pepreviewpro/browserAttribution capture on pages with no React.
@pepreviewpro/aiA store directory tool for the AI SDK, for partner assistants.
@pepreviewpro/cliSetup, order imports, and checks, run as npx @pepreviewpro/cli.

It retries a 429 or a 5xx with backoff, and sends an Idempotency-Key with every write. Quick starts for Next.js and React are under Widgets, Integrations. How do I connect a store that isn't on WordPress?

Versioning

v1 is stable. We add fields, parameters, and endpoints, and we never remove or rename them. Build your client to ignore fields it does not know.

The public endpoints and the deprecated /my-orders still answer at their old names, like /functions/v1/api-v1-my-orders, on both old hosts. Every other endpoint that takes a key answers at /api/v1 only.

  • https://pepreviewpro.com/functions/v1
  • https://qzlfuygefaocfmjdoueg.supabase.co/functions/v1

Changelog

The endpoints that take a key now share one shape. This was the one change that renamed and removed things in v1.

  • A success answers with the resource itself. A failure answers with { error: { code, message, request_id } }, and code is what to branch on.
  • Stores are named by slug: store in a request, slug in a store object. store_id is gone.
  • Lists page with limit and cursor, and answer next_cursor. offset is gone.
  • Every write takes an Idempotency-Key header.
  • The per-second limit is per key. Store keys have no monthly quota.
  • Timestamps are UTC with milliseconds, and your own order and product ids always come back as strings.
  • /heartbeat needs store:write, and /vendor-miss and /partner-events need partner:write.
  • /my-store is gone: /my-stores and /my-reviews cover it. POST /my-orders keeps its old shape and its old paths.

Your stores

Read your stores and reviews, and send orders, your catalog, and check-ins. Call these with an API key or a store key.

Your key

GET/api/v1/me

The key you called with and how much of this month's quota is left.

Key
API key or plugin key
Scope
me:read
Rate limited
No
curl "https://pepreviewpro.com/api/v1/me" \
  -H "Authorization: Bearer $PEPREVIEWPRO_API_KEY"
Response
{
  "key": {
    "id": "0b6f1f9e-2f7c-4d3e-9a51-7c4f0e3b8d12",
    "key_prefix": "pk_live_Ab3x",
    "key_type": "public",
    "scopes": [
      "me:read",
      "store:read"
    ]
  },
  "limits": {
    "rate_per_second": 20,
    "monthly_quota": 10000,
    "used_this_month": 412,
    "remaining_this_month": 9588
  }
}

Good to know

rate_per_second is this key's own limit. The month figures are the account's, across its pk_live_ keys.

On a store key, monthly_quota and remaining_this_month are null: store keys have no monthly quota.

List your stores

GET/api/v1/my-stores

Every store the key reaches, by name, with its branding, widget settings, review totals, and shipping. A store key reaches only its own store.

Key
API key, store key, or plugin key
Scope
store:read
Rate limited
Yes
Pages
cursor

Query

  • storestring

    A store's slug, for that store alone.

  • limitintegerDefault 20

    Results per page, 1 to 100.

  • cursorstring

    next_cursor from the previous page. Send the same other parameters with it.

curl
curl "https://pepreviewpro.com/api/v1/my-stores?store=example-store" \
  -H "Authorization: Bearer $PEPREVIEWPRO_API_KEY"
Response
{
  "stores": [
    {
      "slug": "example-store",
      "name": "Example Store",
      "description": "Research peptides, tested in the US.",
      "website_url": "https://store.example",
      "logo_url": "https://cdn.pepreviewpro.com/logos/example-store.png",
      "accent_color": "#1f6feb",
      "rating": 4.78,
      "review_count": 128,
      "verified_review_count": 117,
      "ships_from": "US",
      "ships_to": [
        "CA",
        "GB",
        "US"
      ],
      "payment_methods": [
        "card",
        "crypto"
      ],
      "coa_url": "https://store.example/lab-results",
      "catalog_synced_at": "2026-10-30T03:31:12.000Z",
      "rating_distribution": {
        "1": 0,
        "2": 1,
        "3": 2,
        "4": 21,
        "5": 104
      },
      "widget_logo_url": null,
      "banner_url": null,
      "public_page_enabled": true,
      "profile_url": "https://pepreviewpro.com/vendors/example-store",
      "widget_settings": {
        "theme": "light",
        "display_mode": "single",
        "autoplay_speed": 5,
        "transition_type": "slide",
        "show_aggregate": true,
        "show_distribution": true,
        "show_dates": true,
        "show_verified_badge": true,
        "min_rating_display": 1,
        "ticker_speed": 8
      }
    }
  ],
  "next_cursor": null
}

Errors

StatusCodeMessageWhen
400invalid_cursorcursor is not one this endpoint sent. Start again without it.cursor is not a next_cursor this endpoint sent.
400invalid_limitlimit must be a whole number from 1 to 100.limit is anything else.
400uuid_not_acceptedstore takes the store's slug, like example-store, not a UUID.store is a UUID. Stores are addressed by slug.
404store_not_foundThis key does not reach that store.store is not one of the key's live stores.

Plus the errors every keyed endpoint can return.

Good to know

slug is what every other endpoint takes as store.

profile_url is null while the store's public page is off. widget_settings is null until the store saves them.

ships_to is empty when the store has not listed its countries.

Your reviews

GET/api/v1/my-reviews

One store's approved reviews, with its rating aggregate.

Key
API key, store key, or plugin key
Scope
store:reviews
Rate limited
Yes
Pages
cursor

Query

  • storestring

    The store's slug. Required when the key reaches more than one store.

  • product_refstring

    Your own id for the product, the one you send in orders and the catalog. Narrows the reviews and the aggregate.

  • updated_sincestring

    An ISO 8601 time. Only reviews changed since then, oldest change first, for keeping your own copy current.

  • sortnewest | oldest | highest | lowestDefault newest

    Review order. Ignored with updated_since.

  • min_ratingnumber

    Only reviews rated at least this. Does not change the aggregate.

  • verified_onlybooleanDefault false

    Only purchase-verified reviews. Does not change the aggregate.

  • limitintegerDefault 20

    Results per page, 1 to 100.

  • cursorstring

    next_cursor from the previous page. Send the same other parameters with it.

curl "https://pepreviewpro.com/api/v1/my-reviews?store=example-store" \
  -H "Authorization: Bearer $PEPREVIEWPRO_API_KEY"
Response
{
  "reviews": [
    {
      "id": "5f0c2a64-8a7e-4f53-9d0b-6a4f3f0f6b21",
      "store": "example-store",
      "product_ref": "bpc-157-10mg",
      "order_ref": "ORD-1001",
      "rating": 5,
      "title": "Fast and careful",
      "body": "Arrived in two days, packed cold, and matched the certificate of analysis.",
      "author": "Jennifer K.",
      "verified": true,
      "created_at": "2026-10-03T14:21:07.512Z",
      "updated_at": "2026-10-04T09:12:44.001Z"
    }
  ],
  "aggregate": {
    "rating": 4.78,
    "review_count": 128,
    "verified_review_count": 117,
    "distribution": {
      "1": 0,
      "2": 1,
      "3": 2,
      "4": 21,
      "5": 104
    }
  },
  "next_cursor": "eyJvIjoyMH0"
}

Errors

StatusCodeMessageWhen
400invalid_cursorcursor is not one this endpoint sent. Start again without it.cursor is not a next_cursor this endpoint sent.
400invalid_limitlimit must be a whole number from 1 to 100.limit is anything else.
400invalid_min_ratingmin_rating must be a number from 0 to 5.min_rating is anything else.
400invalid_sortsort must be newest, oldest, highest, or lowest.sort is anything else.
400invalid_updated_sinceupdated_since must be an ISO 8601 time.updated_since is not a time.
400store_requiredstore is required because this key reaches more than one store. /api/v1/my-stores lists them.No store and the key reaches several stores.
400uuid_not_acceptedstore takes the store's slug, like example-store, not a UUID.store is a UUID. Stores are addressed by slug.
404store_not_foundThis key does not reach that store.store is not one of the key's live stores, or the key reaches none.

Plus the errors every keyed endpoint can return.

Good to know

With updated_since, or a cursor from such a listing, reviews come in the order they last changed. Save the time you start a sweep and pass it as updated_since on the next one.

product_ref and order_ref are your own ids for the product and order, as strings. They are null when the review names none. A product_ref we have never seen matches no reviews.

author is the reviewer's first name and last initial. verified is true when the review is matched to an order.

Send orders (deprecated)

POST/api/v1/my-ordersDeprecated

Records completed orders and queues review requests. Use POST /api/v1/orders instead. This endpoint keeps its old shape until the stores that use it have moved.

Key
API key, store key, or plugin key
Scope
store:orders
Rate limited
Yes
Old path
/functions/v1/api-v1-my-orders

Body

  • store_idstringRequired

    The store's slug.

  • orderOrder

    One order. Send this or orders.

  • ordersOrder[]

    Up to 50 orders. Processing stops at the first one that fails.

  • Order.idintegerRequired

    The WooCommerce order number.

  • Order.emailstringRequired

    The buyer's email, up to 255 characters.

  • Order.namestring

    The buyer's full name, up to 200 characters.

  • Order.products{ id, name }[]

    Up to 100 products. The first one names the review request.

curl
curl -X POST "https://pepreviewpro.com/api/v1/my-orders" \
  -H "Authorization: Bearer $PEPREVIEWPRO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "store_id": "example-store",
    "order": {
      "id": 10432,
      "email": "jennifer@example.com",
      "name": "Jennifer Kim",
      "products": [
        {
          "id": 88,
          "name": "BPC-157 10mg"
        }
      ]
    }
  }'
Response
{
  "success": true,
  "store_id": "example-store",
  "results": [
    {
      "order_id": 10432,
      "queued": 1,
      "duplicates": 0
    },
    {
      "order_id": 10433,
      "queued": 0,
      "duplicates": 0,
      "skipped_reason": "no_active_campaign"
    }
  ]
}

Errors

StatusCodeMessageWhen
400invalid_bodyInvalid request body.The body is not valid JSON or fails validation. details lists the problems per field.
400uuid_not_acceptedstore_id must be a store slug (e.g. `example-store`), not an internal UUID.store_id is a UUID.
401expired_api_keyAPI key expiredThe key is past its expiry date.
401invalid_api_keyInvalid API keyThe key does not match any issued key.
401missing_api_keyMissing API keyNo Authorization or X-Api-Key header.
401revoked_api_keyAPI key revokedThe key was revoked.
403insufficient_scopeInsufficient scope — key needs store:ordersThe key does not hold store:orders.
404store_not_foundStore not found for this API key.store_id is not one of the key's stores.
405method_not_allowedMethod not allowedAny other HTTP method. Checked before the key.
429monthly_quota_exceededMonthly quota exceededThe account's monthly quota is used up. reason is monthly_quota.
429rate_limitedRate limit exceededToo many requests in one second from this key. reason is rate_limit.
500internal_errorInternal server errorSomething failed on our side. Retry with backoff.
500queue_failedFailed to queue emailsAn order could not be saved or queued. results shows how far it got.

Good to know

Failures here answer { success: false, error, code }, the shape this endpoint had before November 3, 2026.

Sending an order twice is safe. Repeats count as duplicates and are not mailed again.

Send orders

POST/api/v1/orders

Records orders from any storefront, with their status, totals, and where the buyer came from, and queues review requests when an order reaches the status your store asks for. Sending an order again updates it.

Key
API key, store key, or plugin key
Scope
store:orders
Rate limited
Yes

Headers

  • Idempotency-Keystring

    1 to 200 characters. A repeat with the same key and body within 24 hours gets the first answer and changes nothing.

Body

  • storestringRequired

    The store's slug.

  • importbooleanDefault false

    True for past orders. They are saved and credited, and no email is queued.

  • ordersOrder[]Required

    1 to 200 orders.

  • Order.idstring | integerRequired

    Your order id. It comes back as a string.

  • Order.statuspending | paid | completed | refunded | cancelledRequired

    Review requests are queued on completed, or also on paid when the store asks for it. refunded and cancelled skip any still queued.

  • Order.emailstring

    The buyer's email, up to 255 characters. No email, no review request.

  • Order.namestring

    The buyer's full name.

  • Order.customer_idstring | integer

    Your customer id. Only whole numbers are kept.

  • Order.total, Order.currencynumber, string

    The order total in major units, and its currency code.

  • Order.line_items{ product_id, variation_id, name, quantity, total }[]

    Up to 500 items. product_id is your own product id, as in the catalog. The first item names the review request.

  • Order.created_at, Order.paid_at, Order.completed_atstring

    ISO 8601 times.

  • Order.attribution{ landing_url, referrer, utm_source, utm_medium, utm_campaign, utm_content, utm_term, first_seen_at }

    Where the buyer's visit began. A prp_click_id in landing_url credits the order to the PepReviewPro click that sent them.

  • Order.customer_ip, Order.customer_user_agentstring

    The buyer's at checkout. We match them to a profile view, then keep only a hash.

  • Order.reviews_seen_atinteger

    Unix seconds when your reviews widget last showed the buyer your reviews.

curl -X POST "https://pepreviewpro.com/api/v1/orders" \
  -H "Authorization: Bearer $PEPREVIEWPRO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "store": "example-store",
    "orders": [
      {
        "id": "ORD-1001",
        "status": "completed",
        "email": "jennifer@example.com",
        "name": "Jennifer Kim",
        "total": 99,
        "currency": "USD",
        "line_items": [
          {
            "product_id": "bpc-157-10mg",
            "name": "BPC-157 10mg",
            "quantity": 1,
            "total": 99
          }
        ]
      }
    ]
  }'
Response
{
  "store": "example-store",
  "results": [
    {
      "id": "ORD-1001",
      "status": "created",
      "queued": 1,
      "duplicates": 0
    },
    {
      "id": "10433",
      "status": "unchanged",
      "queued": 0,
      "duplicates": 1
    }
  ]
}

Errors

StatusCodeMessageWhen
400invalid_bodyThe request body is not valid JSON or does not match this endpoint.The body is not valid JSON or fails validation. error.details lists the problems per field.
400invalid_idempotency_keyIdempotency-Key must be 1 to 200 characters.The Idempotency-Key header is empty or longer.
400uuid_not_acceptedstore takes the store's slug, like example-store, not a UUID.store is a UUID. Stores are addressed by slug.
404store_not_foundThis key does not reach that store.store is not one of the key's live stores.
409idempotency_key_in_progressA request with this Idempotency-Key is still running. Retry in a moment.The first request with this key has not finished. Retry it.
422idempotency_key_reusedThis Idempotency-Key was sent with a different body. Use a new key for a new request.The key was used in the last 24 hours with another body.

Plus the errors every keyed endpoint can return.

Good to know

status in each result is created, updated, or unchanged, so a re-send of recent orders is safe and shows what it changed.

A field you leave out keeps what we have, so { id, status } alone updates the status. A review request it queues uses the email and items sent earlier.

Attribution, customer_ip, customer_user_agent, and reviews_seen_at are what an order's credit is worked out from. Send any of them and the credit is worked out again from what you sent; send none and the stored credit stays.

skipped_reason says why nothing was queued: import, not_trigger_status, nothing_to_email (no email or no items), or no_active_campaign.

A 500 may leave some orders saved. Retry the same call: saved orders come back unchanged and are never mailed twice.

Send a new Idempotency-Key for each call and the same one when you retry it, and a retry after a timeout or a 5xx is never applied twice. Keys are per store, the body is compared with its keys sorted, and a failed call stores nothing, so its retry runs again.

Send your catalog

PUT/api/v1/catalog

Replaces your store's product list on PepReviewPro, with where you ship and how buyers pay. Products you leave out are hidden, never deleted, since reviews point at them.

Key
API key or store key
Scope
store:catalog
Rate limited
Yes

Headers

  • Idempotency-Keystring

    1 to 200 characters. A repeat with the same key and body within 24 hours gets the first answer and changes nothing.

Body

  • storestringRequired

    The store's slug.

  • productsProduct[]Required

    Your whole published catalog, up to 5,000 products. A product that does not parse is skipped.

  • Product.idstring | integerRequired

    Your product id, the same one your orders send.

  • Product.namestringRequired

    The product's name.

  • Product.short_description, Product.url, Product.image_urlstring

    HTML is stripped from the description.

  • Product.price_min, Product.price_max, Product.currencynumber, number, string

    Prices in major units. price_max defaults to price_min.

  • Product.in_stockbooleanDefault true

    Whether it can be bought now.

  • ships_tostring[]

    Two-letter country codes. Applied when the list differs from the one you sent last, so an owner's edit survives an unchanged push.

  • ships_fromstring

    A two-letter country code. Used until the owner saves their own.

  • payment_methods(card | crypto | bankTransfer | zelle | cashApp | venmo | paypal | other)[]

    How buyers pay. Used only while the store has none saved.

curl -X PUT "https://pepreviewpro.com/api/v1/catalog" \
  -H "Authorization: Bearer $PEPREVIEWPRO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "store": "example-store",
    "products": [
      {
        "id": "bpc-157-10mg",
        "name": "BPC-157 10mg",
        "url": "https://store.example/p/bpc-157",
        "price_min": 49.5,
        "currency": "USD",
        "in_stock": true
      }
    ],
    "ships_to": [
      "US",
      "CA"
    ]
  }'
Response
{
  "store": "example-store",
  "products": 120,
  "mapped": 97,
  "skipped": 2
}

Errors

StatusCodeMessageWhen
400invalid_bodyThe request body is not valid JSON or does not match this endpoint.The body is not valid JSON or fails validation. error.details lists the problems per field.
400invalid_idempotency_keyIdempotency-Key must be 1 to 200 characters.The Idempotency-Key header is empty or longer.
400uuid_not_acceptedstore takes the store's slug, like example-store, not a UUID.store is a UUID. Stores are addressed by slug.
404store_not_foundThis key does not reach that store.store is not one of the key's live stores.
409idempotency_key_in_progressA request with this Idempotency-Key is still running. Retry in a moment.The first request with this key has not finished. Retry it.
422idempotency_key_reusedThis Idempotency-Key was sent with a different body. Use a new key for a new request.The key was used in the last 24 hours with another body.

Plus the errors every keyed endpoint can return.

Good to know

Leave out ships_to, ships_from, or payment_methods to keep what the store has.

mapped counts the products matched to a compound in the library. Once you send a catalog, we stop reading it from your site.

Send a new Idempotency-Key for each call and the same one when you retry it, and a retry after a timeout or a 5xx is never applied twice. Keys are per store, the body is compared with its keys sorted, and a failed call stores nothing, so its retry runs again.

Report your integration

POST/api/v1/heartbeat

Tells us which SDK version your store runs and what it does, so the dashboard can show the connection and name anything missing.

Key
API key or store key
Scope
store:write
Rate limited
Yes

Headers

  • Idempotency-Keystring

    1 to 200 characters. A repeat with the same key and body within 24 hours gets the first answer and changes nothing.

Body

  • storestringRequired

    The store's slug.

  • sdk_versionstring

    The SDK's version.

  • packages{ [name]: version }

    Up to 50 installed packages and their versions.

  • framework, runtimestring

    Like next@16.4.2 and node@22.11.0.

  • capabilities(orders | attribution | reviewsSeen | catalog | webhooks | jsonLd)[]

    What the install does. Values we do not know are dropped.

curl -X POST "https://pepreviewpro.com/api/v1/heartbeat" \
  -H "Authorization: Bearer $PEPREVIEWPRO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "store": "example-store",
    "sdk_version": "0.1.0",
    "framework": "next@16.4.2",
    "runtime": "node@22.11.0",
    "capabilities": [
      "orders"
    ]
  }'
Response
{
  "store": "example-store",
  "received_at": "2026-11-03T16:00:00.000Z"
}

Errors

StatusCodeMessageWhen
400invalid_bodyThe request body is not valid JSON or does not match this endpoint.The body is not valid JSON or fails validation. error.details lists the problems per field.
400invalid_idempotency_keyIdempotency-Key must be 1 to 200 characters.The Idempotency-Key header is empty or longer.
400uuid_not_acceptedstore takes the store's slug, like example-store, not a UUID.store is a UUID. Stores are addressed by slug.
404store_not_foundThis key does not reach that store.store is not one of the key's live stores.
409idempotency_key_in_progressA request with this Idempotency-Key is still running. Retry in a moment.The first request with this key has not finished. Retry it.
422idempotency_key_reusedThis Idempotency-Key was sent with a different body. Use a new key for a new request.The key was used in the last 24 hours with another body.

Plus the errors every keyed endpoint can return.

Good to know

The SDK sends one when it starts and once a day. A field you leave out keeps what we have.

An empty capabilities list keeps the stored one, so init can check in before the install reports what it does. A list with values replaces it.

Send a new Idempotency-Key for each call and the same one when you retry it, and a retry after a timeout or a 5xx is never applied twice. Keys are per store, the body is compared with its keys sorted, and a failed call stores nothing, so its retry runs again.

Directory

Every store in the vendor directory, its products, and its reviews. For partner accounts.

Vendor directory

GET/api/v1/stores

Every store partners may show, ranked. A store is listed while its public page is on and its owner is on the Growth or Professional plan.

Key
API key
Scope
stores:read
Rate limited
Yes
Pages
cursor

Query

  • searchstring

    Only stores whose name contains this. It never matches products or countries.

  • min_ratingnumber

    Only stores whose score is at least this, 0 to 5.

  • compoundstring

    A compound slug, name, or alias from the ChatPEP library. Only stores with an active product that maps to it.

  • ships_tostring

    A two-letter country code. Only stores that list it. A store that lists no countries is left out.

  • paymentcard | crypto | bankTransfer | zelle | cashApp | venmo | paypal | other

    Only stores that take this payment method.

  • in_stockbooleanDefault false

    With compound, only stores that have a matching product in stock.

  • sortbest | reviews | rating | nameDefault best

    best weighs the score by review volume; rating is the score alone.

  • limitintegerDefault 20

    Results per page, 1 to 100.

  • cursorstring

    next_cursor from the previous page. Send the same other parameters with it.

curl "https://pepreviewpro.com/api/v1/stores?compound=bpc-157&ships_to=US" \
  -H "Authorization: Bearer $PEPREVIEWPRO_API_KEY"
Response
{
  "stores": [
    {
      "slug": "example-store",
      "name": "Example Store",
      "description": "Research peptides, tested in the US.",
      "website_url": "https://store.example",
      "logo_url": "https://cdn.pepreviewpro.com/logos/example-store.png",
      "accent_color": "#1f6feb",
      "rating": 4.78,
      "review_count": 128,
      "verified_review_count": 117,
      "ships_from": "US",
      "ships_to": [
        "CA",
        "GB",
        "US"
      ],
      "payment_methods": [
        "card",
        "crypto"
      ],
      "coa_url": "https://store.example/lab-results",
      "catalog_synced_at": "2026-10-30T03:31:12.000Z",
      "score": 4.71,
      "profile_url": "https://pepreviewpro.com/vendors/example-store?utm_source=example-partner&utm_medium=partner",
      "click_url": "https://pepreviewpro.com/go/eyJzaXRlSWQiOiI4MTAwZGUxNSJ9.Zk2pQ9vR2mT8sLwYc1bNx7Uf",
      "matched_products": [
        {
          "ref": "bpc-157-10mg",
          "name": "BPC-157 10mg",
          "page_url": "https://store.example/product/bpc-157-10mg",
          "click_url": "https://pepreviewpro.com/go/eyJzaXRlSWQiOiI4MTAwZGUxNSIsInByb2R1Y3RJZCI6IjBiNmYyYTdlIn0.q3V9mXcT2bLr8nWkP4sYd1Ae",
          "image_url": "https://cdn.pepreviewpro.com/products/8100de15-5438-4c9f-94aa-2969cd1bb9f7/412/3f1c.webp",
          "price_min": 89,
          "price_max": 89,
          "currency": "USD",
          "in_stock": true,
          "review_count": 14,
          "store": {
            "slug": "example-store",
            "name": "Example Store",
            "website_url": "https://store.example"
          }
        }
      ],
      "matched_product_count": 4
    }
  ],
  "total": 1,
  "next_cursor": null,
  "filters": {
    "compound": {
      "slug": "bpc-157",
      "name": "BPC-157"
    },
    "unknown_compound": false,
    "ships_to": "GB",
    "payment": null,
    "in_stock": false,
    "min_rating": null
  }
}

Errors

StatusCodeMessageWhen
400invalid_cursorcursor is not one this endpoint sent. Start again without it.cursor is not a next_cursor this endpoint sent.
400invalid_in_stockin_stock must be true or false.in_stock is anything else.
400invalid_limitlimit must be a whole number from 1 to 100.limit is anything else.
400invalid_min_ratingmin_rating must be a number from 0 to 5.min_rating is anything else.
400invalid_paymentpayment must be one of: card, crypto, bankTransfer, zelle, cashApp, venmo, paypal, other.payment is anything else.
400invalid_ships_toships_to must be a two-letter country code, like GB.ships_to is not a country code.
400invalid_sortsort must be best, reviews, rating, or name.sort is anything else.

Plus the errors every keyed endpoint can return.

Good to know

total counts the stores left after every filter, min_rating included.

rating is the stores' average star rating. score is the PepReviewPro score that min_rating and sort=rating compare.

matched_products is there only when compound was passed: up to three of the store's matching products, in stock first, then cheapest. matched_product_count beside it counts all of them, so you can tell a store has more.

ships_to is empty when a store has not listed its countries. That means unknown, so a ships_to filter leaves the store out.

A compound outside the library matches nothing and answers an empty list, with filters.unknown_compound true.

filters echoes what was applied: the compound as the library names it, or null, and the other filters as read, null when not passed. unknown_compound is true when compound was passed and is not in the library, so an empty list means we do not know the compound rather than that no store has it.

On a partner key, send buyers through click_url and profile_url. Tracked links, in the overview, says how.

Products by compound

GET/api/v1/products

Every product partners may show for one compound, with its store. Only stores on the Growth or Professional plan are included.

Key
API key
Scope
stores:read
Rate limited
Yes
Pages
cursor

Query

  • compoundstringRequired

    A compound slug, name, or alias from the ChatPEP library.

  • ships_tostring

    A two-letter country code. Only stores that list it. A store that lists no countries is left out.

  • paymentcard | crypto | bankTransfer | zelle | cashApp | venmo | paypal | other

    Only stores that take this payment method.

  • in_stockbooleanDefault false

    Only products the store has in stock.

  • min_ratingnumber

    Only products from stores whose score is at least this, 0 to 5.

  • storestring

    Only products from these stores: one or more store slugs, comma separated, like example-store. Applied before paging, so total counts only their products.

  • sortbest | price | ratingDefault best

    best ranks by the store's score weighed by review volume, then price. price is cheapest first.

  • limitintegerDefault 20

    Results per page, 1 to 100.

  • cursorstring

    next_cursor from the previous page. Send the same other parameters with it.

curl "https://pepreviewpro.com/api/v1/products?compound=bpc-157" \
  -H "Authorization: Bearer $PEPREVIEWPRO_API_KEY"
Response
{
  "products": [
    {
      "ref": "bpc-157-10mg",
      "name": "BPC-157 10mg",
      "page_url": "https://store.example/product/bpc-157-10mg",
      "click_url": "https://pepreviewpro.com/go/eyJzaXRlSWQiOiI4MTAwZGUxNSIsInByb2R1Y3RJZCI6IjBiNmYyYTdlIn0.q3V9mXcT2bLr8nWkP4sYd1Ae",
      "image_url": "https://cdn.pepreviewpro.com/products/8100de15-5438-4c9f-94aa-2969cd1bb9f7/412/3f1c.webp",
      "price_min": 89,
      "price_max": 89,
      "currency": "USD",
      "in_stock": true,
      "review_count": 14,
      "store": {
        "slug": "example-store",
        "name": "Example Store",
        "website_url": "https://store.example"
      }
    }
  ],
  "total": 1,
  "next_cursor": null,
  "filters": {
    "compound": {
      "slug": "bpc-157",
      "name": "BPC-157"
    },
    "unknown_compound": false,
    "ships_to": null,
    "payment": null,
    "in_stock": false,
    "min_rating": null
  }
}

Errors

StatusCodeMessageWhen
400invalid_cursorcursor is not one this endpoint sent. Start again without it.cursor is not a next_cursor this endpoint sent.
400invalid_in_stockin_stock must be true or false.in_stock is anything else.
400invalid_limitlimit must be a whole number from 1 to 100.limit is anything else.
400invalid_min_ratingmin_rating must be a number from 0 to 5.min_rating is anything else.
400invalid_paymentpayment must be one of: card, crypto, bankTransfer, zelle, cashApp, venmo, paypal, other.payment is anything else.
400invalid_ships_toships_to must be a two-letter country code, like GB.ships_to is not a country code.
400invalid_sortsort must be best, price, or rating.sort is anything else.
400missing_compoundcompound is required.No compound.
400uuid_not_acceptedstore takes the store's slug, like example-store, not a UUID.A store slug is a UUID. Stores are addressed by slug.

Plus the errors every keyed endpoint can return.

Good to know

ref is the store's own id for the product. Prices are in the store's own currency, in major units.

total counts every product that matches. An empty list means nothing qualifies. Filters are never dropped to fill it.

A store slug that is not a partner store matches nothing.

Each product's store carries the store's website_url. A to on a click_url must be on that host.

A product's click_url goes to the product page, or to the store when the product is gone by the time it is clicked.

filters echoes what was applied: the compound as the library names it, or null, and the other filters as read, null when not passed. unknown_compound is true when compound was passed and is not in the library, so an empty list means we do not know the compound rather than that no store has it.

On a partner key, send buyers through click_url and profile_url. Tracked links, in the overview, says how.

Directory reviews

GET/api/v1/reviews

Approved reviews from every store in the vendor directory feed, or one of them, newest first.

Key
API key
Scope
reviews:read
Rate limited
Yes
Pages
cursor

Query

  • storestring

    The slug of a store in the vendor directory feed.

  • min_ratingnumber

    Only reviews rated at least this.

  • limitintegerDefault 20

    Results per page, 1 to 100.

  • cursorstring

    next_cursor from the previous page. Send the same other parameters with it.

curl "https://pepreviewpro.com/api/v1/reviews?store=example-store" \
  -H "Authorization: Bearer $PEPREVIEWPRO_API_KEY"
Response
{
  "reviews": [
    {
      "id": "5f0c2a64-8a7e-4f53-9d0b-6a4f3f0f6b21",
      "store": "example-store",
      "product_ref": "bpc-157-10mg",
      "rating": 5,
      "title": "Fast and careful",
      "body": "Arrived in two days, packed cold, and matched the certificate of analysis.",
      "author": "Jennifer K.",
      "verified": true,
      "created_at": "2026-10-03T14:21:07.512Z",
      "updated_at": "2026-10-04T09:12:44.001Z"
    }
  ],
  "next_cursor": "eyJvIjoyMH0"
}

Errors

StatusCodeMessageWhen
400invalid_cursorcursor is not one this endpoint sent. Start again without it.cursor is not a next_cursor this endpoint sent.
400invalid_limitlimit must be a whole number from 1 to 100.limit is anything else.
400invalid_min_ratingmin_rating must be a number from 0 to 5.min_rating is anything else.
400uuid_not_acceptedstore takes the store's slug, like example-store, not a UUID.store is a UUID. Stores are addressed by slug.
404store_not_foundNo store in the partner directory has that slug.store is not in the vendor directory feed.

Plus the errors every keyed endpoint can return.

Good to know

The order is fixed, newest first, and there is no product filter.

Reviews here carry no order_ref.

Report a store you could not show

POST/api/v1/vendor-miss

Tells us your user asked about a store you could not show, so we can invite it. Send it whenever a store name finds nothing.

Key
API key
Scope
partner:write
Rate limited
Yes

Headers

  • Idempotency-Keystring

    1 to 200 characters. A repeat with the same key and body within 24 hours gets the first answer and changes nothing.

Body

  • namestringRequired

    The store's name as your user wrote it. We keep the first 120 characters.

  • urlstring

    The store's address, when your user pasted one.

  • questionstring

    Your user's question as they asked it. We keep the first 200 characters. Leave out anything that identifies them.

curl -X POST "https://pepreviewpro.com/api/v1/vendor-miss" \
  -H "Authorization: Bearer $PEPREVIEWPRO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Value Research Labs",
    "question": "Is Value Research Labs legit?"
  }'
Response
{
  "in_directory": false,
  "listed_with_partners": false
}

Errors

StatusCodeMessageWhen
400invalid_bodyThe request body is not valid JSON or does not match this endpoint.The body is not a JSON object.
400invalid_idempotency_keyIdempotency-Key must be 1 to 200 characters.The Idempotency-Key header is empty or longer.
400missing_namename is required.No name, or only spaces.
409idempotency_key_in_progressA request with this Idempotency-Key is still running. Retry in a moment.The first request with this key has not finished. Retry it.
422idempotency_key_reusedThis Idempotency-Key was sent with a different body. Use a new key for a new request.The key was used in the last 24 hours with another body.

Plus the errors every keyed endpoint can return.

Good to know

in_directory is true for a store on PepReviewPro. listed_with_partners is true when partners may show it, so a search missed it.

Tell your user only that the store is not reviewed on PepReviewPro.

A name over 120 characters is cut to 120 and recorded.

Send a new Idempotency-Key for each call and the same one when you retry it, and a retry after a timeout or a 5xx is never applied twice. Keys are per account, the body is compared with its keys sorted, and a failed call stores nothing, so its retry runs again.

Report what you showed

POST/api/v1/partner-events

Tells us which stores you showed your users, and the clicks you sent to a store without click_url. Stores see these figures in their insights. Partner keys only.

Key
API key
Scope
partner:write
Rate limited
Yes

Headers

  • Idempotency-Keystring

    1 to 200 characters. A repeat with the same key and body within 24 hours gets the first answer and changes nothing.

Body

  • eventsarrayRequired

    Up to 50 events. Each is an object with the fields below.

  • events[].idstringRequired

    Your own id for the event, 1 to 100 characters. An id you already sent in the last 7 days counts as a duplicate.

  • events[].typeshown | clickRequired

    shown for a store you showed. click for a buyer you sent to the store by a link other than click_url.

  • events[].storestringRequired

    The store's slug from /stores or /products.

  • events[].product_refstring

    The product's ref from /products, when a click went to a product.

  • events[].compoundstring

    The compound your user asked about: a slug, name, or alias.

  • events[].placementstoreHeader | storeAbout | directoryCard | finderProduct | finderStore | offerCard | returnToStore

    Where the link was, like finderProduct or finderStore.

curl -X POST "https://pepreviewpro.com/api/v1/partner-events" \
  -H "Authorization: Bearer $PEPREVIEWPRO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "id": "evt_10432",
        "type": "shown",
        "store": "example-store",
        "compound": "bpc-157",
        "placement": "finderStore"
      }
    ]
  }'
Response
{
  "recorded": 2,
  "duplicates": 1,
  "skipped": 0
}

Errors

StatusCodeMessageWhen
400invalid_eventsevents must be an array.The body has no events list.
400invalid_idempotency_keyIdempotency-Key must be 1 to 200 characters.The Idempotency-Key header is empty or longer.
400too_many_eventsSend at most 50 events per call.events has more than 50 items.
403not_partner_keyThis endpoint takes partner keys only. Ask us to link your key to your partner account.The key is not linked to a partner.
409idempotency_key_in_progressA request with this Idempotency-Key is still running. Retry in a moment.The first request with this key has not finished. Retry it.
422idempotency_key_reusedThis Idempotency-Key was sent with a different body. Use a new key for a new request.The key was used in the last 24 hours with another body.

Plus the errors every keyed endpoint can return.

Good to know

Safe to call without waiting. An event with no id, no type, or an unknown store is skipped and counted in skipped. An optional field that is malformed is dropped, and the event still counts.

duplicates counts events whose id you already sent in the last 7 days, or that repeat an id within the call. They are not counted again.

Once you report shown events, they are your impressions in each store's insights, and your API reads stop counting. If you send none for 7 days, your reads count again.

Use click_url wherever you can. A click event counts the visit, but its order cannot be matched to you.

Send a new Idempotency-Key for each call and the same one when you retry it, and a retry after a timeout or a 5xx is never applied twice. Keys are per account, the body is compared with its keys sorted, and a failed call stores nothing, so its retry runs again.

Public

No key needed. The widgets and review forms call these.

Widget data

GET/api/v1/widget-data

What the review widgets render: store branding, widget settings, recent reviews, and the aggregate.

Key
None
Old path
/functions/v1/get-widget-data

Query

  • site_idstringRequired

    The store's slug or id.

  • product_idinteger

    Only this product's reviews and aggregate. Digits only.

  • product_refstring | integer

    Your own id for the product, as your orders and catalog send it. Used instead of product_id. One we have never seen matches no reviews.

  • limitintegerDefault 20

    Reviews to send, 1 to 50.

curl "https://pepreviewpro.com/api/v1/widget-data?site_id=example-store"
Response
{
  "success": true,
  "requested_site_id": "example-store",
  "resolved_site_id": "8100de15-5438-4c9f-94aa-2969cd1bb9f7",
  "store": {
    "id": "8100de15-5438-4c9f-94aa-2969cd1bb9f7",
    "slug": "example-store",
    "name": "Example Store",
    "accent_color": "#1f6feb",
    "widget_logo_url": null,
    "public_page_enabled": true,
    "app_url": "https://pepreviewpro.com"
  },
  "settings": {
    "theme": "light",
    "autoplay_speed": 5,
    "ticker_speed": 8,
    "show_aggregate": true,
    "show_distribution": true,
    "show_verified_badge": true,
    "min_rating_display": 1,
    "cache_bust_token": "2026-10-01T09:00:00.000000+00:00"
  },
  "reviews": [
    {
      "id": "5f0c2a64-8a7e-4f53-9d0b-6a4f3f0f6b21",
      "reviewer_name": "Jennifer K.",
      "rating": 5,
      "title": "Fast and careful",
      "body": "Arrived in two days, packed cold, and matched the certificate of analysis.",
      "is_verified": true,
      "created_at": "2026-10-03T14:21:07.512934+00:00",
      "product_ref": "88",
      "order_ref": null,
      "updated_at": "2026-10-04T09:12:44.001207+00:00"
    }
  ],
  "aggregate": {
    "average_rating": 4.78,
    "score": 4.71,
    "total": 128,
    "distribution": {
      "1": 0,
      "2": 1,
      "3": 2,
      "4": 21,
      "5": 104
    }
  }
}

Errors

StatusMessageWhen
400site_id is requiredNo site_id.
404Store not foundNo live store has that slug or id.
500Internal server errorSomething failed on our side.

Good to know

Errors on the public endpoints say message, not error.

Responses are never cached. X-Store-Scope echoes site_id.

Each review's product_ref is your own product id. order_ref is always null here, since anyone can read this response.

Store reviews (deprecated)

GET/api/v1/store-reviews

A page of one store's approved reviews. Use /my-reviews with a key instead.

Key
None
Old path
/functions/v1/get-reviews

Query

  • site_idstringRequired

    The store's slug or id.

  • product_idinteger

    Only this product's reviews and aggregate.

  • pageintegerDefault 1

    Page number.

  • per_pageintegerDefault 20

    Reviews per page, up to 200.

  • sortnewest | highest | lowestDefault newest

    Anything else sorts newest first.

  • verified_onlybooleanDefault false

    Only purchase-verified reviews.

curl
curl "https://pepreviewpro.com/api/v1/store-reviews?site_id=example-store"
Response
{
  "success": true,
  "requested_site_id": "example-store",
  "resolved_site_id": "8100de15-5438-4c9f-94aa-2969cd1bb9f7",
  "reviews": [
    {
      "id": "5f0c2a64-8a7e-4f53-9d0b-6a4f3f0f6b21",
      "reviewer_name": "Jennifer K.",
      "rating": 5,
      "title": "Fast and careful",
      "body": "Arrived in two days, packed cold, and matched the certificate of analysis.",
      "is_verified": true,
      "created_at": "2026-10-03T14:21:07.512934+00:00"
    }
  ],
  "aggregate": {
    "average_rating": 4.78,
    "score": 4.71,
    "total": 128,
    "verified": 117,
    "distribution": {
      "1": 0,
      "2": 1,
      "3": 2,
      "4": 21,
      "5": 104
    }
  },
  "page": 1,
  "per_page": 20,
  "total_count": 128,
  "deprecated": true
}

Errors

StatusMessageWhen
400site_id is requiredNo site_id.
404Store not foundNo live store has that slug or id.
500Internal server errorSomething failed on our side.

Good to know

total_count counts every approved review, even with verified_only.

Responses carry Deprecation: true.

Store profile

GET/api/v1/store-profile

A store's public branding and widget settings.

Key
None
Old path
/functions/v1/get-store-profile

Query

  • site_idstringRequired

    The store's slug or id.

curl
curl "https://pepreviewpro.com/api/v1/store-profile?site_id=example-store"
Response
{
  "success": true,
  "requested_site_id": "example-store",
  "resolved_site_id": "8100de15-5438-4c9f-94aa-2969cd1bb9f7",
  "store": {
    "id": "8100de15-5438-4c9f-94aa-2969cd1bb9f7",
    "slug": "example-store",
    "name": "Example Store",
    "wp_url": "https://store.example",
    "logo_url": "https://cdn.pepreviewpro.com/logos/example-store.png",
    "widget_logo_url": null,
    "banner_url": null,
    "accent_color": "#1f6feb",
    "business_description": "Research peptides, tested in the US."
  },
  "widget_settings": null
}

Errors

StatusMessageWhen
400site_id is requiredNo site_id.
403Store profile is not publicThe store's public page is off.
404Store not foundNo live store has that slug or id.
500Internal server errorSomething failed on our side.

Submit a review

POST/api/v1/submit-review

The review form on store sites. The review waits until its author confirms it from the email we send.

Key
None
Old path
/functions/v1/submit-review

Body

  • site_idstringRequired

    The store's id.

  • reviewer_namestringRequired

    Shown as first name and last initial.

  • reviewer_emailstringRequired

    Where the confirmation link goes. Never shown.

  • ratingintegerRequired

    1 to 5.

  • bodystringRequired

    The review text.

  • titlestring

    Accepted but ignored. We write the headline from the review text.

  • product_idinteger

    The product reviewed. Leave it out for a store review.

  • order_idinteger

    The order it came from, when known.

  • product_refstring | integer

    Your own product id. Used when product_id is left out.

  • order_refstring | integer

    Your own order id. Used when order_id is left out.

curl
curl -X POST "https://pepreviewpro.com/api/v1/submit-review" \
  -H "Content-Type: application/json" \
  -d '{
    "site_id": "YOUR_STORE_ID",
    "reviewer_name": "Jennifer K.",
    "reviewer_email": "jennifer@example.com",
    "rating": 5,
    "title": "Fast and careful",
    "body": "Arrived in two days, packed cold, and matched the certificate of analysis."
  }'
Response
{
  "success": true,
  "message": "Thank you! Please check your email to verify your review.",
  "review_id": "5f0c2a64-8a7e-4f53-9d0b-6a4f3f0f6b21"
}

Errors

StatusMessageWhen
400Missing required fieldsA required field is empty.
400Rating must be between 1 and 5rating is out of range.
500Failed to save reviewThe review could not be saved.
500Internal server errorThe body is not JSON.

Good to know

site_id: "demo" answers success without saving anything. The homepage form uses it.

Confirm a review

GET/verify-review

The link in the confirmation email. Redirects to /thank-you.

Key
None
Old path
/functions/v1/verify-review

Query

  • tokenstringRequired

    From the email.

curl
curl "https://pepreviewpro.com/verify-review?token=TOKEN_FROM_EMAIL"
Response
302 to https://pepreviewpro.com/thank-you

Errors

StatusMessageWhen
200Review already verifiedThe link was already used. Plain text.
400Missing tokenNo token. Plain text.
404Invalid or expired verification tokenNo review has that token. Plain text.

Verified vendors

GET/api/v1/vendors

The public directory, ranked by rank_score, which also counts the share of purchase-verified reviews.

Key
None
Old path
/functions/v1/list-vendors

Headers

  • Authorizationstring

    Optional. A partner's key, as Bearer pk_live_…, counts each store as served in its insights. The answer is the same without one.

curl
curl "https://pepreviewpro.com/api/v1/vendors"
Response
{
  "success": true,
  "vendors": [
    {
      "id": "8100de15-5438-4c9f-94aa-2969cd1bb9f7",
      "name": "Example Store",
      "slug": "example-store",
      "logo_url": "https://cdn.pepreviewpro.com/logos/example-store.png",
      "accent_color": "#1f6feb",
      "business_description": "Research peptides, tested in the US.",
      "wp_url": "https://store.example",
      "review_count": 128,
      "verified_review_count": 117,
      "avg_rating": 4.78,
      "score": 4.71,
      "rank_score": 10.1237
    }
  ]
}

Errors

StatusMessageWhen
500Internal server errorSomething failed on our side. message carries it.

Good to know

Cached for 60 seconds.

Unsubscribe

POST/api/v1/unsubscribe

One-click unsubscribe from review request emails. Mail clients post here from the List-Unsubscribe header.

Key
None
Old path
/functions/v1/unsubscribe

Query

  • tokenstringRequired

    The email's id, from the link. A JSON body { token } also works.

curl
curl -X POST "https://pepreviewpro.com/api/v1/unsubscribe?token=TOKEN_FROM_EMAIL"
Response
{
  "ok": true
}

Errors

StatusMessageWhen
400This link is not valid.The token is missing or malformed.
404This link is not valid.No email has that id.
405Method not allowedAnything but GET or POST.
500Something went wrong. Please try again.The opt-out could not be saved.

Good to know

Errors here say error and success says ok, unlike the other public endpoints.

A GET never unsubscribes. It answers 303 to the /unsubscribe page, where the reader confirms, because some mail clients open the link instead of posting to it.

Affiliate click

POST/api/v1/affiliate/click

Records a click on a vendor offer. Add the returned click_id to the vendor link as prp_click_id.

Key
None
Old path
/functions/v1/affiliate-click

Body

  • vendor_keystringRequired

    The vendor's slug. store_name or store_slug also work.

  • site_idstring

    The store's id, when known.

  • offer_codestring

    Defaults to the offer's code.

  • placementstring

    Where the offer was shown, like store_profile.

  • session_id, landing_path, referrer, utm_*string

    Attribution for reporting.

curl
curl -X POST "https://pepreviewpro.com/api/v1/affiliate/click" \
  -H "Content-Type: application/json" \
  -d '{
    "vendor_key": "example-store",
    "placement": "store_profile"
  }'
Response
{
  "success": true,
  "click_id": "c7a1d2e4-55b0-4f7e-9c1a-0e6d2b3f4a59"
}

Errors

StatusMessageWhen
400vendor_key is requiredNo vendor named.
405Method not allowedAnything but POST.
500Failed to record clickThe click could not be saved.