Endpoints
| Method | Endpoint | Description |
| GET | /sites/:id/offers | List offers of a site |
| POST | /sites/:id/offers | Create offer |
| GET | /offers/:oid | Offer detail (includes gallery and category ids) |
| PATCH | /offers/:oid | Update — full object required (see below) |
| DELETE | /offers/:oid | Delete offer, its reviews and gallery |
| PUT | /sites/:id/offers/order | Bulk rank: {"order": ["uuid", …]} — position in list = rank |
| GET | /offers/:oid/link-checks | Link health history (plan-gated: link_health) |
| POST | /offers/:oid/link-check | Trigger a link check now (plan-gated) |
The offer payload — every field
Create and update share the same shape. Names are stable — the panel reads exactly these keys.
Basics
| Field | Type | Description |
name required | string | Product name — shown everywhere, used in H1 fallback. |
slug | string | URL slug. Empty on create → generated from name. Review page lives at /reviews/:slug/. |
status | string | active (default) | draft | archived. Only active offers render publicly. |
url | string | Product homepage — displayed on the review page (unless hidden in appearance settings) and used by the screenshot worker. |
description | string (HTML) | The full editorial review — your main content for the offer page. |
summary | string | Short verdict shown on cards and used as meta description fallback. |
custom_header | string | Optional heading replacing the default above the review copy. |
custom_top10_text | string | Override blurb shown in toplist cards instead of the summary. |
ribbon_text | string | Badge on the card, e.g. "Editor's Choice". Empty = no ribbon. |
author_id | uuid|null | Reviewer byline — reference an author from Articles & authors. |
logo | string|null | Logo image URL (from the media library). Empty → card falls back to the first gallery image, then to a monogram. |
Categories & ranking
| Field | Type | Description |
category_ids | uuid[] | Categories this offer belongs to. Drives category pages and "top rated in this category" modules. |
rank | int | Position in toplists (1 = best). Bulk-reorder everything at once with PUT /sites/:id/offers/order. |
Affiliate & monetization
| Field | Type | Description |
affiliate_url | string | Where CTA clicks land (through our click tracker). Never exposed raw in public HTML. |
affiliate_url_mobile | string | Mobile-only destination override. Empty = same as desktop. |
affiliate_network | string | Label for your own reporting, e.g. "Segpay", "PartnerStack". |
link_slots | string[] | Placements: toplink, spotlight, carousel, popular, footer. An offer can sit in several. |
do_follow | bool | Default false → outbound links get rel="nofollow sponsored". Set true for editorial followed links. |
availability | string | auto (default — offer hides itself when link health reports dead), force_available (always show), force_unavailable (always hide — page shows "check these instead" alternatives). |
Ratings & reviews
| Field | Type | Description |
rating_editorial | float|null | Your score (0–5, one decimal). Shown as stars and in schema.org markup; drives "editorial" sort. |
allow_reviews | bool | Default true. False hides the review form and existing reviews on the offer page. |
noindex_negative_reviews | bool | Negative review pages get noindex — keeps complaint content out of Google while staying transparent on-site. |
price_label | string | Free-form price hint on cards, e.g. "from $2.99/mo". |
Pros & cons
| Field | Type | Description |
pros | string[] | Bullet list rendered with ✓ marks. [] hides the block. |
cons | string[] | Bullet list rendered with ✕ marks. Honest cons lift conversion — don't skip them. |
JSON
"pros": ["Zero-knowledge AES-256 encryption", "98.4% autofill success in our tests"],
"cons": ["Free tier limited to one device type", "No monthly billing on the cheapest tier"]
Info sections (key facts table)
Label/value rows rendered as the "Key facts" table on the offer page. Shape matters — it's key/value, not title/body:
JSON
"info_sections": [
{"key": "Platforms", "value": "Windows, macOS, Linux, iOS, Android"},
{"key": "Security audit", "value": "Independently audited by Cure53 (2025)"}
]
SEO
| Field | Type | Description |
meta_title | string | <title> override. Empty → "{name} Review — {site title}". |
meta_description | string | Meta description override. Empty → summary. |
custom_h1 | string | H1 override on the review page. |
search_volume | int|null | Monthly searches you target — internal planning field, shown in the panel's SEO views. |
Read-only (computed by us)
rating_average, rating_count (from approved consumer reviews), link_status, link_checked_at, expired, created_at. Sent in responses; ignored in writes.
Updates replace the whole record
PATCH is full-update. The stored offer is replaced by what you send — omitted optional fields become empty, not unchanged. Always GET /offers/:oid first, modify the object, send it back. Scripts that PATCH only {"rank": 2} will wipe the description, pros, cons and SEO fields.
Safe update pattern
OFFER=$(curl -s $API/offers/$OID -H "Authorization: Bearer $KEY")
echo "$OFFER" | jq '.rank = 2' | curl -s -X PATCH $API/offers/$OID \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d @-
Gallery — screenshots & photos per offer
Every offer can carry an image gallery: screenshots of the dashboard, photos, press-kit images. The public review page renders them as a thumbnail grid with captions and a built-in lightbox (keyboard and touch support included — you don't ship any JS).
Two image sources, exactly one per item:
- Uploaded — upload to the media library with kind
offer_shot, then attach the returned media_id. We host it on our CDN.
- Hotlinked — attach an absolute
url pointing at the producer's server. Nothing to upload, and when the producer refreshes the image your site shows the new one automatically.
| Method | Endpoint | Description |
| GET | /offers/:oid/gallery | List gallery items (ordered) |
| POST | /offers/:oid/gallery | Attach: {"media_id": "…"} or {"url": "https://…"}, plus optional caption, alt |
| PATCH | /gallery/:gid | Update caption / alt / sort_order |
| POST | /offers/:oid/gallery/reorder | {"order": ["gid1", "gid2", …]} — list position becomes sort order |
| DELETE | /gallery/:gid | Remove from gallery (uploaded file stays in the library) |
| Field | Type | Description |
caption | string | Shown under the thumbnail and in the lightbox. Describe what the visitor is looking at — "Settings panel", not "image3". |
alt | string | SEO / accessibility text. Empty → caption is used. |
external | bool | Read-only. True = hotlinked, not hosted by us. |
Upload a screenshot and attach it
# 1. upload the file
MEDIA=$(curl -s -X POST $API/media \
-H "Authorization: Bearer $KEY" \
-F "[email protected]" -F "kind=offer_shot" -F "site_id=$SITE_ID" \
| jq -r .media.id)
# 2. attach to the offer's gallery
curl -X POST $API/offers/$OID/gallery \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d "{\"media_id\":\"$MEDIA\",\"caption\":\"Main dashboard\",\"alt\":\"Acme VPN dashboard screenshot\"}"
# 3. or hotlink straight from the producer — no upload at all
curl -X POST $API/offers/$OID/gallery \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"url":"https://producer.com/press/app-screenshot.png","caption":"Producer press kit image"}'
Card fallback. The first gallery image doubles as the card image on toplists when the offer has no logo. Order matters even if you never show the gallery section.
Link health
We check affiliate destinations on a schedule. An offer on availability: auto with a dead link hides itself from public pages until the link recovers — no manual babysitting.
GET /offers/:oid/link-checks — history of checks (status, HTTP code, latency, timestamp).
POST /offers/:oid/link-check — run one right now, e.g. after the merchant fixes their side.
GET /sites/:id/link-health — site-wide summary for dashboards.