Skip to content

DEVELOPERS

Verdikx API.
Plug disputes into anything.

A REST API, signed webhooks and Zapier triggers for disputes, evidence and order risk.

Authentication

Create a key in Settings → API keys. Keys look like rk_live_…, belong to one store and are shown once; Verdikx stores only a hash. Send the key on every request:

Authorization: Bearer rk_live_...

Session cookies and Shopify session tokens are never accepted on /api/v1. The API is part of the Growth and Pro plans; creating cases from other processors (POST /api/v1/chargebacks) is available on every plan.

Scopes

  • read:disputes: Read disputes and dispute events
  • write:evidence: Add notes and evidence text to cases, create cases from other processors
  • read:orders: Read order references
  • read:risk: Read order screening verdicts and order events

Limits, errors and request ids

Each key may make about 120 requests in a burst, refilling at 2 per second. Every response carries an X-Request-Id header. Errors are JSON:

{ "error": { "type": "insufficient_scope", "message": "…" }, "request_id": "req_…" }

Types: unauthorized (401), plan_required (402), forbidden / insufficient_scope (403), not_found (404), invalid_request (400), conflict (409), rate_limited (429, with Retry-After), unavailable (503).

Endpoints

curl https://verdikx.com/api/v1/disputes?status=needs_response&limit=10 \
  -H "Authorization: Bearer rk_live_..."
  • GET /api/v1/me (any): The store and key behind the request. Use it to test a connection.
  • GET /api/v1/disputes (read:disputes): Disputes, newest first. Filters: status, provider, updated_since (ISO 8601). Pagination: limit (1–100, default 25) and cursor (next_cursor from the previous page).
  • GET /api/v1/disputes/{id} (read:disputes): One dispute by its Verdikx id.
  • PATCH /api/v1/disputes/{id} (write:evidence): Records the final outcome of a case Verdikx does not sync (cases from /api/v1/chargebacks, email or CSV intake, and Authorize.net): { outcome: "won" | "lost" | "accepted" | "closed", recovered_amount? }. "closed" (refunded, withdrawn or canceled) is reported as conceded. Returns 409 for cases whose processor reports outcomes itself. Sends dispute.updated.
  • GET /api/v1/disputes/{id}/evidence-notes (read:disputes): Notes and evidence text added through the API.
  • POST /api/v1/disputes/{id}/evidence-notes (write:evidence): Adds a note or evidence text ({ body, kind: "note" | "evidence_text", external_reference? }). Nothing is submitted to the processor; you review it in the case workspace.
  • GET /api/v1/orders/{order id}/risk (read:risk): The latest screening verdict for a Shopify order: score, decision, reasons.
  • POST /api/v1/chargebacks (write:evidence): Opens a case from another processor: { provider, case_id, amount, currency, reason, due_date?, order_reference?, payment_reference?, customer_email?, status? }. Repeating the same provider + case_id returns the existing case with created: false. When a connected processor already imported that case, it is returned instead (merged: true) and no duplicate is created. Case ids are scoped by provider, so they never collide with another provider's ids.
  • POST /api/v1/hooks (event scope): Zapier REST Hook subscribe: { hookUrl, event }. Returns { id, secret }.
  • DELETE /api/v1/hooks/{id} (any): Zapier REST Hook unsubscribe.
  • GET /api/v1/triggers/{event} (event scope): Recent events of one type as an array (polling triggers). Add ?sample=1 to get an example when there are none yet.

List responses look like { "data": [...], "has_more": true, "next_cursor": "…" }. Amounts are in major units (89.50, not 8950).

Webhooks and events

Add an endpoint in Settings → Webhooks (HTTPS only; private and internal addresses are refused). Each event is sent as a JSON POST:

{
  "id": "6f1c2c1e-…",            // event id; also sent as Verdikx-Event-Id
  "type": "dispute.created",
  "created_at": "2026-10-01T12:00:00.000Z",
  "shop_id": "…",
  "data": { "dispute": { "id": "…", "provider": "paypal", "case_id": "PP-D-12345", "amount": 89.5, "currency": "USD", "status": "needs_response" } }
}
  • dispute.created (read:disputes): A new dispute or chargeback case was opened (any provider).
  • dispute.updated (read:disputes): A case changed: status, deadline, linked order or evidence notes.
  • dispute.won (read:disputes): The provider decided a case in the merchant's favour.
  • dispute.lost (read:disputes): The provider decided a case against the merchant.
  • evidence.ready (read:disputes): An evidence packet was generated and is ready to review.
  • order.screened (read:risk): An order received a screening verdict.
  • order.held (read:risk): A high-risk order was flagged for a fulfillment hold.
  • order.flagged_known_offender (read:risk): A buyer with chargebacks at your store placed a new order (Shopify or WooCommerce). The event itself holds and cancels nothing.

Respond with any 2xx status within 10 seconds. Failed deliveries retry with exponential backoff (1 minute, doubling, up to 10 attempts over about 8 hours). After 30 failed attempts in a row an endpoint is paused; re-enable it in Settings. Events can arrive more than once or out of order: de-duplicate on the event id.

Verifying signatures

