Skip to content

API reference

Pro

Base: https://reviews.leadit.tech/api/v1. Every request is signed. The Permission column is the key’s minimum scope.

Method Path Permission What for
POST /review-requests moderate Get an order’s link
GET /review-requests read List requests and what changed
POST /review-requests/{id}/dispatched moderate Confirm you sent it
POST /review-requests/{id}/send-now moderate Send now
POST /review-requests/{id}/cancel moderate Cancel
POST /reviews moderate Create a review
GET /reviews read List and filter reviews
GET /reviews/{id} read One review
PATCH /reviews/{id} moderate Moderate and reply
DELETE /reviews/{id} full Delete
POST /reviews/import full Import up to 1,000 reviews
POST /uploads moderate Upload a photo or a video
POST /uploads/{id}/complete moderate Report that it’s uploaded
GET /stats read The Home metrics
GET /settings read Settings, flow and moderation
PATCH /settings full Change them
GET /locations read Active locations
GET /webhook-deliveries read Webhook deliveries
POST /webhook-deliveries/{id}/retry full Retry a delivery

Lists (GET /review-requests, GET /reviews, GET /webhook-deliveries) share three parameters and are sorted by the last time they changed:

Parameter What it does
since ISO 8601 date: returns what changed since then. Ideal for syncing without missing anything.
limit From 1 to 100. Default, 50.
cursor The next_cursor from the previous page.
{ "data": [ … ], "next_cursor": "MjAyNi0xMC0xNlQxNjowMDowMC4wMDBafGNtMms4eDFmMzAwMDE" }

When next_cursor is null, there’s nothing more.

POST /review-requests is the heart of the API: you give it an order and it returns its link.

Request
{
"order": { "shopify_order_id": "gid://shopify/Order/5812345678901" },
"channel": "api",
"locale": "en",
"expires_in_days": 45
}
Field What it is
order.shopify_order_id required The order’s gid or number.
channel optional api (default), leadit_whatsapp or email.
locale optional es or en: the language of the form. It also changes the language of a request that already existed.
expires_in_days optional From 1 to 365. Only counts when creating it.

Response: 201 if we created it, 200 if the order already had a request (the same one, with the same link). The body is the request, the same as the data of request events.

  • Getting the link doesn’t change the schedule. If you never confirm with /dispatched, the request goes out on its date through the store’s channel.
  • If the order had no request, we create it without going through the app’s filters: asking for it is your decision.
  • The order has to exist in Shopify and not be cancelled. If we can’t find it: 404 order_not_found.

POST /review-requests/{id}/dispatched, no body. The request leaves the schedule and the reminders start running. Repeating it changes nothing.

POST /review-requests/{id}/send-now, no body. The same as Send now in Requests: the date moves to this instant. It only works with a scheduled or failed request; otherwise, 409 invalid_state.

POST /review-requests/{id}/cancel, no body. A request that was already answered or expired returns 409 request_closed.

GET /review-requests?status=due,dispatched&since=2026-10-01T00:00:00Z

status accepts several, separated by commas: scheduled, due, paused, dispatched, opened, partial, responded, expired, cancelled, failed. Each request also includes updated_at.

POST /reviews, for example with a review your customer left on another channel.

Request
{
"order": { "shopify_order_id": "5812345678901" },
"product": { "sku": "SUD-ARENA-M" },
"rating": 5,
"title": "Really comfy",
"body": "The fabric is soft inside.",
"author": { "name": "María G.", "email": "maria@example.com" },
"language": "en"
}
  • The product is looked up in this order: product.shopify_product_id, product.sku, product.handle and, if the order has a single product, the order.
  • Don’t send verified_purchase: we compute it from the real Shopify order.
  • Without a verified order, the review stays pending and waits for someone to approve it, whether it’s 1 or 5 stars.
  • One review per product per order: the second returns 409 review_exists with the id of the first.
  • author.name is required when there’s no order.
  • Accepts Idempotency-Key.

Response: 201 with the review. To add photos or videos, continue with /uploads.

GET /reviews?status=pending,flagged&rating=1&since=…

Parameter Values
status pending, published, rejected, spam, private and flagged (pending with some flag: phone, email, blocked word…). Several, comma-separated.
kind product or store.
product_id The product’s gid or number.
rating From 1 to 5.
source request_link, qr_store, qr_location, manual, import, api or pos.

GET /reviews/{id} returns the review with its flags, its reply and its photos and videos.

PATCH /reviews/{id}

{ "status": "rejected", "rejection_reason": "It's about a different product." }
Field What it does
status published, rejected or spam.
rejection_reason Required when rejecting: from 3 to 500 characters.
merchant_reply Your public reply, up to 2,000 characters. null removes it.

A change moderation doesn’t allow returns 409 invalid_state.

DELETE /reviews/{id}. full key.

POST /reviews/import with { "reviews": [ … ] }: up to 1,000 reviews and 5 MB per call. full key. Accepts Idempotency-Key.

Each review takes external_id, product, rating, title, body, author.name, created_at (required) and, if you have them, order.shopify_order_id (it can be older than 60 days), author.email, status, verified_purchase, incentivized, reply.body, reply.replied_at, photos (up to 5 https URLs) and videos (1).

Response
{
"created": 998,
"existing": 1,
"rejected": 1,
"results": [
{ "index": 0, "status": "created", "review_id": "cm2k…" },
{ "index": 1, "status": "exists", "review_id": "cm2j…" },
{ "index": 2, "status": "rejected", "error": { "code": "product_not_found", "message": "…" } }
]
}

One bad review doesn’t take down the others, and sending the same batch again returns everything as exists: retrying doesn’t duplicate.

They’re uploaded straight to our storage, in three steps:

  1. POST /uploads with { "review_id": "cm2k…", "content_type": "image/jpeg", "bytes": 2483123 }. It returns 201 with media_id and upload: the method (PUT), the signed URL and the headers.
  2. Upload the file with a PUT to upload.url, with those headers.
  3. POST /uploads/{media_id}/complete. We process it in the background.
Type Formats Maximum
Photo image/jpeg, image/png, image/heic, image/heif 15 MB, up to 5 per review
Video video/mp4, video/quicktime 100 MB and 90 seconds, 1 per review

GET /stats?period=30 (7, 30 or 90 days): the same you see in Home.

{
"period_days": 30,
"since": "2026-09-16T16:00:00.000Z",
"metrics": {
"requests_sent": 412,
"reviews_collected": 131,
"response_rate": 31.8,
"with_media_rate": 42.7,
"average_rating": 4.7
},
"pending_reviews": 6,
"alerts": { "failed_requests": 0, "webhook_dlq": 0, "flagged_reviews": 2 },
"setup": { "channel_connected": true, "timing_saved": true, "form_saved": true, "first_request_sent": true }
}

GET /settings returns the store’s settings, the flow (timing and reminders) and moderation (threshold and blocked words). PATCH /settings changes only what you send, with the same validation as the admin; a field that doesn’t exist returns 422 with its path in details. full key.

GET /locations returns the active locations, with their internal reviews and whether the location question is turned on.

GET /webhook-deliveries?status=dlq lists the webhook deliveries (pending, delivered, failed, dlq). POST /webhook-deliveries/{id}/retry retries one by hand, the same as the button in Channels. full key.