Developers
API and webhooks
Read your monitoring data from your own systems. Everything the dashboard shows about a violation is available as one flat record through a read-only REST API, as CSV or Excel, and as signed webhook deliveries when something changes. Included on Business, Intelligence and Enterprise.
Authentication
Create a key under Settings → API keys in the dashboard. It is shown once. Send it as a bearer token on every request.
curl https://dashboard.ipzest.app/api/v1/brand \
-H "Authorization: Bearer ipz_live_…"Keys are read-only and belong to the account. Revoking a key takes effect on the next request that presents it.
Rate limits
120 requests per minute and 5,000 per UTC day, per key. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining. Over the limit you receive 429 with a message saying which limit and when it resets.
The violation record
One shape everywhere: the list and detail endpoints, the CSV and Excel exports, the weekly export email and webhook payloads all use these fields, so nothing has to be reconciled between them.
| Field | Meaning |
|---|---|
| id | Stable id. Re-scans update the same record. |
| brand | Brand name on the account. |
| market | National site the listing was found on: US, GB, DE, IT, FR or ES. Empty on older records. |
| platform | Listing host, e.g. ebay.it, amazon.de, temu.com. |
| listing_url | The listing, with tracking parameters removed. |
| seller | Seller or storefront name where the marketplace reports one. |
| title | Listing title as captured. |
| severity | low, medium, high or critical. Reflects any manual override. |
| risk_score | Assessment confidence, 0 to 100. |
| listing_type | counterfeit, reseller, authorized, accessory or noise. |
| status | pending, investigating, takedown_sent, resolved or escalated. |
| takedown_outcome | sent, removed, relisted, still_active, unreachable or unknown; empty before a takedown. |
| detected_at, updated_at | ISO 8601, UTC. |
| takedown_drafted_at, takedown_sent_at, removed_at, relisted_at | Enforcement milestones; empty until they happen. |
| evidence_count, has_screenshot | What was captured at detection. |
| evidence_bundle_url | The ZIP endpoint for this violation. Fetch it with the same key. |
Endpoints
Base URL https://dashboard.ipzest.app/api/v1. All endpoints are GET and return { "data": … }.
| GET /brand | The brand this key monitors, its target markets and authorised sellers. |
| GET /violations | List, newest first. Filters: status, severity, market, platform, brand, since, until, updated_since, limit (≤100), cursor. |
| GET /violations/export.csv | Same filters, up to 10,000 rows, as CSV. |
| GET /violations/{id} | One violation, plus its description and outcome history. |
| GET /violations/{id}/history | Every event on the case, oldest first: detection, overrides, drafting, sending, outcome checks, enforcement actions. |
| GET /violations/{id}/evidence | Evidence files with per-file URLs. |
| GET /violations/{id}/evidence/{index} | One evidence file, streamed. |
| GET /violations/{id}/evidence.zip | Notice, page snapshot, screenshot, detection record and a SHA-256 manifest, as one archive. |
curl "https://dashboard.ipzest.app/api/v1/violations?market=IT&severity=high&limit=100" \
-H "Authorization: Bearer ipz_live_…"
{
"data": [
{
"id": "…",
"brand": "Example",
"market": "IT",
"platform": "ebay.it",
"listing_url": "https://www.ebay.it/itm/123456789012",
"seller": "some-seller",
"severity": "high",
"status": "pending",
"detected_at": "2026-09-02T10:14:03.000Z",
…
}
],
"next_cursor": "WzE3NTY4…"
}Paging and syncing
Pages hold up to 100 rows. Pass next_cursor back as cursor until it is null. A page can hold fewer than limit rows while a cursor is still set, because some filters apply after the page is read; only a null cursor means the end.
To keep a copy current, poll with updated_since. Ordering switches to oldest change first; remember the highest updated_at you have processed and send it next time. Webhooks make polling unnecessary for most integrations.
Exports
The Violations page exports the current filters as CSV or Excel. GET /violations/export.csv does the same through the API. Under Settings → Email alerts you can also schedule a weekly file of everything detected or changed in the last seven days. All three use the record above, capped at 10,000 rows per file.
Webhooks
Add an HTTPS endpoint under Settings → Webhooks and choose events. Deliveries arrive within about five minutes of a change and are retried with backoff for about a day; after 20 consecutive failures the endpoint is paused and you are emailed.
violation.created: a new violation was detected.violation.status_changed,violation.severity_changed: the case moved;previousholds the old value.takedown.outcome_changed: a takedown was recorded as sent, removed, relisted or still active.ping: the test button in Settings.
POST https://your-portal.example/hooks/ipzest
Content-Type: application/json
X-IPzest-Event: violation.status_changed
X-IPzest-Delivery: GhqyvjdL51sJQtHXmtfA
X-IPzest-Signature: t=1756857885,v1=3f0c…e9
{
"id": "GhqyvjdL51sJQtHXmtfA",
"event": "violation.status_changed",
"created_at": "2026-09-03T00:04:45.377Z",
"data": { …violation record… },
"previous": { "status": "pending" }
}Verify every delivery: compute HMAC-SHA256 of {t}.{raw body} with your endpoint secret, compare it to v1 in constant time, and reject timestamps older than five minutes.
// Node
import { createHmac, timingSafeEqual } from "node:crypto"
export function verify(secret, header, rawBody, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.trim().split("=")))
const t = Number(parts.t)
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest()
const given = Buffer.from(parts.v1 || "", "hex")
return expected.length === given.length && timingSafeEqual(expected, given)
}# Python
import hmac, hashlib, time
def verify(secret: str, header: str, raw_body: bytes, tolerance=300) -> bool:
parts = dict(p.strip().split("=", 1) for p in header.split(","))
t = int(parts.get("t", "0"))
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))Respond with any 2xx within ten seconds. Do the work after you have responded.
Errors
Every error is JSON with a stable code and a message written for a person.
{ "error": { "code": "invalid_request", "message": "status must be one of pending, investigating, takedown_sent, resolved, escalated" } }Codes: unauthorized (401), plan_required (403), rate_limited (429), not_found (404), invalid_request (400), internal (500).
OpenAPI
The full specification is published at https://dashboard.ipzest.app/api/v1/openapi.json (OpenAPI 3.1). Import it into Postman, Insomnia or a client generator.
Questions or a field you need that is not here: info@ipzest.app. See the pricing page for which plans include API access.