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.

FieldMeaning
idStable id. Re-scans update the same record.
brandBrand name on the account.
marketNational site the listing was found on: US, GB, DE, IT, FR or ES. Empty on older records.
platformListing host, e.g. ebay.it, amazon.de, temu.com.
listing_urlThe listing, with tracking parameters removed.
sellerSeller or storefront name where the marketplace reports one.
titleListing title as captured.
severitylow, medium, high or critical. Reflects any manual override.
risk_scoreAssessment confidence, 0 to 100.
listing_typecounterfeit, reseller, authorized, accessory or noise.
statuspending, investigating, takedown_sent, resolved or escalated.
takedown_outcomesent, removed, relisted, still_active, unreachable or unknown; empty before a takedown.
detected_at, updated_atISO 8601, UTC.
takedown_drafted_at, takedown_sent_at, removed_at, relisted_atEnforcement milestones; empty until they happen.
evidence_count, has_screenshotWhat was captured at detection.
evidence_bundle_urlThe 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 /brandThe brand this key monitors, its target markets and authorised sellers.
GET /violationsList, newest first. Filters: status, severity, market, platform, brand, since, until, updated_since, limit (≤100), cursor.
GET /violations/export.csvSame filters, up to 10,000 rows, as CSV.
GET /violations/{id}One violation, plus its description and outcome history.
GET /violations/{id}/historyEvery event on the case, oldest first: detection, overrides, drafting, sending, outcome checks, enforcement actions.
GET /violations/{id}/evidenceEvidence files with per-file URLs.
GET /violations/{id}/evidence/{index}One evidence file, streamed.
GET /violations/{id}/evidence.zipNotice, 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; previous holds 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.