> For the complete documentation index, see [llms.txt](https://docs.chargebackstop.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.chargebackstop.com/developer/merchant-integration-guides/orders-api-full-integration-merchant-guide.md).

# Orders API Full Integration Merchant Guide

This guide is for merchants who integrate their own backend directly with ChargebackStop using `COMPLETE` orders on a `CUSTOM_ORDERS` integration. It is designed for larger merchants with one organisation, multiple MIDs, and the full prevention suite enabled.

Use this guide when:

* You process payments through your own acquiring setup (one or more MIDs), rather than a processor ChargebackStop connects to directly.
* You want to power **Ethoca Alerts**, **Verifi RDR**, **Visa Order Insight**, **Mastercard Consumer Clarity**, **Compelling Evidence 3.0 (CE3.0)** and **First-Party Trust (FPT)** from a single order feed.
* You have (or will have) a live mode organisation and a test mode organisation.

If you already have a payment processor integration with ChargebackStop (Stripe, Adyen, etc.) and only need to enrich those transactions, use the [Orders API partial enrichment merchant guide](https://docs.chargebackstop.com/developer/merchant-integration-guides/orders-api-partial-enrichment-merchant-guide) instead.

{% hint style="danger" %}
**Webhooks and alert actioning are not optional in this integration.**

Ethoca Alerts arrive in `ACTION_REQUIRED` status with a deadline. Because a `CUSTOM_ORDERS` integration has no processor connection, ChargebackStop **cannot issue refunds on your behalf**. For every actionable alert you must:

1. Receive the `alert.created` webhook (or poll the Alerts API).
2. **Refund the customer with your payment processor.**
3. **Mark the alert as resolved** with `PATCH /v1/alerts/{alert_id}` and `"action": "REFUND"`.

Alerts left unactioned past their deadline are eventually auto-resolved as not refunded, and the underlying dispute will usually proceed to a chargeback. Build the webhook consumer and the alert resolution workflow before you go live.
{% endhint %}

## What you will build

| Component                 | What it does                                                                                             | ChargebackStop surface                           |
| ------------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
| Order feed                | Sends every order (with transactions, items, deliveries, customer and device data) shortly after payment | `POST /v1/orders/`                               |
| Lifecycle updates         | Keeps orders current with refunds, disputes, delivery status and subscription changes                    | `PATCH /v1/orders/{order_id}`                    |
| Webhook consumer          | Receives alert, enrolment and lookup events                                                              | Your HTTPS endpoint, configured in the dashboard |
| Alert resolution workflow | Refunds with your processor and resolves actionable alerts before the deadline                           | `PATCH /v1/alerts/{alert_id}`                    |
| Deflection monitoring     | Tracks digital receipt lookups and CE3.0 deflection outcomes                                             | `lookup.*` webhooks, `GET /v1/lookups/`          |

One order feed powers every tool:

| Tool              | Network                        | What it does                                                               | How your order data is used                                                               |
| ----------------- | ------------------------------ | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| Ethoca Alerts     | Mastercard (+ Visa via Ethoca) | Early dispute/fraud alerts you resolve by refunding                        | Transaction identifiers (ARN, auth code, BIN, last 4, amount) match alerts to your orders |
| Verifi RDR        | Visa                           | Eligible disputes are automatically accepted and refunded at network level | Refund and dispute records reconcile RDR outcomes; rulesets control which cases refund    |
| Consumer Clarity  | Mastercard                     | Shows cardholders a rich digital receipt in their banking app              | Order details, items, deliveries, refunds and merchant profile build the receipt          |
| Order Insight     | Visa                           | Shows cardholders purchase details at the point of dispute                 | Order details, items, delivery and payment information build the response                 |
| CE3.0             | Visa                           | Deflects disputes using the cardholder's history of legitimate purchases   | `customer_email` plus device identifiers link purchases into qualifying history           |
| First-Party Trust | Mastercard                     | Identifies legitimate transactions to reduce first-party fraud             | `customer_email`, `order_email` and device identifiers                                    |

## How your account is structured

| Concept      | ID prefix  | Meaning in this integration                                                                                                   |
| ------------ | ---------- | ----------------------------------------------------------------------------------------------------------------------------- |
| Organisation | `org_`     | Your company. You have one live organisation and one test organisation.                                                       |
| Merchant     | `mrch_`    | One merchant per MID/CAID. An organisation with three MIDs has three merchants.                                               |
| Integration  | `int_`     | Your `CUSTOM_ORDERS` order feed, linked to one or more merchants.                                                             |
| Enrolment    | `enrl_`    | A connection to one prevention programme (Ethoca Alerts, Verifi RDR, Consumer Clarity, Order Insight) for specific merchants. |
| Order        | `ord_`     | A record you create via the Orders API.                                                                                       |
| Alert        | `netalrt_` | A network alert routed to your organisation via an enrolment.                                                                 |
| Lookup       | `lkup_`    | A digital receipt or evidence request served from your order data.                                                            |

### Multiple MIDs

Each MID (or CAID) is represented by one merchant. Programmes are enrolled per MID:

* **Ethoca Alerts** — enrolled by billing descriptor. Each MID's descriptors are registered on its enrolment.
* **Verifi RDR** — enrolled by Visa BIN + CAID (or ARNs). One enrolment per BIN/CAID pair.
* **Consumer Clarity / First-Party Trust** — enrolled per Mastercard merchant identity.
* **Order Insight / CE3.0** — enrolled by Visa BIN + CAID.

We recommend a **single `CUSTOM_ORDERS` integration linked to all of your merchants**. Alerts and lookups are matched against orders from integrations whose merchants overlap the enrolment's merchants, so one integration spanning every MID keeps the feed simple and matching complete.

{% hint style="warning" %}
All `reference_id` values (orders, transactions, items, deliveries, refunds, subscriptions, disputes) are unique **per integration**. With one integration spanning multiple MIDs, make sure your reference IDs are unique across all MIDs — prefix them with a store or MID code if your systems generate overlapping IDs.
{% endhint %}

### What ChargebackStop sets up, and what you build

During onboarding the ChargebackStop team provisions for you:

* Your live and test organisations.
* One merchant per MID, in each organisation.
* Your `CUSTOM_ORDERS` integration(s), linked to your merchants, with the validation rules for your enabled tools.
* Enrolments for every programme: Ethoca Alerts (descriptors per MID), Verifi RDR (BIN + CAID per MID), Consumer Clarity (with First-Party Trust enabled) and Order Insight (with CE3.0 enabled).

You build:

* API keys (created in your dashboard) and the order feed.
* A webhook endpoint per environment.
* The alert resolution workflow (refund with your processor, then resolve the alert).
* Lifecycle updates (refunds, disputes, deliveries, subscriptions) into the Orders API.

{% hint style="info" %}
Enrolment management endpoints (`/v2/enrolments`) and the Merchants API are partner-level APIs. Organisation-level API keys cannot call them — your enrolments and merchants are managed for you. Track enrolment progress via `enrolment.created` / `enrolment.updated` webhooks or in your dashboard, and treat a programme as live only when its enrolment status is `ENABLED`.
{% endhint %}

## Environments

Both environments use the same base URL: `https://api.chargebackstop.com`.

|                    | Test                                              | Live                                                    |
| ------------------ | ------------------------------------------------- | ------------------------------------------------------- |
| Organisation       | `[TEST]`-prefixed test mode organisation          | Live mode organisation                                  |
| API key            | Created in the test organisation's dashboard      | Created in the live organisation's dashboard            |
| Alerts and lookups | Generated with the Simulations API                | Real network traffic                                    |
| Webhooks           | Test endpoint configured on the test organisation | Production endpoint configured on the live organisation |

{% hint style="danger" %}
Test and live IDs are entirely separate. Never carry `org_`, `mrch_`, `int_`, `enrl_`, `ord_` or `netalrt_` IDs between environments, and keep the two API keys and webhook secrets in separate secret stores.
{% endhint %}

## Authentication

Create an API key in your dashboard under **Settings → API keys**, once per organisation (one test key, one live key). The key is displayed **once** — store it in your secrets manager immediately.

Every request uses the standard bearer scheme:

```
Authorization: Bearer <api_key>
Content-Type: application/json
```

Organisation API keys created in the dashboard include the abilities this guide uses:

* `orders:read`, `orders:write`, `orders:update`
* `alerts:read`, `alerts:write`
* `lookups:read`
* `integrations:read`, `integrations:write`
* `rulesets:read`, `rulesets:write`
* `simulations:alerts`, `simulations:enrollments`, `simulations:lookups`, `simulations:scheme_notices`

**Rate limits:** 100 requests per minute per endpoint, per organisation. Each endpoint has an independent pool, and the limit applies to your organisation, not to the API key — creating extra keys does not raise it. A `429` response returns the code `RATE_LIMITED` with no `Retry-After` header, so use exponential backoff with jitter.

## Step 1 — confirm your setup

Fetch your integration ID (you will send it with every order):

```bash
curl -X GET "https://api.chargebackstop.com/v1/integrations/?limit=20&offset=0" \
  -H "Authorization: Bearer <api_key>"
```

Find the integration with `"type": "CUSTOM_ORDERS"` in the response and store its `id`. Your organisation ID is shown in your dashboard and returned on every API object.

Store these IDs in your system's configuration:

| Your system        | ChargebackStop ID                | Used for                                                |
| ------------------ | -------------------------------- | ------------------------------------------------------- |
| Environment config | `organisation_id`                | Every order, alert filter and simulation                |
| Environment config | `integration_id`                 | Every order                                             |
| Per-MID mapping    | `merchant_id`                    | Interpreting which MID an alert or enrolment belongs to |
| Order record       | `order_id` + your `reference_id` | `PATCH` updates and reconciliation                      |
| Alert record       | `alert_id`                       | `GET` and `PATCH` on alerts                             |

## Step 2 — set up webhooks

Configure your endpoint in the dashboard under **Settings → Webhooks** (do this in both organisations). Subscribe to:

* `alert.created`, `alert.updated` — network alerts and their resolution
* `enrolment.created`, `enrolment.updated` — programme enrolment status changes
* `lookup.created`, `lookup.updated` — digital receipt lookups and deflection outcomes

Your endpoint must be HTTPS and should return a `2xx` within 20 seconds. Saving the endpoint generates a signing secret (`whsec_...`) — reveal it with the eye icon and store it with your API key.

### Delivery contract

* **Payload:**

  ```json
  {
    "id": "evt_dbXKdyUWLzSP98HMVdoFW",
    "type": "alert.created",
    "created_at": "2026-02-17T10:30:00Z",
    "data": {
      "object": { "...": "entity snapshot" },
      "previous_attributes": { "...": "only on *.updated events" }
    },
    "api_version": "v1"
  }
  ```

  For alert events, `data.object` is the same alert object the Alerts API returns. Enrolment events use `api_version: "v2"` and return `merchant_ids` as an array.
* **Retries:** if delivery fails, we retry up to 5 times over approximately two days (after 1 minute, 5 minutes, 30 minutes, 2 hours, then 12 hours). Retries of the same delivery keep the same `X-Idempotency-Key` header.
* **Deduplication:** rely on the event `id` from the payload to process each event exactly once.
* **Health:** endpoints that keep failing are automatically disabled and you are notified by email. Keep handlers fast — verify the signature, persist the event, enqueue processing, return `2xx`.
* **Fallback:** poll `GET /v1/alerts/?status=ACTION_REQUIRED` on a schedule so an outage on your side can never leave an actionable alert unseen.

### Verify the signature

Every request includes an `X-Signature` header in the form `t=<unix_timestamp>,v1=<hex_digest>`, where the digest is an HMAC-SHA512 of `"{timestamp}.{raw_body}"` using your webhook secret. Reject requests whose timestamp is outside a small tolerance (for example five minutes) and whose signature does not match.

{% tabs %}
{% tab title="Node.js" %}

```javascript
import crypto from 'crypto';

function verifySignature(rawBody, signatureHeader, secret) {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map((p) => p.split('='))
  );
  const { t: timestamp, v1: receivedSig } = parts;
  if (!timestamp || !receivedSig) throw new Error('Malformed signature header');

  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - Number(timestamp)) > 5 * 60) {
    throw new Error('Timestamp outside allowed window');
  }

  const expectedSig = crypto
    .createHmac('sha512', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  const a = Buffer.from(receivedSig, 'hex');
  const b = Buffer.from(expectedSig, 'hex');
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    throw new Error('Invalid signature');
  }
  return true;
}
```

{% endtab %}

{% tab title="Python" %}

```python
import hashlib
import hmac
import time

def verify_signature(raw_body: bytes, signature_header: str, secret: str) -> None:
    parts = dict(p.split("=", 1) for p in signature_header.split(",") if "=" in p)
    t, v1 = parts.get("t"), parts.get("v1")
    if not t or not v1:
        raise ValueError("Malformed signature header")

    if abs(int(time.time()) - int(t)) > 5 * 60:
        raise ValueError("Timestamp outside allowed window")

    payload = f'{t}.{raw_body.decode("utf-8")}'.encode("utf-8")
    expected = hmac.new(secret.encode(), payload, hashlib.sha512).hexdigest()

    if not hmac.compare_digest(expected, v1):
        raise ValueError("Invalid signature")
```

{% endtab %}

{% tab title="PHP" %}

```php
function verifySignature(string $rawBody, string $signatureHeader, string $secret): bool
{
    $parts = [];
    foreach (explode(',', $signatureHeader) as $part) {
        [$key, $val] = array_pad(explode('=', $part, 2), 2, null);
        $parts[$key] = $val;
    }
    $timestamp = $parts['t'] ?? null;
    $receivedSig = $parts['v1'] ?? null;
    if (!$timestamp || !$receivedSig) {
        throw new Exception('Malformed signature header');
    }
    if (abs(time() - (int) $timestamp) > 5 * 60) {
        throw new Exception('Timestamp outside allowed window');
    }
    $expectedSig = hash_hmac('sha512', $timestamp . '.' . $rawBody, $secret);
    if (!hash_equals($expectedSig, $receivedSig)) {
        throw new Exception('Invalid signature');
    }
    return true;
}
```

{% endtab %}
{% endtabs %}

Full delivery details, sample payloads for every event type, and a Postman collection are in the [webhooks guide](https://docs.chargebackstop.com/developer/partner-integration-guide/webhooks).

## Step 3 — send every order

Send each order to `POST /v1/orders/` shortly after payment is captured. The request body is always a **JSON array** (even for one order), with up to 100 orders per request.

```
POST https://api.chargebackstop.com/v1/orders/
Authorization: Bearer <api_key>
Content-Type: application/json
```

Every order in a full integration uses `"type": "COMPLETE"` and must include:

| Field                            | Description                                                                             |
| -------------------------------- | --------------------------------------------------------------------------------------- |
| `type`                           | Must be `"COMPLETE"`                                                                    |
| `organisation_id`                | Your organisation ID                                                                    |
| `integration_id`                 | Your `CUSTOM_ORDERS` integration ID                                                     |
| `reference_id`                   | Unique order identifier from your system                                                |
| `order_datetime`                 | When the order was placed (ISO 8601)                                                    |
| `order_number`                   | Customer-facing order number                                                            |
| `order_subtotal_amount_in_cents` | Subtotal before tax                                                                     |
| `order_currency`                 | ISO 4217 currency code                                                                  |
| `order_total_amount_in_cents`    | Total amount                                                                            |
| `order_status`                   | `OPEN_PENDING`, `OPEN_PENDING_RETURN`, `CLOSED_COMPLETE`, `CLOSED_CANCELLED` or `OTHER` |

{% hint style="success" %}
**Set the order `reference_id` equal to the primary transaction's `reference_id`.** When a network requests a digital receipt (Consumer Clarity, Order Insight, CE3.0), we first match the card transaction, then resolve the order whose `reference_id` equals the matched transaction's reference. Using the same value for both guarantees the full order — items, deliveries, refunds, receipt links — is served back to the network rather than transaction data alone.
{% endhint %}

### Field sets per tool

One payload powers every tool. This table shows which fields each tool relies on beyond the required core above:

| Tool                                                      | Fields                                                                                                                                                                                                                                                                                                             |
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Alert matching** (Ethoca, RDR reconciliation)           | At least one transaction with `amount_in_cents`, `currency`, `authorised_at`, `payment_method_type`, `authorisation_status`; for cards also `payment_method_card_brand`, `payment_method_card_last_4`, and at least one identifier: `acquirer_reference_number`, `authorisation_code` or `payment_method_card_bin` |
| **Consumer Clarity**                                      | `order_datetime`, `order_number`, `order_subtotal_amount_in_cents`, `order_currency`, `order_total_amount_in_cents`; `refund_datetime` on every refund; receipt extras (`items`, `deliveries`, `order_*_url` links, merchant profile) make the receipt materially better                                           |
| **Order Insight**                                         | At least one `items[]` entry with `name`; keep `order_number` to 25 characters or fewer (longer values are truncated in Order Insight responses)                                                                                                                                                                   |
| **First-Party Trust**                                     | `customer_email`, `order_email`, and at least one of `device_ip_address`, `device_id`, `device_fingerprint`                                                                                                                                                                                                        |
| **CE3.0**                                                 | `customer_email` and at least one device identifier — on **every** order, so historical purchases qualify as evidence                                                                                                                                                                                              |
| **Merchant information** (multi-brand / per-MID receipts) | `merchant_name`, `merchant_store_name`, `merchant_contact_phone`, `merchant_url` when values differ per order; stable values can be configured as defaults in the platform instead                                                                                                                                 |

Your integration is configured with validation rules for the tools you have enabled, so missing fields are rejected with a `MISSING_FIELD` error naming the field and the rule — you will find out at submission time, not at dispute time.

**Matching identifiers.** For card transactions, send all three identifiers whenever available. If you cannot, at least one of these combinations must be present:

* `acquirer_reference_number` (works on its own — the strongest identifier)
* `authorisation_code` + `payment_method_card_last_4`
* `payment_method_card_bin` + `payment_method_card_last_4` + `payment_method_card_brand` + `amount_in_cents` + `currency`

Also send the transaction `descriptor` — with multiple MIDs, the billing descriptor confirms which MID's enrolment an alert belongs to.

### Example: one order powering the full suite

<details>

<summary>Full COMPLETE order payload</summary>

```json
[
  {
    "type": "COMPLETE",
    "organisation_id": "org_live123",
    "integration_id": "int_orders123",
    "reference_id": "txn-us1-100241",

    "order_datetime": "2026-07-30T10:30:00Z",
    "order_number": "AUR-100241",
    "order_subtotal_amount_in_cents": 9000,
    "order_currency": "USD",
    "order_tax_amount_in_cents": 720,
    "order_total_amount_in_cents": 9720,
    "order_status": "CLOSED_COMPLETE",
    "order_phone": "+14155551234",

    "order_view_url": "https://aurora.example.com/orders/AUR-100241",
    "order_request_refund_url": "https://aurora.example.com/orders/AUR-100241/refund",
    "order_proof_of_consent": "Customer accepted Terms of Service at checkout on 2026-07-30",
    "order_communications": "Order confirmation email sent 2026-07-30. Shipping notification sent 2026-07-31.",

    "customer_email": "casey.jordan@example.com",
    "customer_first_name": "Casey",
    "customer_last_name": "Jordan",
    "customer_account_id": "cust-88712",
    "order_email": "casey.jordan@example.com",

    "device_ip_address": "216.24.60.94",
    "device_id": "device-abc123",
    "device_fingerprint": "fp-xyz789",

    "merchant_name": "Aurora Retail Group",
    "merchant_store_name": "Aurora US Store",
    "merchant_contact_phone": "+14155559876",
    "merchant_url": "https://aurora.example.com",

    "transactions": [
      {
        "reference_id": "txn-us1-100241",
        "amount_in_cents": 9720,
        "currency": "USD",
        "payment_method_type": "CARD",
        "authorisation_status": "SETTLED",
        "payment_method_reference_id": "pay-100241",
        "authorised_at": "2026-07-30T10:30:05Z",
        "descriptor": "AURORA US STORE",
        "acquirer_reference_number": "74027012345678901234567",
        "authorisation_code": "123456",
        "settlement_datetime": "2026-07-31T00:00:00Z",
        "cvc_verified": true,
        "three_d_secure_verified": true,
        "payment_method_card_brand": "VISA",
        "payment_method_card_last_4": "4242",
        "payment_method_card_bin": "424242",
        "billing_address": {
          "line_1": "123 Main St",
          "city": "New York",
          "country_subdivision": "NY",
          "postal_code": "10001",
          "country": "US"
        }
      }
    ],

    "deliveries": [
      {
        "reference_id": "dlv-100241-1",
        "type": "PHYSICAL",
        "physical_shipping_carrier": "UPS",
        "physical_shipping_tracking_number": "1Z999AA10123456784",
        "physical_shipping_status": "SHIPPED",
        "physical_shipping_datetime_shipped": "2026-07-31T08:00:00Z",
        "physical_shipping_address": {
          "line_1": "123 Main St",
          "city": "New York",
          "country_subdivision": "NY",
          "postal_code": "10001",
          "country": "US"
        }
      }
    ],

    "items": [
      {
        "reference_id": "item-100241-1",
        "name": "Trail Runner Pro",
        "price_in_cents": 9000,
        "quantity": 1,
        "sku": "TRP-001",
        "product_url": "https://aurora.example.com/products/trail-runner-pro",
        "delivery_reference_id": "dlv-100241-1"
      }
    ]
  }
]
```

For subscription MIDs, include a `subscriptions` entry (`reference_id`, `interval`, `interval_price_in_cents`, `interval_currency`, plus trial fields) and link items to it with `subscription_reference_id`. For digital goods, use a `DIGITAL` delivery with the `digital_*` field set — download timestamps and delivery IP are strong evidence.

</details>

The complete field reference — every field, type, validation rule and enum — is in the [Orders API reference](https://docs.chargebackstop.com/developer/api-documentation/orders).

### Handle the response

`POST /v1/orders/` uses **partial success**: each order in the batch is processed independently.

```json
{
  "created": 1,
  "failed": 1,
  "results": [ { "id": "ord_abc123", "reference_id": "txn-us1-100241", "...": "..." } ],
  "errors": [
    {
      "index": 1,
      "reference_id": "txn-us1-100242",
      "code": "DUPLICATE_ORDER",
      "message": "Order with reference_id 'txn-us1-100242' already exists for this integration",
      "field": "reference_id"
    }
  ]
}
```

* Inspect `errors[]` for every batch; route failures to a retry queue keyed by `reference_id`.
* There is no idempotency header — `reference_id` is your idempotency key. Retrying a request that already succeeded returns `DUPLICATE_ORDER` (or `DUPLICATE_REFERENCE_ID` under concurrent retries). **Treat both as "already stored"**, not as failures.
* A malformed request body (not an array, invalid types) returns a full-request `422` and no orders are processed.
* On `429` or `5xx`, retry the same payload with exponential backoff — duplicates are rejected safely.

{% hint style="info" %}
**Backfill before go-live.** Submit your historical orders (not just new ones) before alert traffic is enabled, so the very first alert can match a transaction. For CE3.0, qualifying evidence comes from the cardholder's prior undisputed purchases in the 120–365 days before a dispute — backfilling up to 12 months of history, with `customer_email` and device identifiers, makes customers eligible from day one. Respect the 100-orders-per-request and 100-requests-per-minute limits when backfilling.
{% endhint %}

## Step 4 — keep orders up to date

Send lifecycle changes to `PATCH /v1/orders/{order_id}` as they happen. This is what keeps the receipt data current and lets us detect **invalid alerts** (for example an alert for a transaction you already refunded) before you spend money resolving them.

| When this happens in your system             | Send                                                                          |
| -------------------------------------------- | ----------------------------------------------------------------------------- |
| You issue a refund                           | A `refunds` entry (include `refund_datetime` — required for Consumer Clarity) |
| An item ships or is delivered                | A `deliveries` update with the new `physical_shipping_status` and timestamps  |
| A subscription is cancelled                  | A `subscriptions` update with `status: "CANCELLED"`                           |
| You receive a chargeback or inquiry directly | A `disputes` entry                                                            |
| The order is cancelled or returned           | An `order_status` update                                                      |

{% tabs %}
{% tab title="Record a refund" %}

```bash
curl -X PATCH "https://api.chargebackstop.com/v1/orders/ord_abc123" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "refunds": [
      {
        "reference_id": "refund-100241-1",
        "amount_in_cents": 9720,
        "currency": "USD",
        "status": "SUCCEEDED",
        "original_transaction_reference_id": "txn-us1-100241",
        "refund_datetime": "2026-08-02T12:00:00Z"
      }
    ]
  }'
```

{% endtab %}

{% tab title="Mark delivered" %}

```bash
curl -X PATCH "https://api.chargebackstop.com/v1/orders/ord_abc123" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "deliveries": [
      {
        "reference_id": "dlv-100241-1",
        "physical_shipping_status": "DELIVERED",
        "physical_shipping_datetime_delivered": "2026-08-02T14:30:00Z"
      }
    ]
  }'
```

{% endtab %}

{% tab title="Cancel a subscription" %}

```bash
curl -X PATCH "https://api.chargebackstop.com/v1/orders/ord_abc123" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "subscriptions": [
      {
        "reference_id": "sub-88712-1",
        "status": "CANCELLED",
        "cancellation_date": "2026-08-02T12:00:00Z"
      }
    ]
  }'
```

{% endtab %}

{% tab title="Record a dispute" %}

```bash
curl -X PATCH "https://api.chargebackstop.com/v1/orders/ord_abc123" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "disputes": [
      {
        "reference_id": "dispute-100241-1",
        "amount_in_cents": 9720,
        "currency": "USD",
        "stage": "1ST_CHARGEBACK",
        "status": "OPEN",
        "type": "CHARGEBACK",
        "network_reason_code": "10.4",
        "payment_method_type": "CARD",
        "card_brand": "VISA"
      }
    ]
  }'
```

{% endtab %}
{% endtabs %}

Notes:

* `PATCH` is all-or-nothing: if any part fails validation, the whole update is rolled back with a `422`.
* Refunds and disputes are upserts by `reference_id`; on existing records `amount_in_cents` and `currency` (and dispute `type`) are immutable.
* Deliveries and subscriptions can only be updated if they were created with the order; delivery type cannot change.

## Step 5 — respond to alerts

This is the operational core of the integration. Alert behaviour differs by programme:

| Enrolment type | How it arrives                                                                                                    | What you must do                                                                                                         |
| -------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `ETHOCA_ALERT` | `status: "ACTION_REQUIRED"` with an `action_required_deadline` (typically around 48 hours)                        | **Refund with your processor, then resolve the alert via the API** — or accept the dispute                               |
| `VERIFI_RDR`   | `status: "RESOLVED"` with `transaction_refund_outcome: "REFUNDED"` — the refund already happened at network level | Reconcile only: record the refund, stop fulfilment, cancel any subscription. **Never refund again with your processor.** |

{% hint style="danger" %}
**Do not double-refund RDR alerts.** When an RDR alert arrives it is already resolved — Visa has returned the funds to the cardholder through the network. Issuing another refund through your processor refunds the customer twice. Your only job for RDR alerts is reconciliation.
{% endhint %}

### The Ethoca alert workflow

{% stepper %}
{% step %}

### Receive the alert

Your webhook receives `alert.created` with `status: "ACTION_REQUIRED"`:

```json
{
  "id": "evt_dbXKdyUWLzSP98HMVdoFW",
  "type": "alert.created",
  "created_at": "2026-08-01T09:15:00Z",
  "data": {
    "object": {
      "id": "netalrt_abc123",
      "organisation_id": "org_live123",
      "merchant_id": "mrch_usstore1",
      "enrolment_id": "enrl_ethoca1",
      "enrolment_type": "ETHOCA_ALERT",
      "status": "ACTION_REQUIRED",
      "action_required_deadline": "2026-08-03T09:15:00Z",
      "transaction_amount_in_cents": 9720,
      "transaction_currency_code": "USD",
      "transaction_authorised_at": "2026-07-30T10:30:05Z",
      "transaction_authorisation_code": "123456",
      "transaction_acquirer_reference_number": "74027012345678901234567",
      "transaction_statement_descriptor": "AURORA US STORE",
      "transaction_card_bin": "424242",
      "transaction_card_last4": "4242",
      "transaction_card_scheme": "VISA",
      "transaction_refund_outcome": null,
      "integration_id": "int_orders123",
      "integration_transaction_id": "txn-us1-100241"
    }
  },
  "api_version": "v1"
}
```

Acknowledge with a `2xx` immediately and process asynchronously.
{% endstep %}

{% step %}

### Locate the order

When the alert matched one of your transactions, `integration_transaction_id` contains **your transaction `reference_id`** — look the order up directly in your own system, or via `GET /v1/orders/?reference_id=<id>`. `merchant_id` tells you which MID the alert belongs to.

If `integration_transaction_id` is `null`, fall back to the transaction fields on the alert (ARN, auth code, card last 4, amount, descriptor). Persistent unmatched alerts usually mean gaps in your order feed — investigate them.
{% endstep %}

{% step %}

### Decide and act with your processor

Apply your policy (most merchants refund fraud alerts below a value threshold and review the rest). If you decide to refund:

* **Issue the refund through your payment processor** for the alerted transaction.
* Stop fulfilment and cancel any related subscription in your own system.

ChargebackStop cannot do this step for you on a `CUSTOM_ORDERS` integration — the refund must happen in your systems.
{% endstep %}

{% step %}

### Resolve the alert before the deadline

Report the outcome so it reaches the issuer through Ethoca:

```bash
curl -X PATCH "https://api.chargebackstop.com/v1/alerts/netalrt_abc123" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "REFUND",
    "note": "Refunded in full via processor, refund id re_9k2..."
  }'
```

| Action           | Meaning for a Custom Orders integration                                                                                                                    |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `REFUND`         | Declares you have refunded the customer with your processor. Resolves the alert with `transaction_refund_outcome: "REFUNDED"`. It does **not** move money. |
| `ACCEPT_DISPUTE` | You are not refunding; the dispute proceeds and you may fight it. Resolves with `transaction_refund_outcome: "NOT_REFUNDED"`.                              |

(`CANCEL` and `REFUND_AND_CANCEL` exist for integrations where ChargebackStop has processor or subscription access; with Custom Orders use `REFUND` or `ACCEPT_DISPUTE`.)

The response returns the alert with `status: "RESOLVED"`. Resolved alerts cannot be actioned again (`422 INVALID_ACTION`).
{% endstep %}

{% step %}

### Update the order

Record the refund on the order too, so future alerts and receipts reflect it:

```bash
curl -X PATCH "https://api.chargebackstop.com/v1/orders/ord_abc123" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "refunds": [
      {
        "reference_id": "refund-100241-1",
        "amount_in_cents": 9720,
        "currency": "USD",
        "status": "SUCCEEDED",
        "original_transaction_reference_id": "txn-us1-100241",
        "refund_datetime": "2026-08-01T10:05:00Z"
      }
    ]
  }'
```

{% endstep %}
{% endstepper %}

{% hint style="warning" %}
**Respect the deadline.** `action_required_deadline` is the point after which resolving the alert no longer reliably prevents the chargeback. Alerts left unactioned are eventually auto-resolved as not refunded, and the dispute usually proceeds. Alert your operations team well before the deadline, and treat approaching-deadline alerts as incidents.
{% endhint %}

You will also receive `alert.updated` events whenever `status`, `transaction_refund_outcome` or `subscription_cancel_outcome` change — including for resolutions made by your own team in the dashboard, and for RDR alerts. Alerts with `status: "INVALID"` were withdrawn or detected as invalid (for example, already refunded before the alert) — store them for reporting; no action is needed.

## Step 6 — monitor deflection outcomes

Consumer Clarity, Order Insight, FPT and CE3.0 activity is visible as **lookups** — one record per time a network requested your order data:

* `lookup.created` fires when an issuer/cardholder triggers a lookup against your enrolments.
* `lookup.updated` fires when `lookup_status` or `deflection_status` changes.

Key fields on the lookup object:

| Field                        | Values                                                                                                        | Meaning                                      |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| `type`                       | `ETHOCA_CONSUMER_CLARITY`, `ETHOCA_FIRST_PARTY_TRUST`, `VERIFI_ORDER_INSIGHT`, `VERIFI_COMPELLING_EVIDENCE_3` | Which product served the request             |
| `lookup_status`              | `PENDING`, `SUCCEEDED`, `FAILED`, `TIMEOUT`                                                                   | Whether we answered the network successfully |
| `deflection_status`          | `NOT_ATTEMPTED`, `PENDING`, `SUCCEEDED`, `FAILED`                                                             | Whether the dispute was deflected (CE3.0)    |
| `integration_transaction_id` | Your transaction `reference_id`                                                                               | Ties the lookup back to your order           |

Use `deflection_status: "SUCCEEDED"` as the positive CE3.0 deflection signal for reporting. A high rate of lookups with `integration_transaction_id: null` means the networks are asking about transactions your order feed doesn't cover — the fix is always more complete order data. You can also query history with `GET /v1/lookups/` ([Lookups API reference](https://docs.chargebackstop.com/developer/api-documentation/lookups)).

## Optional — control RDR decisioning with rulesets

By default, RDR accepts and refunds every eligible Visa dispute on your enrolled BIN/CAIDs. If you want exceptions — for example, contest disputes above a value threshold — create a ruleset with the Rulesets API (your organisation key has `rulesets:read` / `rulesets:write`):

```bash
curl -X POST "https://api.chargebackstop.com/v1/rulesets/" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "organisation_id": "org_live123",
    "enrolment_ids": ["enrl_rdr_us1"],
    "outcome": "ACCEPT_DISPUTE",
    "join_operator": "OR",
    "rules": [
      {
        "type": "AMOUNT",
        "parameters": {
          "operator": "GREATER_THAN",
          "currency_code": "USD",
          "amount_in_cents": 25000
        }
      }
    ]
  }'
```

This example declines the automatic refund for disputes over $250, letting them proceed as normal disputes you can fight. Create rulesets **before** go-live if you need them — see the [Rulesets API reference](https://docs.chargebackstop.com/developer/api-documentation/rulesets-developer-preview) and [resolution rules](https://docs.chargebackstop.com/chargeback-alerts/resolution-rules) for details.

## Test the integration end to end

Everything above works identically in your test organisation, with simulated network traffic. Use your **test organisation's** API key and IDs throughout.

{% stepper %}
{% step %}

### Enable your test enrolments

Test enrolments are not activated by the real networks, so enable them via simulation and confirm the `enrolment.updated` webhook arrives:

```bash
curl -X PATCH "https://api.chargebackstop.com/v1/simulate/enrolments/enrl_test456" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"status": "ENABLED"}'
```

{% endstep %}

{% step %}

### Submit a test order

Send a `COMPLETE` order to your test integration with a known ARN (exactly 23 characters) and authorisation code (exactly 6 characters) on the transaction.
{% endstep %}

{% step %}

### Simulate an actionable Ethoca alert

```bash
curl -X POST "https://api.chargebackstop.com/v1/simulate/alerts" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "organisation_id": "org_test123",
    "enrolment_id": "enrl_test456",
    "status": "ACTION_REQUIRED",
    "card_scheme": "VISA",
    "amount_in_cents": 9720,
    "currency_code": "USD",
    "transaction_acquirer_reference_number": "74027012345678901234567",
    "transaction_authorisation_code": "123456"
  }'
```

This fires a real `alert.created` webhook to your test endpoint. Note: the simulator does not run order matching inline, so `integration_transaction_id` may be `null` — exercise your fallback matching path here, and verify matched alerts against real traffic during go-live.
{% endstep %}

{% step %}

### Resolve it

Run your full workflow: webhook → order lookup → (pretend) processor refund → `PATCH /v1/alerts/{alert_id}` with `"action": "REFUND"` → `PATCH` the order with the refund. Confirm the `alert.updated` webhook shows `status: "RESOLVED"` and `transaction_refund_outcome: "REFUNDED"`.
{% endstep %}

{% step %}

### Simulate the rest of the suite

* An RDR alert (RDR only supports `RESOLVED` or `INVALID`): confirm your handler records it **without** triggering a processor refund.
* A lookup via `POST /v1/simulate/lookups` for each `type` you use: confirm `lookup.created` handling and your deflection reporting.

See the [Simulations API reference](https://docs.chargebackstop.com/developer/api-documentation/simulations) for all options.
{% endstep %}
{% endstepper %}

### Sandbox acceptance checklist

* [ ] Test API key stored in secrets manager; never used against live IDs
* [ ] Webhook endpoint verifies `X-Signature` and rejects bad/stale signatures
* [ ] Webhook processing is idempotent by event `id` and returns `2xx` within 20 seconds
* [ ] `COMPLETE` orders submit successfully with all enabled field sets (matching, CC, OI, FPT, CE3.0)
* [ ] Batch errors (`errors[]`) are inspected and retried; `DUPLICATE_ORDER` treated as success
* [ ] Refund/delivery/subscription/dispute updates flow via `PATCH /v1/orders/{order_id}`
* [ ] Simulated `ACTION_REQUIRED` alert resolved with `REFUND` before deadline
* [ ] Simulated alert resolved with `ACCEPT_DISPUTE` (no-refund path)
* [ ] Simulated RDR alert reconciled without a processor refund
* [ ] Simulated lookups processed; deflection reporting uses `deflection_status`
* [ ] Polling fallback on `GET /v1/alerts/?status=ACTION_REQUIRED` works
* [ ] Client handles `401`, `403`, `404`, `422`, `429` and `5xx` with backoff

## Go-live checklist

{% stepper %}
{% step %}

### Create live credentials and webhooks

Create the live organisation API key and configure the production webhook endpoint on the live organisation; store both secrets separately from test.
{% endstep %}

{% step %}

### Backfill order history

Backfill order history (12 months where possible) into the live integration before alert traffic is enabled.
{% endstep %}

{% step %}

### Enable real-time feeds

Switch on the real-time order feed and lifecycle updates.
{% endstep %}

{% step %}

### Confirm enrolments are enabled

Confirm every enrolment reports `status: "ENABLED"` (via `enrolment.updated` webhooks or your dashboard) before treating a programme as live.
{% endstep %}

{% step %}

### Verify alert matching

Verify the first live alerts arrive with `integration_transaction_id` populated — unmatched alerts at go-live mean feed gaps.
{% endstep %}

{% step %}

### Cover the Ethoca deadline

Confirm your operations rota covers the Ethoca deadline window (including weekends) and that deadline alerts page a human.
{% endstep %}

{% step %}

### Verify refund workflows

Verify the first refunds flow end to end: processor refund → alert resolved → order updated.
{% endstep %}

{% step %}

### Monitor lookups

Monitor lookups in the first weeks: `lookup_status: "SUCCEEDED"` with your orders attached, and CE3.0 `deflection_status` outcomes.
{% endstep %}
{% endstepper %}

## Error handling reference

All APIs use the same error envelope:

```json
{
  "errors": [
    {
      "code": "ERROR_CODE",
      "message": "Human-readable message"
    }
  ]
}
```

| Situation                            | Code                                                         | Handling                                                                       |
| ------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------------------------ |
| Missing/invalid API key              | `UNAUTHORISED` (401)                                         | Check the key and the environment it belongs to                                |
| Key lacks an ability                 | `FORBIDDEN` (403)                                            | Recreate the key in the dashboard                                              |
| Rate limited                         | `RATE_LIMITED` (429)                                         | Exponential backoff with jitter; no `Retry-After` header is sent               |
| Order already exists                 | `DUPLICATE_ORDER` / `DUPLICATE_REFERENCE_ID` (in `errors[]`) | Treat as already stored                                                        |
| Wrong order type for integration     | `INVALID_ORDER_TYPE`                                         | `COMPLETE` orders require the `CUSTOM_ORDERS` integration                      |
| Integration not found/accessible     | `INVALID_INTEGRATION`                                        | Check `integration_id` and `organisation_id`                                   |
| Field required by your enabled tools | `MISSING_FIELD`                                              | The message names the field and the rule (e.g. `visa_compelling_evidence_3_0`) |
| Whole-request schema failure         | `VALIDATION_*` (422)                                         | Fix payload shape/types; nothing was processed                                 |
| Actioning a resolved alert           | `INVALID_ACTION` (422)                                       | Fetch the latest alert state before actioning                                  |
| Simulating against a live org        | `SIMULATION_NOT_ALLOWED` (422)                               | Simulations work only on test organisations                                    |

One inconsistency to code around: `PATCH /v1/orders/{order_id}` returns `404` as `{"detail": "Order with ID ord_x not found"}` rather than the standard `errors[]` envelope.

## Related documentation

* [Orders API reference](https://docs.chargebackstop.com/developer/api-documentation/orders) — every field, validation rule and error code
* [Alerts API reference](https://docs.chargebackstop.com/developer/api-documentation/alerts)
* [Lookups API reference](https://docs.chargebackstop.com/developer/api-documentation/lookups)
* [Simulations API reference](https://docs.chargebackstop.com/developer/api-documentation/simulations)
* [Webhooks guide](https://docs.chargebackstop.com/developer/partner-integration-guide/webhooks) — payloads for all event types and a Postman collection
* [Transaction matching](https://docs.chargebackstop.com/chargeback-alerts/transaction-matching)
* [Ethoca Alerts](https://docs.chargebackstop.com/chargeback-alerts/ethoca-alerts) and [Verifi RDR](https://docs.chargebackstop.com/chargeback-alerts/verifi-rdr)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.chargebackstop.com/developer/merchant-integration-guides/orders-api-full-integration-merchant-guide.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