Every delivery has a Verdikx-Signature header: t=<unix seconds>,v1=<hex HMAC-SHA256>. The HMAC is computed with your endpoint's signing secret over <t>.<raw body>. Compare in constant time and reject old timestamps:

import crypto from "node:crypto";

// rawBody: the exact request body string, before JSON parsing.
export function verifyVerdikx(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map((part) => part.trim().split("=", 2)));
  const timestamp = Number(parts.t);
  if (!Number.isInteger(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
  const given = Buffer.from(parts.v1 ?? "", "utf8");
  return given.length === expected.length && crypto.timingSafeEqual(given, Buffer.from(expected, "utf8"));
}

Universal inbound webhook (beta)

The reverse direction: any tool pushes records into Verdikx. Set it up under Connections → Any other tool; each workspace gets one URL (/api/integrations/universal/webhook?connection=…) and a signing secret, shown once. Send a JSON POST with X-Verdikx-Timestamp (unix seconds) and X-Verdikx-Signature: v1=<hex HMAC-SHA256 of "timestamp.body">. Requests outside a 5-minute window, over 256 KB or with more than 50 events are refused; retries are idempotent on your ids. This connector is in beta: built and tested with mocked HTTP, not yet exercised by a live merchant workflow.

Body: one event or { "events": [ ... ] }. Types: dispute, order, refund, shipment, customer_message. Disputes become cases you answer yourself (like email and CSV intake), orders are screened, refunds and shipments become order evidence, messages join the conversation record. Nothing in a payload is fetched or executed.

dispute payload
{
  "type": "dispute",
  "data": {
    "case_id": "CB-20261009-001",
    "processor": "authorize_net",
    "amount": 89.5,
    "currency": "USD",
    "reason": "Item not received",
    "due_by": "2026-10-23",
    "order_id": "1001",
    "customer_email": "buyer@example.com",
    "payment_reference": "txn_7f3a",
    "opened_at": "2026-10-09T08:00:00Z"
  }
}
order payload
{
  "type": "order",
  "data": {
    "id": "1001",
    "number": "#1001",
    "created_at": "2026-10-01T12:00:00Z",
    "total": 89.5,
    "currency": "USD",
    "payment_status": "paid",
    "fulfillment_status": "fulfilled",
    "payment_gateway": "authorize_net",
    "customer": {
      "id": "c_42",
      "email": "buyer@example.com"
    },
    "billing_address": {
      "first_name": "Ada",
      "last_name": "Buyer",
      "line1": "1 Main St",
      "city": "Austin",
      "region": "TX",
      "postal_code": "78701",
      "country": "US"
    },
    "shipping_address": {
      "first_name": "Ada",
      "last_name": "Buyer",
      "line1": "1 Main St",
      "city": "Austin",
      "region": "TX",
      "postal_code": "78701",
      "country": "US"
    },
    "line_items": [
      {
        "id": "li_1",
        "sku": "MUG-01",
        "title": "Mug",
        "quantity": 1,
        "price": 89.5
      }
    ],
    "fulfillments": [
      {
        "id": "f_1",
        "carrier": "UPS",
        "tracking_number": "1Z999AA10123456784",
        "shipped_at": "2026-10-02T15:00:00Z"
      }
    ]
  }
}
refund payload
{
  "type": "refund",
  "data": {
    "id": "rf_501",
    "order_id": "1001",
    "amount": 20,
    "currency": "USD",
    "reason": "Partial refund for damaged item",
    "created_at": "2026-10-05T10:00:00Z"
  }
}
shipment payload
{
  "type": "shipment",
  "data": {
    "order_id": "1001",
    "carrier": "UPS",
    "tracking_number": "1Z999AA10123456784",
    "status": "Delivered",
    "shipped_at": "2026-10-02T15:00:00Z",
    "delivered_at": "2026-10-04T13:20:00Z",
    "checkpoints": [
      {
        "time": "2026-10-02T15:00:00Z",
        "description": "Shipment picked up",
        "location": "Austin, TX"
      },
      {
        "time": "2026-10-04T13:20:00Z",
        "description": "Delivered, left at front door",
        "location": "Dallas, TX",
        "status": "Delivered"
      }
    ]
  }
}
customer_message payload
{
  "type": "customer_message",
  "data": {
    "id": "msg_88",
    "thread_id": "t_12",
    "direction": "inbound",
    "customer_email": "buyer@example.com",
    "subject": "Order #1001",
    "body": "Hi, where is my order #1001?",
    "sent_at": "2026-10-03T09:00:00Z"
  }
}

Field reference and Zapier / Make recipes: Universal connector guide.

Zapier

Connection test: GET /api/v1/me with the API key. Triggers use REST Hooks: subscribe with POST /api/v1/hooks ({ "hookUrl": "{{bundle.targetUrl}}", "event": "dispute.created" }), unsubscribe with DELETE /api/v1/hooks/{id}, and load samples from GET /api/v1/triggers/{event}?sample=1. Actions: “Create chargeback case” (POST /api/v1/chargebacks) and “Add evidence note” (POST /api/v1/disputes/{id}/evidence-notes).

Stop the next chargeback before it starts.

Install on Shopify in minutes, or sign up by card on any other platform, then connect your processors, helpdesk and carriers. Free 7-day trial on every plan.