# Welcome

ChargebackStop gives you a single platform to prevent chargebacks before they happen. We sit between you and the card networks, handling the integration, automation, and resolution logic so you can focus on your business.

This documentation covers everything you need to get set up, configure your products, and start preventing disputes.

### How ChargebackStop works

Every chargeback has a lifecycle. We intervene at three different stages, each with its own protection layer. Use one, two, or all three depending on your risk profile and goals.

{% hint style="info" %}
**New here?** Start with [Chargeback Alerts](https://docs.chargebackstop.com/chargeback-alerts/introduction) if you want the fastest path to reducing disputes. Most merchants are protected within a few working days.
{% endhint %}

{% stepper %}
{% step %}

#### Digital Receipts

Digital Receipts push rich order data to cardholders and their banks in real time. When a cardholder views a transaction in their banking app or a bank agent opens a dispute portal, they see subscription details, itemised breakdowns, delivery tracking, and more, before a dispute is ever raised.

This reduces cardholder confusion and gives banks the context they need to resolve queries without filing a chargeback.

**Powered by:** Order Insights (Visa) · Consumer Clarity (Mastercard)

[Get started with Digital Receipts →](https://docs.chargebackstop.com/digital-receipts/introduction)
{% endstep %}

{% step %}

#### Systematic Dispute Deflection

Systematic Dispute Deflection blocks friendly fraud disputes at the card network level. When a cardholder attempts to dispute a transaction, the network checks their purchase history and qualifying identifiers. If there's a match, the dispute is instantly blocked, you retain the funds and it never hits your dispute rate.

**Powered by:** Compelling Evidence 3.0 (Visa) · First Party Trust (Mastercard)

[Get started with Systematic Dispute Deflection →](https://docs.chargebackstop.com/systematic-dispute-deflection/introduction)
{% endstep %}

{% step %}

#### Chargeback Alerts

Chargeback Alerts notify you of a dispute before it becomes an official chargeback. When an alert comes in, you can refund the transaction and prevent the chargeback entirely, or decline the alert and let the dispute proceed. Alerts can be fully automated with resolution rules or handled case by case in your dashboard.

When both alert programs are active, merchants typically see up to 95% dispute prevention coverage across Visa and Mastercard.

**Powered by:** Ethoca Alerts (Mastercard) · Verifi RDR (Visa)

[Get started with Chargeback Alerts →](https://docs.chargebackstop.com/chargeback-alerts/introduction)
{% endstep %}
{% endstepper %}

### Connecting your payment processor

ChargebackStop integrates directly with your payment processor to locate transactions, issue refunds, and pull order data. We currently support:

* [Stripe](https://docs.chargebackstop.com/integration/stripe)
* [Adyen](https://docs.chargebackstop.com/integration/adyen)
* [Authorize.net](https://docs.chargebackstop.com/integration/authorize.net)
* [NMI](https://docs.chargebackstop.com/integration/nmi)
* ACI Worldwide (OPPWA)

If your processor isn't listed, reach out to our team, we're adding new integrations regularly.

### For developers

If you're integrating via API or building on top of ChargebackStop as a partner, head to the [API documentation](https://docs.chargebackstop.com/developer/api-documentation) or the [Partner integration guide](https://docs.chargebackstop.com/developer/partner-integration-guide).

### For payment service providers

ChargebackStop is built for both merchants and PSPs. If you're a payment service provider looking to white-label our platform, offer chargeback prevention to your merchants, or manage risk at portfolio level, contact our sales team to discuss partner options.


# Organizations

An organization is the top-level entity in ChargebackStop. It groups your merchant accounts, team members, email preferences, APIs, webhooks, and integrations under one roof.

We recommend one organization per brand or website. If that brand has multiple merchant accounts, keep them in the same organization, they'll share dispute logic, resolution rules, APIs, and integrations.

You can belong to multiple organizations and switch between them with a single login.

**Permissions to note**

Anyone invited to an organization can see and read everything in that account. If different teams need restricted access to certain data, use separate organizations.

The same applies to API keys and webhooks, any credentials created within an organization have full account access.

**Billing**

A billing account determines how you're charged and which organizations are covered under it.

If you have multiple organizations, they can share a single billing account, useful for keeping invoices in one place. If you'd prefer separate billing for different organizations, let us know during onboarding and we'll set that up.

To change your payment method, billing frequency, or other billing settings, contact support.<br>


# Partner SSO

If you're a ChargebackStop Partner you're able to integrate custom SSO for your users to easily authenticate into their organisations from your application, no re-authentication required.

{% hint style="warning" %}
Note that this is not for integrating enterprise SSO providers such as Okta or Azure AD for partner or merchant accounts, if you want to enable enterprise SSO login options please contact support.
{% endhint %}

These are instructions on how to generate Single Sign-On tokens on your server. These token can be used to authenticate your users into our merchant dashboard for your organisations.

### 1. Generate your Partner SSO Key

You can do this by logging into your partner dashboard and under **Settings > Single Sign-On (SSO) for Organisations** and generate a key. Store this key in a secure environment variable on your server. Do not expose this key in any frontend interface.

### 2. Get your Partner ID

From your partner dashboard grab the first ID in the URL. For example:

<pre><code><strong>https://dashboard.chargebackstop.com/partners/ptnr_T1Ng7EgYrY7uaKMgPsASE/settings
</strong></code></pre>

In this case <mark style="color:green;">**ptnr\_T1Ng7EgYrY7uaKMgPsASE**</mark> would be your Partner ID.

### 3. Generate your SSO Token

You can do this using all major programming languages, see below for common examples:

{% stepper %}
{% step %}

#### Install a JWT Library

We use JSON Web Tokens to securely authenticate your users. First, install the appropriate JWT library for your server.

{% tabs %}
{% tab title="Python" %}

```
pip install PyJWT
```

{% endtab %}

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

```
npm install --save jsonwebtoken
```

{% endtab %}

{% tab title="C#" %}

```
dotnet add package System.IdentityModel.Tokens.Jwt
```

{% endtab %}

{% tab title="Go" %}

```
go get github.com/golang-jwt/jwt
```

{% endtab %}

{% tab title="Java" %}

```
implementation 'com.auth0:java-jwt:4.4.0'
```

{% endtab %}

{% tab title="Ruby" %}

```
sudo gem install jwt
```

{% endtab %}
{% endtabs %}
{% endstep %}

{% step %}

#### Generate Tokens on your Server

**These tokens must be used within 15 minutes of issuance (UTC timezone)**

{% hint style="warning" %}
Make sure to thoroughly read through the appropriate documentation for your library and ensure the right security methods are used such as setting appropriate expiries on JWT tokens.
{% endhint %}

{% tabs %}
{% tab title="Python" %}

```python
import jwt
from datetime import datetime, timezone

private_key = 'YOUR_PRIVATE_PARTNER_SSO_KEY'

def create_chargebackstop_sso_token(email):
  user_data = {
      "iat": datetime.now(tz=timezone.utc),
      "user_email": email,
  }
  return jwt.encode(user_data, private_key, algorithm='HS256')
```

{% endtab %}

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

```javascript
const jwt = require('jsonwebtoken');

const privateKey = 'YOUR_PRIVATE_PARTNER_SSO_KEY';

function createChargebackstopSsoToken(email) {
  const userData = {
    iat: Math.floor(Date.now() / 1000),
    user_email: email,
  };

  return jwt.sign(userData, privateKey, { algorithm: 'HS256' });
}
```

{% endtab %}

{% tab title="C#" %}

```csharp
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;

public class JwtTokenGenerator
{
    private const string PrivateKey = "YOUR_PRIVATE_PARTNER_SSO_KEY";

    public static string CreateChargebackstopSsoToken(string email)
    {
        var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(PrivateKey));
        var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

        var claims = new[]
        {
            new Claim("user_email", email),
            new Claim(JwtRegisteredClaimNames.Iat, 
                new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64)
        };

        var token = new JwtSecurityToken(
            claims: claims,
            signingCredentials: credentials);

        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}

```

{% endtab %}

{% tab title="Go" %}

```go
package main

import (
	"fmt"
	"time"

	"github.com/golang-jwt/jwt"
)

var privateKey = []byte("YOUR_PRIVATE_PARTNER_SSO_KEY")

func createChargebackstopSsoToken(email string) (string, error) {
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"iat":        time.Now().Unix(),
		"user_email": email,
	})

	tokenString, err := token.SignedString(privateKey)
	if err != nil {
		return "", err
	}

	return tokenString, nil
}

```

{% endtab %}

{% tab title="Java" %}

```java
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;

import java.time.Instant;
import java.util.Date;

public class JwtGenerator {
    private static final String PRIVATE_KEY = "YOUR_PRIVATE_PARTNER_SSO_KEY";

    public static String createChargebackstopSsoToken(String email) {
        Algorithm algorithm = Algorithm.HMAC256(PRIVATE_KEY);

        return JWT.create()
                .withClaim("user_email", email)
                .withIssuedAt(Date.from(Instant.now()))
                .sign(algorithm);
    }
}
```

{% endtab %}

{% tab title="Ruby" %}

```ruby
require 'jwt'

PRIVATE_KEY = 'YOUR_PRIVATE_PARTNER_SSO_KEY'

def create_chargebackstop_sso_token(email)
  payload = {
    iat: Time.now.to_i,
    user_email: email
  }

  JWT.encode(payload, PRIVATE_KEY, 'HS256')
end

```

{% endtab %}
{% endtabs %}
{% endstep %}
{% endstepper %}

### 4. Redirect your user

Your user must already be a member of an organisation with your partner account. If the user is not a member the SSO will fail, and due to security implications you cannot SSO a user that is a member of an organisation outside of your partner account.

{% hint style="success" %}
The token you generate must be used within 15 minutes of creation, so we recommend only creating these tokens when the user wants to be redirected rather than ahead of time
{% endhint %}

Your <mark style="color:green;">**Base URL**</mark> will be either **<https://dashboard.chargebackstop.com>,** or your own white label instance such **<https://chargebacks.example.com>**, make sure to redirect your customer to the right one.

Your <mark style="color:green;">**Partner ID**</mark> will be the ID we retrieved earlier such as **ptnr\_T1Ng7EgYrY7uaKMgPsASE**

Your <mark style="color:green;">**SSO Token**</mark> will be the token we generated above. You need to generate this token each time.

**Putting it all together**

```
{Base URL}/auth/partner-organisation-sso?partner_id={Partner ID}&token={SSO Token}
```

**For example**

<https://dashboard.chargebackstop.com/auth/partner-organisation-sso?partner\\_id=ptnr\\_T1Ng7xxxxx\\&token=eyJhbGciOiJIUzI1NiIsInR5cCIyyyyy>

If you have any questions, please let our customer success team know and we'll do our best to help you.


# Introduction

Digital Receipts let you send additional order information to cardholders and their banks, including subscription details, itemised breakdowns, in-flight refunds, and more. When implemented, they reduce cardholder confusion, help banks better understand charges before raising a dispute, and ultimately lower your chargeback rate.

<figure><img src="/files/j2gcD38voVBdqXzgipee" alt=""><figcaption></figcaption></figure>

### How it works for cardholders

{% stepper %}
{% step %}

#### Cardholder views a transaction

A cardholder opens a transaction in their banking app.
{% endstep %}

{% step %}

#### Real-time lookup

The app performs a real-time lookup to check whether additional order data is available.
{% endstep %}

{% step %}

#### Data displayed

If data is available, it's shown directly in the cardholder's banking app alongside the transaction.
{% endstep %}
{% endstepper %}

### How it works for banking agents

{% stepper %}
{% step %}

#### Cardholder contacts their bank

A cardholder queries a transaction with their bank.
{% endstep %}

{% step %}

#### Agent opens the dispute portal

The banking agent opens the Mastercard or Visa dispute portal.
{% endstep %}

{% step %}

#### Real-time lookup

A real-time lookup checks whether additional order data is available.
{% endstep %}

{% step %}

#### Data displayed

If data is available, the agent sees it in the portal before proceeding with the dispute.
{% endstep %}
{% endstepper %}

### What data can be returned?

Depending on what's available at the time of the query, the platform automatically shapes the response to provide maximum context. This can include:

* Subscription details
* Transaction details
* Merchant details
* Refund details
* Digital deliveries
* Physical deliveries
* Hospitality booking details

{% hint style="info" %}
See the full options for enrichment using our [Orders API](/developer/api-documentation/orders)
{% endhint %}

### Card network integration

Digital Receipts are powered by two card network solutions. We harmonise both systems into a single unified interface built to scale and support both programs as they evolve. You never need to integrate directly or manage those connections.

* **Order Insights** (Visa)
* **Consumer Clarity** (Mastercard)


# Use cases

**B2C subscription products**

* Display subscription billing cycles to cardholders
* Embed deep-links for managing subscriptions or requesting refunds

**Merchant of Record PSPs**

* Surface the details of the sub-merchant that delivered the goods or services

**eCommerce brands**

* Show itemised breakdowns of items ordered, including delivery and tracking status


# Intergrating

You may need to perform additional integration work to support Digital Receipts. Depending on your payment processor or CRM, we may need extra information to return data to cardholders and their banks.

During your initial conversations with our team, we'll evaluate which of three paths applies to your setup:

1. **No additional work needed:** We can gather enough data from your payment processor, CRM, or eCommerce platform. No work required from your technical team.
2. **Partial order enrichment:** We can pull most of the data from your existing integrations, but some additional enrichment is needed from your side.
3. **Complete order data:** Most of the data we need lives outside of our reach. Your systems need to pass complete order data to ours.

{% hint style="info" %}
Both partial enrichment and complete order data flows are powered by our [Orders API](/developer/api-documentation/orders). Depending on your use case, integration method, and available data, we'll tell you exactly what fields you need to provide.
{% endhint %}

### How partial order enrichment works

{% stepper %}
{% step %}

#### We connect to your systems

We integrate with your payment processor, CRM, or eCommerce platform.
{% endstep %}

{% step %}

#### Order data is imported

As much order data as possible is pulled into our systems automatically.
{% endstep %}

{% step %}

#### You enrich remaining data

You make an API request to enrich existing order IDs with any data we couldn't pull ourselves.
{% endstep %}
{% endstepper %}


# Introduction

Systematic Dispute Deflection prevents friendly fraud by proving historic cardholder purchase activity with qualifying data. When a deflection is successful, the dispute is blocked by the card networks before it's raised. You retain the funds, prevent fraud claims (TC40s) from being filed, and reduce your dispute rate.

<figure><img src="/files/y02szPwP0gZb3nNjnUA5" alt=""><figcaption></figcaption></figure>

### How it works

{% stepper %}
{% step %}

#### Cardholder attempts a dispute

A cardholder contacts their bank to dispute a transaction.
{% endstep %}

{% step %}

#### Real-time lookup

The card network performs a real-time lookup against your historical transaction data.
{% endstep %}

{% step %}

#### Dispute blocked

If the criteria for deflection are met, the dispute is instantly blocked and never reaches you.
{% endstep %}
{% endstepper %}

### Historic purchase history

A dispute becomes eligible for deflection when the cardholder has completed at least two previous transactions within 120 to 365 days before the disputed charge. These earlier transactions form the purchase history used to verify legitimacy.

### Qualifying data

For a deflection to succeed, identifiers unique to the cardholder must match across the disputed transaction and the historic purchase history. Qualifying identifiers include:

* IP address
* Email address
* Device fingerprint
* Billing address

At least two identifiers must match, and one of them must be either an IP address or an email address.

### Eligible dispute conditions

Currently, deflection is attempted for the following dispute conditions:

* **Visa 10.4** — Card Not Present
* **Mastercard 4814** — Card Not Present

Other conditions may become eligible as the card networks expand their programs.

### Card network integration

Systematic Dispute Deflection is powered by two card network solutions. We harmonise both systems into a single unified interface built to scale and support both programs as they evolve. You never need to integrate directly or manage those connections.

* **Compelling Evidence 3.0** (Visa)
* **First Party Trust** (Mastercard)


# Integrating

Systematic Dispute Deflection builds on top of the same data set used for [Digital Receipts](https://docs.chargebackstop.com/digital-receipts), in addition to the qualifying data.

* Customer's IP address or device fingerprint
* Customer's email address

The qualifying data identifiers listed above are mandatory for deflection. If you're setting up Digital Receipts alongside this product, pay close attention to the required fields for Consumer Clarity, First Party Trust, Order Insights, and Compelling Evidence 3.0 during your integration.

{% hint style="info" %}
Both products share the same underlying data pipeline. Getting your Digital Receipts integration right means Systematic Dispute Deflection works out of the box.
{% endhint %}


# VAMP Ratio Reduction

The [Visa Acquirer Monitoring Program (VAMP)](https://www.chargebackstop.com/visa-acquirer-monitoring-program) is Visa's unified framework for monitoring fraud and dispute levels across merchants. If your VAMP ratio exceeds Visa's thresholds, you risk fines, restrictions, or removal from the network.

[Systematic Dispute Deflection](/systematic-dispute-deflection/introduction) is the only tool available that can prevent first-party misuse disputes and remove the associated fraud notices (TC40s) from your VAMP ratio. Chargeback alerts and other prevention tools resolve disputes reactively, but the TC40 has already been filed and still counts against you. Systematic Dispute Deflection blocks the dispute before it's created, and the TC40 is excluded from your VAMP calculation entirely.

{% hint style="success" %}
**The short version:** Systematic Dispute Deflection is the single most effective way to reduce your VAMP ratio from first-party misuse. No other tool prevents the dispute and removes the TC40 at the same time.
{% endhint %}

### What is VAMP?

Visa introduced VAMP in 2024/25 to replace its previous fragmented monitoring programs with a single framework. It monitors your fraud and dispute activity relative to your transaction volume. If your ratio crosses Visa's thresholds, your acquirer is notified and you may face escalating consequences.

### What changed

Visa updated the VAMP formula to include all disputes, both fraudulent and non-fraud, rather than fraud-only. This makes first-party misuse (friendly fraud, subscription confusion, unrecognised charges) significantly more important to your ratio than before, and harder to address without the right tooling.

{% hint style="info" %}
[See here for a full breakdown of the ratios and threadholds for merchants and acquirers](https://www.chargebackstop.com/visa-acquirer-monitoring-program)
{% endhint %}

### How Systematic Dispute Deflection reduces your ratio

When a cardholder attempts to dispute a transaction, our system checks their purchase history against qualifying identifiers you've provided through Digital Receipts, IP address, email, device fingerprint, and billing address. If the evidence proves prior legitimate activity, the dispute is blocked at the network level.

When a dispute is successfully deflected:

* The dispute never reaches you
* The TC40 is excluded from your VAMP ratio
* You retain the funds from the original transaction

The richer your Digital Receipts integration, the more transaction data and qualifying identifiers are available, and the more disputes become eligible for deflection. This is why we recommend implementing Digital Receipts and Systematic Dispute Deflection together, the same data powers both products.

### Why other tools can't do this

Chargeback Alerts let you refund a transaction and prevent the chargeback from landing, but by the time an alert is raised, the TC40 has already been filed with Visa. The dispute is resolved, but your VAMP ratio still takes the hit.

Systematic Dispute Deflection intervenes earlier. It blocks the dispute before it's created, which is the only way to prevent the TC40 from being filed in the first place. This makes it the only product in the ecosystem that both prevents first-party misuse disputes and reduces your VAMP exposure.

### What you should do

If you're already using Systematic Dispute Deflection, your deflections are automatically benefiting your VAMP ratio, no additional setup is required.

If you're not yet enrolled, see the [introduction](https://docs.chargebackstop.com/systematic-dispute-deflection/introduction) for how it works, or the [integration guide](https://docs.chargebackstop.com/systematic-dispute-deflection/integrating) to begin.

{% hint style="info" %}
**Want to understand your current VAMP exposure?** Contact our support team. We can help you assess your ratio and estimate the impact of Systematic Dispute Deflection on your standing.
{% endhint %}


# Introduction

Chargeback prevention alerts warn you of a dispute before it becomes an official chargeback with your payment service provider. When you receive an alert, you can refund the transaction and avoid the chargeback entirely.

### How does it works?

{% stepper %}
{% step %}

#### Cardholder starts chargeback

A cardholder contacts their bank to dispute a transaction they made with your business.
{% endstep %}

{% step %}

#### Card issuer checks runs check

As the issuer begins filing the dispute, they check whether you're enrolled to receive alerts (this depends on the card network and issuer).
{% endstep %}

{% step %}

#### Alert received from the card networks

We receive the alert in real time and locate the order in your payment processor or connected gateway.
{% endstep %}

{% step %}

#### Dispute deflected based on setup

Based on rules you configure during onboarding or later in your dashboard, the system takes one of these actions:

* **Refund and resolve:** We refund the transaction and mark the alert as resolved.
* **Decline the alert:** The alert is declined and the dispute proceeds normally.
* **Manual decision:** The alert appears in your dashboard and triggers an email, giving you 24–48 hours to decide whether to refund.
  {% endstep %}
  {% endstepper %}


# Alert Providers

Chargeback alerts are powered by Visa and Mastercard. ChargebackStop is an authorized, direct reseller of alert data from both card networks. We handle onboarding, management, and automation so you get full value from alerts without extra overhead.

**Ethoca Alerts (Mastercard)** covers roughly 95% of Mastercard-related chargebacks and about 40% of Visa-related chargebacks. [Learn more here.](/chargeback-alerts/ethoca-alerts)

**Verifi RDR (Visa)** covers roughly 95% of Visa-related chargebacks. [Learn more here.](/chargeback-alerts/verifi-rdr)

When both programs are enabled together, merchants typically see up to 95%\* dispute prevention coverage across all Visa and Mastercard transactions.

{% hint style="info" %}
**A note on coverage estimates:** The 95% figure reflects what we typically see across our customer portfolio. Your actual coverage depends on issuer spread and cardholder regions. Contact our sales or support team with details about your setup and volume, we can give you a specific estimate before you onboard.
{% endhint %}

### What about Amex & Discover?

Both alert providers offer some coverage for Amex and Discover disputes, but availability depends on your payment processor setup. Reach out to sales or support to confirm what's possible for your configuration.


# Enrolments

### **Enrolling for the first time**

An enrollment registers your merchant setup with ChargebackStop and the card networks (Visa and Mastercard). You'll likely have more than one enrollment, each merchant account and product requires its own.

Our onboarding and support teams handle all enrollment work and communicate with the card networks on your behalf. No action needed from you.

{% hint style="info" %}
Enrollment timelines vary by product and provider. When we begin an enrollment, we'll tell you the expected timeframe, typically 2–10 working days to be fully enrolled across all programs. See individual product pages on this site for specifics.
{% endhint %}

View your enrollments in your dashboard under **Settings > Enrollments.**

### Ethoca descriptor conflicts

Ethoca enrollment is descriptor-based. If your descriptor is too generic or already enrolled by another merchant globally across Ethoca's network, the enrollment may need attention. When this happens, our support team will reach out to discuss alternatives — for example, using a more specific descriptor or a different match type. This typically takes 1–2 business days to resolve, after which we resubmit the enrollment.

### **Changing enrollment settings**

ChargebackStop manages your enrollments, but if you need to pause, extend, or reduce coverage, contact our support team. They'll gather the required information and make the changes. Depending on what you're changing, there may be a delay while we coordinate with the card networks.

### **Transferring enrollments from another provider**

If you're already enrolled with another provider and want to move to ChargebackStop, you can transfer your enrollments to avoid any gap in coverage.

Transfers require:

* Approval from your current provider
* Authorization from ChargebackStop to receive the transfer
* All fees paid and notice given to your current provider

Once your provider initiates the transfer, we work directly with the card networks to ensure coverage continues without interruption.

To start a transfer, contact our sales or support team. They'll provide an email template you can send to your current provider and walk you through the process.


# Resolution Rules

Resolution rules tell the system how to handle incoming alerts, whether to refund, dismiss, or escalate for manual review. You can filter by transaction amount, date, reason code, reason category, and more.

Go to **Settings > Resolution Rules** to create, edit, or delete rules.

### Creating a new rule

Each resolution rule applies to exactly one enrollment. If you have multiple enrollments or merchant accounts, you'll need separate rules for each. This gives you precise control over behavior per merchant account.

### Modifying existing rules

Changes to a resolution rule apply only to alerts received after you save. Already-received alerts aren't affected. If you have unresolved alerts that should match a new rule, contact support, we can re-queue them for processing.

### Deleting or disabling rules

{% hint style="danger" %}
Deleting or disabling a rule stops it from applying to new alerts immediately. It won't change your enrollment settings or affect previously resolved or declined alerts.
{% endhint %}

If an incoming alert doesn't match any rule, it goes to manual review in the Action Required tab. You'll have up to 48 hours to make a refund decision (timeframe varies by network). After that, the alert is automatically declined.


# Ethoca Alerts

Ethoca Alerts cover roughly 95% of Mastercard disputes and 40% of Visa disputes. Alerts can be fully automated or managed individually.

When you receive an alert, you have two options:

1. Refund the transaction and resolve the alert
2. Dismiss the alert, skip the refund, and let the chargeback proceed

If you've set up a resolution rule, this happens automatically. Otherwise, the alert appears in your dashboard. You have 48 hours from receipt to provide an outcome. If you don't respond in time, the alert is automatically declined and the chargeback comes through.

Important: Ethoca Alerts must be refunded at the gateway level to prevent a chargeback. Marking an alert as resolved in the portal or API alone won't stop the dispute, the transaction must also be refunded through your gateway.

### Enrollment

Ethoca enrollment is descriptor-based. During onboarding, we'll ask for all billing descriptors used by your merchant account(s). Provide every variation: soft, hard, static, dynamic. Missing a descriptor reduces your coverage.

We evaluate each descriptor to confirm:

1. It's unique enough to enroll
2. It isn't already enrolled elsewhere

If a descriptor is too generic (e.g., "Pet Store"), another merchant may already be enrolled with the same name, blocking your enrollment or causing you to receive their alerts. Using your site name or legal company name typically works better.

### Billing

Ethoca Alerts are billed at a fixed per-alert fee, whether you refund or not.

### Invalid alerts

Around 5–10% of Ethoca alerts each month are invalid. Examples:

* The transaction was already refunded
* The transaction was already disputed
* The alert is clearly for another merchant
* You can't find the transaction
* It's a duplicate of a previous alert

Our system detects invalid alerts automatically, filters them out, and credits your invoice. You never pay for an alert that provided no value. If you spot an invalid alert that wasn't auto-detected, submit it for manual credit review in your dashboard. If the card networks approve, the credit appears on your next invoice.

#### Valid duplicate alerts

Although rare, cross network alerts are still valid, if we see alerts raised by both networks these are counted:

**Scenario 1**

An Ethoca alert is raised, you accept the chargeback and allow it to flow. Verifi then also raise an alert for this transaction which, is not automatically refunded (as you accepted Ethoca). In this case, both alerts are valid with the Verifi not being a duplicate as the chargeback alert is still correct.

**Scenario 2**

An Ethoca alert is raised, you refund, but Verifi still triggers. This may happen if both alerts within 24 hour window from each other.

### Reducing coverage

If you want to filter alerts by amount, date, reason code, or another criterion, contact sales or support. We work with the card networks case by case to add filter capabilities to your descriptor.

For example: if you'd refund low-value chargebacks but prefer to accept high-value ones, a filter lets you avoid paying alert fees on alerts you don't plan to act on.

### Multiple merchant accounts

If multiple merchant accounts share the same descriptor, our system attempts to locate the transaction across all connected payment providers.

When possible, we recommend adding a unique character to each merchant account's descriptor. This helps us match orders more efficiently during alert processing, especially at higher volumes.


# Verifi RDR

Verifi RDR (Rapid Dispute Resolution) covers roughly 95% of Visa transactions. Unlike Ethoca Alerts, RDR cases are fully automated at the Visa network level, you can't manage them individually.

During onboarding, you'll define which transactions should be automatically refunded and which should proceed as disputes. These rules apply to all cases unless you request changes.

### Available RDR rules

Work with our support team during onboarding to set your strategy. You can filter by:

* Purchase amount
* Date
* Currency
* Reason code
* Reason category
* Transaction identifier
* BIN range

These rules are defined upfront and apply automatically. Changes can take anywhere from a few minutes to a few days depending on your setup.

### Enrollment

Verifi RDR enrollment requires your Visa BIN and CAID. These identifiers are set by your payment processor for each merchant account.

* BIN: A six-digit number identifying your acquiring bank
* CAID: A 16-character alphanumeric identifier unique to your merchant account with your acquirer

Together, these form your unique Visa enrollment.

Most processors don't display this information by default, but their support team or your relationship manager can provide it. Our team will collect and validate your BINs and CAIDs before submitting them. Visa's enrollment SLA is 10 working days, though it sometimes completes sooner.

{% hint style="success" %}
**Can't get your BIN and CAID?** Provide one or two ARNs (acquirer reference numbers) from recent Visa transactions, completed, refunded, or disputed, within the last 120 days. An ARN is a 23-digit transaction identifier. We can use it to query your BIN and CAID directly with Visa.
{% endhint %}

### Billing

Verifi RDR cases are billed per case, regardless of refund outcome. We also offer a model where you're only billed for refunded cases. Contact us for details.

### Multiple merchant accounts

Multiple merchant accounts typically means multiple BINs and/or CAIDs. Make sure you provide all of them. If using ARN lookup instead, provide at least one or two ARNs per merchant account.


# Transaction Matching

Our system locates transactions in your account using data received from the card networks. This is the foundation for automation and resolution rules.

Depending on your account agreement, available integrations, and connector quality, we offer either strict matching or soft matching.

### Strict matching

With strict matching enabled, every alert from the card network is matched to a transaction in your payment processor, and the refund is confirmed as executed.

If the system can't find a match, someone on our team manually investigates using the alert data and transaction records. If we still can't locate it, you're automatically credited for that alert.

### Soft matching

If connector data quality or account configuration prevents strict matching, we fall back to soft matching. The system attempts a best-case match, but if it fails, the alert lands in your dashboard as "Action Required" for manual review.

We try to offer strict matching to as many customers as possible at no extra cost. But some setups make it technically impossible, missing data fields, limited query options, or specific configurations. Examples:

* Digital wallet providers like Stripe Link that mask card data
* Network tokens that scramble card data, increasing the risk of invalid matches

### Unmatchable transactions

Sometimes the data from the card network isn't enough to produce a definitive match, or any results at all.

If you're on strict matching, these cases are fully creditable. You won't pay for them.

However, if matching issues stem from your specific setup (e.g., using Stripe Link in wallet mode), Mastercard may consider it a problem on your end rather than theirs. In those cases, we likely can't credit every instance.


# API documentation

All API endpoints authenticate using bearer tokens (`Authorization: Bearer cbs_<token>`). There are two types of API key:

* **Partner key** — tied to a partner group; can access resources across all organisations within that group.
* **Organisation key** — tied to a single organisation; can only access resources belonging to that organisation.

The sections below describe which APIs are available to each key type.

## Partner-level only

These APIs are restricted to partner keys. Requests made with an organisation key will be rejected.

They cover tenant provisioning — creating and managing organisations, merchants, memberships, and enrolments.

| API                        | Methods                       | Docs                                                        |
| -------------------------- | ----------------------------- | ----------------------------------------------------------- |
| Organisations              | `POST` `GET` `PATCH` `DELETE` | [Organisations](/developer/api-documentation/organisations) |
| Merchants                  | `POST` `GET` `PATCH` `DELETE` | [Merchants](/developer/api-documentation/merchants)         |
| Memberships                | `POST` `GET` `PATCH` `DELETE` | [Memberships](/developer/api-documentation/memberships)     |
| Enrolments V1 (deprecated) | `POST` `GET` `DELETE`         | —                                                           |
| Enrolments V2              | `POST` `GET` `DELETE`         | [Enrolments](/developer/api-documentation/enrolments)       |

{% hint style="info" %}
**Note:** Enrolments V1 is deprecated. Use the V2 enrolments API for all new integrations. V2 adds support for enrolling a descriptor with multiple merchants in a single request.
{% endhint %}

{% hint style="info" %}
**Note:** Access to the Merchants and Enrolments V2 APIs with an organisation key can be enabled for select organisations on request. Contact support if you need this.
{% endhint %}

## Organisation and partner level

These APIs accept both key types. Data is automatically scoped to the organisations the key has access to.

| API            | Methods              | Docs                                                          |
| -------------- | -------------------- | ------------------------------------------------------------- |
| Alerts         | `GET` `PATCH`        | [Alerts](/developer/api-documentation/alerts)                 |
| Orders         | `GET` `POST` `PATCH` | [Orders](/developer/api-documentation/orders)                 |
| Integrations   | `GET` `POST` `PATCH` | [Integrations](/developer/api-documentation/integrations)     |
| Lookups        | `GET`                | [Lookups](/developer/api-documentation/lookups)               |
| Representments | `GET` `POST` `PATCH` | —                                                             |
| Scheme Notices | `GET`                | [Scheme Notices](/developer/api-documentation/scheme-notices) |
| Simulations    | `POST` `PATCH`       | [Simulations](/developer/api-documentation/simulations)       |


# Integrations

Manage integrations used by ChargebackStop APIs.

This endpoint set currently supports creating and managing `CUSTOM_ORDERS` integrations, and listing all integrations your API key can access.

Base URL: `https://api.chargebackstop.com/v1/integrations/`\
Authentication: Bearer token via API key.

This endpoint set supports both **organisation-level** and **partner-level** keys.

`integrations:read` and `integrations:write` are enabled automatically for every API key.

* `integrations:read` for GET endpoints
* `integrations:write` for POST and PATCH endpoints

Access scope model:

* Organisation-level keys can access integrations for their own organisation only.
* Admin partner-group keys can access integrations across all organisations for their partner.
* Non-admin partner-group keys can access integrations only for organisations assigned to their group.

{% hint style="info" %}
API-level summary:

* Organisation-level and Partner-level keys are supported.
* Use `integrations:read` for GET endpoints and `integrations:write` for POST/PATCH endpoints.
  {% endhint %}

***

## GET /v1/integrations - List integrations

Returns integrations from organisations accessible to the API key.

API level: Organisation-level and Partner-level\
Authentication: `integrations:read`

### Query parameters

No custom filters are currently supported.

Pagination parameters are supported:

| Parameter | Type    | Description                |
| --------- | ------- | -------------------------- |
| `limit`   | integer | Number of results per page |
| `offset`  | integer | Number of results to skip  |

### Example 1 request (partner-level key)

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

### Example 1 response (partner-level key)

```json
{
  "items": [
    {
      "id": "int_abc123",
      "organisation_id": "org_xyz456",
      "name": "Orders API Integration",
      "type": "CUSTOM_ORDERS",
      "status": "ENABLED",
      "merchant_ids": ["mrch_111", "mrch_222"],
      "links": [
        {
          "rel": "self",
          "uri": "/v1/integrations/int_abc123"
        }
      ]
    },
    {
      "id": "int_def789",
      "organisation_id": "org_xyz456",
      "name": "Stripe Main",
      "type": "STRIPE",
      "status": "ENABLED",
      "merchant_ids": ["mrch_111"],
      "links": [
        {
          "rel": "self",
          "uri": "/v1/integrations/int_def789"
        }
      ]
    }
  ],
  "count": 2
}
```

### Example 2 request (organisation-level key)

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

### Example 2 response (organisation-level key)

```json
{
  "items": [
    {
      "id": "int_abc123",
      "organisation_id": "org_xyz456",
      "name": "Orders API Integration",
      "type": "CUSTOM_ORDERS",
      "status": "ENABLED",
      "merchant_ids": ["mrch_111", "mrch_222"],
      "links": [
        {
          "rel": "self",
          "uri": "/v1/integrations/int_abc123"
        }
      ]
    }
  ],
  "count": 1
}
```

***

## POST /v1/integrations - Create integration

Create a new integration.

API level: Organisation-level and Partner-level\
Authentication: `integrations:write`

### Request body

| Field             | Type           | Required | Validation                                                                          | Description                            |
| ----------------- | -------------- | -------- | ----------------------------------------------------------------------------------- | -------------------------------------- |
| `organisation_id` | string         | Yes      | Must be accessible by API key                                                       | Organisation that owns the integration |
| `name`            | string         | Yes      | 1-255 chars                                                                         | Integration display name               |
| `type`            | string         | Yes      | Must be `CUSTOM_ORDERS`                                                             | Integration type                       |
| `status`          | string         | No       | `ENABLED` or `DISABLED`                                                             | Initial integration status             |
| `merchant_ids`    | array\[string] | Yes      | Non-empty, unique, all merchants must belong to `organisation_id` and be accessible | Merchants linked to this integration   |

Status behavior on create:

* If `status` is omitted, integrations are created with `status: ENABLED`.

### Example 3 request

```bash
curl -X POST "https://api.chargebackstop.com/v1/integrations/" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "organisation_id": "org_xyz456",
    "name": "Orders API Integration",
    "type": "CUSTOM_ORDERS",
    "status": "DISABLED",
    "merchant_ids": ["mrch_111", "mrch_222"]
  }'
```

### Example 3 response

```json
{
  "id": "int_abc123",
  "organisation_id": "org_xyz456",
  "name": "Orders API Integration",
  "type": "CUSTOM_ORDERS",
  "status": "DISABLED",
  "merchant_ids": ["mrch_111", "mrch_222"],
  "links": [
    {
      "rel": "self",
      "uri": "/v1/integrations/int_abc123"
    }
  ]
}
```

***

## GET /v1/integrations/{integration\_id} - Get integration by ID

Retrieve a single integration by ID.

API level: Organisation-level and Partner-level\
Authentication: `integrations:read`

### URL parameters

| Parameter        | Type   | Description    |
| ---------------- | ------ | -------------- |
| `integration_id` | string | Integration ID |

### Example 4 request

```bash
curl -X GET "https://api.chargebackstop.com/v1/integrations/int_abc123" \
  -H "Authorization: Bearer <api_key>"
```

### Example 4 response

```json
{
  "id": "int_abc123",
  "organisation_id": "org_xyz456",
  "name": "Orders API Integration",
  "type": "CUSTOM_ORDERS",
  "status": "ENABLED",
  "merchant_ids": ["mrch_111", "mrch_222"],
  "links": [
    {
      "rel": "self",
      "uri": "/v1/integrations/int_abc123"
    }
  ]
}
```

<details>

<summary>404 Not Found — Integration does not exist (example)</summary>

```json
{
  "errors": [
    {
      "code": "NOT_FOUND",
      "message": "Not found"
    }
  ]
}
```

</details>

***

## PATCH /v1/integrations/{integration\_id} - Update integration

Update an existing integration.

API level: Organisation-level and Partner-level\
Authentication: `integrations:write`

### Important behavior

* `status` updates are only allowed for `CUSTOM_ORDERS` integrations.
* The only allowed status value in PATCH is `DISABLED`.
* `name` can be updated.
* You must provide at least one of `status` or `name`.

### URL parameters

| Parameter        | Type   | Description              |
| ---------------- | ------ | ------------------------ |
| `integration_id` | string | Integration ID to update |

### Request body

| Field    | Type   | Required | Validation                                             | Description            |
| -------- | ------ | -------- | ------------------------------------------------------ | ---------------------- |
| `status` | string | No       | `DISABLED` only; only for `CUSTOM_ORDERS` integrations | New integration status |
| `name`   | string | No       | 1-255 chars                                            | New integration name   |

### Example 5 request

```bash
curl -X PATCH "https://api.chargebackstop.com/v1/integrations/int_abc123" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Orders API Integration - EU"
  }'
```

### Example 5 response

```json
{
  "id": "int_abc123",
  "organisation_id": "org_xyz456",
  "name": "Orders API Integration - EU",
  "type": "CUSTOM_ORDERS",
  "status": "DISABLED",
  "merchant_ids": ["mrch_111", "mrch_222"],
  "links": [
    {
      "rel": "self",
      "uri": "/v1/integrations/int_abc123"
    }
  ]
}
```

***

## Common error responses

<details>

<summary>401 Unauthorised (example)</summary>

```json
{
  "errors": [
    {
      "code": "UNAUTHORISED",
      "message": "Unauthorised"
    }
  ]
}
```

</details>

<details>

<summary>403 Forbidden (missing ability) (example)</summary>

```json
{
  "errors": [
    {
      "code": "FORBIDDEN",
      "message": "Forbidden"
    }
  ]
}
```

</details>

<details>

<summary>404 Not found (example)</summary>

Returned when the target organisation or integration is not found.

```json
{
  "errors": [
    {
      "code": "NOT_FOUND",
      "message": "Not found"
    }
  ]
}
```

</details>

<details>

<summary>422 Unprocessable Entity (business validation) (example)</summary>

```json
{
  "errors": [
    {
      "code": "INVALID_MERCHANT_IDS",
      "message": "One or more merchant_ids are invalid or inaccessible for this organisation"
    }
  ]
}
```

</details>

Other business validation codes you may receive:

| Code                       | Meaning                                                                                             |
| -------------------------- | --------------------------------------------------------------------------------------------------- |
| `DUPLICATE_MERCHANT_IDS`   | `merchant_ids` contains duplicate values                                                            |
| `INVALID_MERCHANT_IDS`     | One or more merchants do not exist, are inaccessible, or do not belong to the provided organisation |
| `INVALID_INTEGRATION_TYPE` | PATCH attempted on a non-`CUSTOM_ORDERS` integration                                                |
| `MISSING_UPDATE_FIELDS`    | PATCH request did not include either `status` or `name`                                             |


# Merchants

Manage merchants under your partner account.

This endpoint set is **partner-level only**. Organisation API keys cannot access these endpoints.

**Base URL:** `https://api.chargebackstop.com/v1/merchants/`\
**Authentication:** Bearer token via API key.

Required abilities:

* `merchants:read` for GET endpoints
* `merchants:write` for POST, PATCH, and DELETE endpoints

Access scope model:

* Admin partner-group keys can access merchants across all organisations belonging to their partner.
* Non-admin partner-group keys can access merchants only in organisations explicitly assigned to their group.
* Organisation-level keys receive `401 Unauthorised` on all endpoints in this API.

***

## GET /v1/merchants - List merchants

Returns merchants accessible to the authenticated partner-group API key.

**API level:** Partner-level only\
**Authentication:** `merchants:read`

### Query parameters

| Parameter         | Type    | Description                                                                            |
| ----------------- | ------- | -------------------------------------------------------------------------------------- |
| `organisation_id` | string  | Filter by organisation ID                                                              |
| `type`            | string  | Filter by merchant type                                                                |
| `name`            | string  | Case-insensitive partial match on merchant name                                        |
| `external_id`     | string  | Filter by exact external merchant ID                                                   |
| `sort`            | string  | Sort field: `name`, `type`, `created_at`, `updated_at`; prefix with `-` for descending |
| `limit`           | integer | Number of results per page                                                             |
| `offset`          | integer | Number of results to skip                                                              |

{% hint style="info" %}
When `sort` is omitted, results are ordered by `created_at` descending.
{% endhint %}

### Example request

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

### Example response

```json
{
  "items": [
    {
      "id": "mrch_abc123",
      "organisation_id": "org_xyz456",
      "name": "Acme Stripe EU",
      "type": "STRIPE",
      "external_id": "acct_eu_001",
      "default_representment_service_type": "PHYSICAL_GOODS",
      "created_at": "2026-02-10T09:30:00Z",
      "updated_at": "2026-02-10T09:30:00Z",
      "links": [
        {
          "rel": "self",
          "uri": "/v1/merchants/mrch_abc123"
        }
      ]
    },
    {
      "id": "mrch_def789",
      "organisation_id": "org_xyz456",
      "name": "Acme Adyen UK",
      "type": "ADYEN",
      "external_id": null,
      "default_representment_service_type": null,
      "created_at": "2026-02-08T15:12:44Z",
      "updated_at": "2026-02-08T15:12:44Z",
      "links": [
        {
          "rel": "self",
          "uri": "/v1/merchants/mrch_def789"
        }
      ]
    }
  ],
  "count": 2
}
```

***

## POST /v1/merchants - Create merchant

Create a new merchant for an accessible organisation.

**API level:** Partner-level only\
**Authentication:** `merchants:write`

### Request body

| Field                                | Type   | Required | Validation                                                 | Description                                                                                                             |
| ------------------------------------ | ------ | -------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `organisation_id`                    | string | Yes      | Must be accessible by API key                              | Organisation that owns the merchant                                                                                     |
| `name`                               | string | Yes      | Min length 1                                               | Merchant display name                                                                                                   |
| `type`                               | string | Yes      | Must be a supported merchant type                          | Merchant type                                                                                                           |
| `external_id`                        | string | No       | Max length 250                                             | Optional external merchant ID                                                                                           |
| `default_representment_service_type` | string | No       | `PHYSICAL_GOODS`, `ONLINE_SERVICES`, or `OFFLINE_SERVICES` | Default representment service type. If you do not use ChargebackStop representment/disputes workflows, omit this field. |

{% hint style="info" %}
If you do not use ChargebackStop representment/disputes workflows, skip `default_representment_service_type`.
{% endhint %}

### Example request

```bash
curl -X POST "https://api.chargebackstop.com/v1/merchants/" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "organisation_id": "org_xyz456",
    "name": "Acme Stripe US",
    "type": "STRIPE",
    "external_id": "acct_us_002",
    "default_representment_service_type": "PHYSICAL_GOODS"
  }'
```

### Example response

```json
{
  "id": "mrch_new123",
  "organisation_id": "org_xyz456",
  "name": "Acme Stripe US",
  "type": "STRIPE",
  "external_id": "acct_us_002",
  "default_representment_service_type": "PHYSICAL_GOODS",
  "created_at": "2026-02-16T12:00:00Z",
  "updated_at": "2026-02-16T12:00:00Z",
  "links": [
    {
      "rel": "self",
      "uri": "/v1/merchants/mrch_new123"
    }
  ]
}
```

***

## GET /v1/merchants/{merchant\_id} - Get merchant by ID

Retrieve one accessible merchant by ID.

**API level:** Partner-level only\
**Authentication:** `merchants:read`

### URL parameters

| Parameter     | Type   | Description |
| ------------- | ------ | ----------- |
| `merchant_id` | string | Merchant ID |

### Example request

```bash
curl -X GET "https://api.chargebackstop.com/v1/merchants/mrch_abc123" \
  -H "Authorization: Bearer <api_key>"
```

### Example response

```json
{
  "id": "mrch_abc123",
  "organisation_id": "org_xyz456",
  "name": "Acme Stripe EU",
  "type": "STRIPE",
  "external_id": "acct_eu_001",
  "default_representment_service_type": "PHYSICAL_GOODS",
  "created_at": "2026-02-10T09:30:00Z",
  "updated_at": "2026-02-16T09:05:00Z",
  "links": [
    {
      "rel": "self",
      "uri": "/v1/merchants/mrch_abc123"
    }
  ]
}
```

***

## PATCH /v1/merchants/{merchant\_id} - Update merchant

Update one accessible merchant.

**API level:** Partner-level only\
**Authentication:** `merchants:write`

### URL parameters

| Parameter     | Type   | Description           |
| ------------- | ------ | --------------------- |
| `merchant_id` | string | Merchant ID to update |

### Request body

At least one field should be provided.

| Field                                | Type           | Required | Validation                                                         | Description                                                                                                                     |
| ------------------------------------ | -------------- | -------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| `name`                               | string         | No       | Min length 1                                                       | Updated merchant name                                                                                                           |
| `type`                               | string         | No       | Must be a supported merchant type                                  | Updated merchant type                                                                                                           |
| `external_id`                        | string or null | No       | Optional                                                           | Updated external ID (`null` clears value)                                                                                       |
| `default_representment_service_type` | string or null | No       | `PHYSICAL_GOODS`, `ONLINE_SERVICES`, `OFFLINE_SERVICES`, or `null` | Updated default representment service type. If you do not use ChargebackStop representment/disputes workflows, omit this field. |

{% hint style="info" %}

* Merchant `type` changes are blocked when related integrations, enrolments, or data providers exist.
* Other fields can still be updated when type changes are blocked.
* If you do not use ChargebackStop representment/disputes workflows, skip `default_representment_service_type`.
  {% endhint %}

### Example request

```bash
curl -X PATCH "https://api.chargebackstop.com/v1/merchants/mrch_abc123" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Stripe Europe",
    "external_id": "acct_eu_010",
    "default_representment_service_type": "ONLINE_SERVICES"
  }'
```

### Example response

```json
{
  "id": "mrch_abc123",
  "organisation_id": "org_xyz456",
  "name": "Acme Stripe Europe",
  "type": "STRIPE",
  "external_id": "acct_eu_010",
  "default_representment_service_type": "ONLINE_SERVICES",
  "created_at": "2026-02-10T09:30:00Z",
  "updated_at": "2026-02-16T12:10:00Z",
  "links": [
    {
      "rel": "self",
      "uri": "/v1/merchants/mrch_abc123"
    }
  ]
}
```

***

## DELETE /v1/merchants/{merchant\_id} - Delete merchant

Delete one accessible merchant.

**API level:** Partner-level only\
**Authentication:** `merchants:write`

### URL parameters

| Parameter     | Type   | Description           |
| ------------- | ------ | --------------------- |
| `merchant_id` | string | Merchant ID to delete |

{% hint style="info" %}
Deletion is blocked when the merchant has related enrolments or alerts.
{% endhint %}

### Example request

```bash
curl -X DELETE "https://api.chargebackstop.com/v1/merchants/mrch_abc123" \
  -H "Authorization: Bearer <api_key>"
```

### Example response

`204 No Content`

***

## Common error responses

### 401 Unauthorised

Returned for invalid or expired API keys, and for organisation-level keys calling this partner-only API.

```json
{
  "errors": [
    {
      "code": "UNAUTHORISED",
      "message": "Unauthorised"
    }
  ]
}
```

### 403 Forbidden (missing ability)

Returned when the API key is valid but missing required ability (`merchants:read` or `merchants:write`).

```json
{
  "errors": [
    {
      "code": "FORBIDDEN",
      "message": "Forbidden"
    }
  ]
}
```

### 404 Not found

Returned when the merchant is not found.

```json
{
  "errors": [
    {
      "code": "NOT_FOUND",
      "message": "Not found"
    }
  ]
}
```

### 422 Unprocessable Entity (merchant type change restricted)

```json
{
  "errors": [
    {
      "code": "MERCHANT_TYPE_CHANGE_RESTRICTED",
      "message": "Cannot change merchant type when integrations are associated with this merchant."
    }
  ]
}
```

### 422 Unprocessable Entity (merchant deletion restricted)

```json
{
  "errors": [
    {
      "code": "MERCHANT_DELETION_RESTRICTED",
      "message": "Merchant cannot be deleted because it has active enrollments. Please contact support if you need to delete this merchant."
    }
  ]
}
```

Other business validation codes you may receive:

| Code                                 | Meaning                                             |
| ------------------------------------ | --------------------------------------------------- |
| `INVALID_MERCHANT_TYPE`              | `type` is not a valid merchant type                 |
| `INVALID_REPRESENTMENT_SERVICE_TYPE` | `default_representment_service_type` is not valid   |
| `MERCHANT_TYPE_CHANGE_RESTRICTED`    | Requested type change is blocked by related records |
| `MERCHANT_DELETION_RESTRICTED`       | Merchant cannot be deleted due to related records   |


# Memberships

Manage organisation memberships under your partner account.

This endpoint set is **partner-level only**. Organisation API keys cannot access these endpoints.

**Base URL:** `https://api.chargebackstop.com/v1/memberships/`\
**Authentication:** Bearer token via API key.

Required abilities:

* `memberships:read` for GET endpoints
* `memberships:write` for POST, PATCH, and DELETE endpoints

Access scope model:

* Admin partner-group keys can access memberships across all organisations belonging to their partner.
* Non-admin partner-group keys can access memberships only in organisations explicitly assigned to their group.
* Organisation-level keys receive `401 Unauthorised` on all endpoints in this API.

***

## GET /v1/memberships - List memberships

Returns memberships accessible to the authenticated partner-group API key.

**API level:** Partner-level only\
**Authentication:** `memberships:read`

### Query parameters

| Parameter         | Type    | Description                     |
| ----------------- | ------- | ------------------------------- |
| `organisation_id` | string  | Filter by exact organisation ID |
| `limit`           | integer | Number of results per page      |
| `offset`          | integer | Number of results to skip       |

{% hint style="info" %}
Results are ordered by `created_at` descending.
{% endhint %}

### Example request

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

### Example response

```json
{
  "items": [
    {
      "id": "mem_abc123",
      "organisation_id": "org_xyz456",
      "user_id": "usr_aaa111",
      "first_name": "John",
      "last_name": "Doe",
      "email": "john.doe@example.com",
      "role": "ADMIN",
      "is_alert_action_required_email_enabled": true,
      "is_alert_resolved_email_enabled": true,
      "is_alert_dismissed_email_enabled": true,
      "is_evidence_requested_email_enabled": true,
      "created_at": "2026-02-10T09:30:00Z",
      "updated_at": "2026-02-10T09:30:00Z",
      "links": [
        {
          "rel": "self",
          "uri": "/v1/memberships/mem_abc123"
        }
      ]
    },
    {
      "id": "mem_def456",
      "organisation_id": "org_xyz456",
      "user_id": "usr_bbb222",
      "first_name": "Jane",
      "last_name": "Smith",
      "email": "jane.smith@example.com",
      "role": "STANDARD",
      "is_alert_action_required_email_enabled": true,
      "is_alert_resolved_email_enabled": false,
      "is_alert_dismissed_email_enabled": false,
      "is_evidence_requested_email_enabled": true,
      "created_at": "2026-02-08T15:12:44Z",
      "updated_at": "2026-02-08T15:12:44Z",
      "links": [
        {
          "rel": "self",
          "uri": "/v1/memberships/mem_def456"
        }
      ]
    }
  ],
  "count": 2
}
```

***

## POST /v1/memberships - Create membership

Create a new membership for an accessible organisation.

**API level:** Partner-level only\
**Authentication:** `memberships:write`

### Request body

| Field                                    | Type    | Required | Validation                    | Description                           |
| ---------------------------------------- | ------- | -------- | ----------------------------- | ------------------------------------- |
| `organisation_id`                        | string  | Yes      | Must be accessible by API key | Organisation that owns the membership |
| `first_name`                             | string  | Yes      | Min length 1                  | User first name                       |
| `last_name`                              | string  | Yes      | Min length 1                  | User last name                        |
| `email`                                  | string  | Yes      | Valid email format            | User email                            |
| `role`                                   | string  | Yes      | `ADMIN` or `STANDARD`         | Membership role                       |
| `is_alert_action_required_email_enabled` | boolean | No       | Defaults to `true`            | Alert action-required email setting   |
| `is_alert_resolved_email_enabled`        | boolean | No       | Defaults to `true`            | Alert resolved email setting          |
| `is_alert_dismissed_email_enabled`       | boolean | No       | Defaults to `true`            | Alert dismissed email setting         |
| `is_evidence_requested_email_enabled`    | boolean | No       | Defaults to `true`            | Evidence requested email setting      |

{% hint style="info" %}

* If the user is already a member of the organisation, the API returns `422` with `MEMBERSHIP_ALREADY_EXISTS`.
* Notification setting values provided in the request are applied as sent.
  {% endhint %}

### Example request

```bash
curl -X POST "https://api.chargebackstop.com/v1/memberships/" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "organisation_id": "org_xyz456",
    "first_name": "Alice",
    "last_name": "Johnson",
    "email": "alice.johnson@example.com",
    "role": "ADMIN",
    "is_alert_action_required_email_enabled": true,
    "is_alert_resolved_email_enabled": false,
    "is_alert_dismissed_email_enabled": true,
    "is_evidence_requested_email_enabled": true
  }'
```

### Example response

```json
{
  "id": "mem_new123",
  "organisation_id": "org_xyz456",
  "user_id": "usr_new123",
  "first_name": "Alice",
  "last_name": "Johnson",
  "email": "alice.johnson@example.com",
  "role": "ADMIN",
  "is_alert_action_required_email_enabled": true,
  "is_alert_resolved_email_enabled": false,
  "is_alert_dismissed_email_enabled": true,
  "is_evidence_requested_email_enabled": true,
  "created_at": "2026-02-16T12:00:00Z",
  "updated_at": "2026-02-16T12:00:00Z",
  "links": [
    {
      "rel": "self",
      "uri": "/v1/memberships/mem_new123"
    }
  ]
}
```

***

## GET /v1/memberships/{membership\_id} - Get membership by ID

Retrieve one accessible membership by ID.

**API level:** Partner-level only\
**Authentication:** `memberships:read`

### URL parameters

| Parameter       | Type   | Description   |
| --------------- | ------ | ------------- |
| `membership_id` | string | Membership ID |

### Example request

```bash
curl -X GET "https://api.chargebackstop.com/v1/memberships/mem_abc123" \
  -H "Authorization: Bearer <api_key>"
```

### Example response

```json
{
  "id": "mem_abc123",
  "organisation_id": "org_xyz456",
  "user_id": "usr_aaa111",
  "first_name": "John",
  "last_name": "Doe",
  "email": "john.doe@example.com",
  "role": "ADMIN",
  "is_alert_action_required_email_enabled": true,
  "is_alert_resolved_email_enabled": true,
  "is_alert_dismissed_email_enabled": true,
  "is_evidence_requested_email_enabled": true,
  "created_at": "2026-02-10T09:30:00Z",
  "updated_at": "2026-02-16T09:05:00Z",
  "links": [
    {
      "rel": "self",
      "uri": "/v1/memberships/mem_abc123"
    }
  ]
}
```

***

## PATCH /v1/memberships/{membership\_id} - Update membership

Update one accessible membership.

**API level:** Partner-level only\
**Authentication:** `memberships:write`

### URL parameters

| Parameter       | Type   | Description             |
| --------------- | ------ | ----------------------- |
| `membership_id` | string | Membership ID to update |

### Request body

All fields are optional.

| Field                                    | Type    | Required | Validation            | Description                                 |
| ---------------------------------------- | ------- | -------- | --------------------- | ------------------------------------------- |
| `role`                                   | string  | No       | `ADMIN` or `STANDARD` | Updated membership role                     |
| `is_alert_action_required_email_enabled` | boolean | No       | Optional              | Updated alert action-required email setting |
| `is_alert_resolved_email_enabled`        | boolean | No       | Optional              | Updated alert resolved email setting        |
| `is_alert_dismissed_email_enabled`       | boolean | No       | Optional              | Updated alert dismissed email setting       |
| `is_evidence_requested_email_enabled`    | boolean | No       | Optional              | Updated evidence requested email setting    |

{% hint style="info" %}

* Only fields included in the request are updated.
* If no fields are provided, the API returns the existing membership unchanged.
  {% endhint %}

### Example request

```bash
curl -X PATCH "https://api.chargebackstop.com/v1/memberships/mem_abc123" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "role": "STANDARD",
    "is_alert_resolved_email_enabled": false,
    "is_alert_dismissed_email_enabled": false
  }'
```

### Example response

```json
{
  "id": "mem_abc123",
  "organisation_id": "org_xyz456",
  "user_id": "usr_aaa111",
  "first_name": "John",
  "last_name": "Doe",
  "email": "john.doe@example.com",
  "role": "STANDARD",
  "is_alert_action_required_email_enabled": true,
  "is_alert_resolved_email_enabled": false,
  "is_alert_dismissed_email_enabled": false,
  "is_evidence_requested_email_enabled": true,
  "created_at": "2026-02-10T09:30:00Z",
  "updated_at": "2026-02-16T12:10:00Z",
  "links": [
    {
      "rel": "self",
      "uri": "/v1/memberships/mem_abc123"
    }
  ]
}
```

***

## DELETE /v1/memberships/{membership\_id} - Delete membership

Delete one accessible membership.

**API level:** Partner-level only\
**Authentication:** `memberships:write`

### URL parameters

| Parameter       | Type   | Description             |
| --------------- | ------ | ----------------------- |
| `membership_id` | string | Membership ID to delete |

### Example request

```bash
curl -X DELETE "https://api.chargebackstop.com/v1/memberships/mem_abc123" \
  -H "Authorization: Bearer <api_key>"
```

### Example response

204 No Content

***

## Common error responses

<details>

<summary>400 Invalid request</summary>

Returned when a membership cannot be created for the target user (for example, a staff user).

```json
{
  "errors": [
    {
      "code": "INVALID_REQUEST",
      "message": "Invalid request"
    }
  ]
}
```

</details>

<details>

<summary>401 Unauthorised</summary>

Returned for invalid or expired API keys, and for organisation-level keys calling this partner-only API.

```json
{
  "errors": [
    {
      "code": "UNAUTHORISED",
      "message": "Unauthorised"
    }
  ]
}
```

</details>

<details>

<summary>403 Forbidden (missing ability)</summary>

Returned when the API key is valid but missing required ability (`memberships:read` or `memberships:write`).

```json
{
  "errors": [
    {
      "code": "FORBIDDEN",
      "message": "Forbidden"
    }
  ]
}
```

</details>

<details>

<summary>404 Not found</summary>

Returned when the membership is not found or outside the key's accessible scope.

```json
{
  "errors": [
    {
      "code": "NOT_FOUND",
      "message": "Not found"
    }
  ]
}
```

</details>

<details>

<summary>422 Unprocessable Entity (membership already exists)</summary>

```json
{
  "errors": [
    {
      "code": "MEMBERSHIP_ALREADY_EXISTS",
      "message": "A user with this email is already a member of this organisation"
    }
  ]
}
```

</details>

<details>

<summary>422 Unprocessable Entity (membership deletion forbidden)</summary>

```json
{
  "errors": [
    {
      "code": "MEMBERSHIP_DELETION_FORBIDDEN",
      "message": "This membership cannot be deleted"
    }
  ]
}
```

</details>

Other business validation codes you may receive:

| Code                            | Meaning                                                    |
| ------------------------------- | ---------------------------------------------------------- |
| `MEMBERSHIP_ALREADY_EXISTS`     | A membership for this user and organisation already exists |
| `MEMBERSHIP_DELETION_FORBIDDEN` | Membership cannot be deleted due to business rules         |

Schema validation errors are also returned as `422` with `VALIDATION_*` codes.


# Organisations

Manage organisations under your partner account.

This endpoint set is **partner-level only**. Organisation API keys cannot access these endpoints.

**Base URL:** `https://api.chargebackstop.com/v1/organisations/`\
**Authentication:** Bearer token via API key.

Required abilities:

* `organisations:read` for GET endpoints
* `organisations:write` for POST, PATCH, and DELETE endpoints

Access scope model:

* Admin partner-group keys can access **all** organisations belonging to their partner.
* Non-admin partner-group keys can access only organisations explicitly assigned to their group.
* Organisation-level keys receive `401 Unauthorised` on all endpoints in this API.

***

## GET /v1/organisations - List organisations

Returns organisations accessible to the authenticated partner-group API key.

Because organisation mode must match partner mode on create, results for a given partner key are expected to be single-mode (`LIVE` or `TEST`).

**API level:** Partner-level only\
**Authentication:** `organisations:read`

### Query parameters

| Parameter | Type    | Description                |
| --------- | ------- | -------------------------- |
| `limit`   | integer | Number of results per page |
| `offset`  | integer | Number of results to skip  |

### Example request

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

### Example response

```json
{
  "items": [
    {
      "id": "org_abc123",
      "name": "Acme Payments",
      "mode": "LIVE",
      "created_at": "2026-01-10T10:45:12Z",
      "updated_at": "2026-01-10T10:45:12Z",
      "links": [
        {
          "rel": "self",
          "uri": "/v1/organisations/org_abc123"
        }
      ]
    },
    {
      "id": "org_def456",
      "name": "Acme Payments EU",
      "mode": "LIVE",
      "created_at": "2026-01-11T09:12:00Z",
      "updated_at": "2026-01-11T09:12:00Z",
      "links": [
        {
          "rel": "self",
          "uri": "/v1/organisations/org_def456"
        }
      ]
    }
  ],
  "count": 2
}
```

***

## POST /v1/organisations - Create organisation

Create a new organisation for the authenticated partner.

**API level:** Partner-level only\
**Authentication:** `organisations:write`

### Request body

| Field  | Type   | Required | Validation   | Description              |
| ------ | ------ | -------- | ------------ | ------------------------ |
| `name` | string | Yes      | Min length 1 | Public organisation name |

{% hint style="info" %}
Important behavior:

* Organisation mode always follows the partner's mode.
* If resulting mode is `TEST` and `name` does not already start with `[TEST]`, the API prefixes the name with `[TEST]` automatically.
* If name already starts with `[TEST]`, no duplicate prefix is added.
  {% endhint %}

### Example request

```bash
curl -X POST "https://api.chargebackstop.com/v1/organisations/" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "New Organisation"
  }'
```

### Example response

```json
{
  "id": "org_new123",
  "name": "New Organisation",
  "mode": "LIVE",
  "created_at": "2026-02-16T12:00:00Z",
  "updated_at": "2026-02-16T12:00:00Z",
  "links": [
    {
      "rel": "self",
      "uri": "/v1/organisations/org_new123"
    }
  ]
}
```

***

## GET /v1/organisations/{organisation\_id} - Get organisation by ID

Retrieve one accessible organisation by ID.

**API level:** Partner-level only\
**Authentication:** `organisations:read`

### URL parameters

| Parameter         | Type   | Description     |
| ----------------- | ------ | --------------- |
| `organisation_id` | string | Organisation ID |

### Example request

```bash
curl -X GET "https://api.chargebackstop.com/v1/organisations/org_abc123" \
  -H "Authorization: Bearer <api_key>"
```

### Example response

```json
{
  "id": "org_abc123",
  "name": "Acme Payments",
  "mode": "LIVE",
  "created_at": "2026-01-10T10:45:12Z",
  "updated_at": "2026-01-15T08:00:00Z",
  "links": [
    {
      "rel": "self",
      "uri": "/v1/organisations/org_abc123"
    }
  ]
}
```

<details>

<summary>Error responses</summary>

**404 Not Found** - Organisation does not exist or is not accessible to this key:

```json
{
  "errors": [
    {
      "code": "NOT_FOUND",
      "message": "Not found"
    }
  ]
}
```

</details>

***

## PATCH /v1/organisations/{organisation\_id} - Update organisation

Update organisation details.

**API level:** Partner-level only\
**Authentication:** `organisations:write`

### URL parameters

| Parameter         | Type   | Description               |
| ----------------- | ------ | ------------------------- |
| `organisation_id` | string | Organisation ID to update |

### Request body

| Field  | Type   | Required | Validation   | Description                      |
| ------ | ------ | -------- | ------------ | -------------------------------- |
| `name` | string | Yes      | Min length 1 | Updated public organisation name |

{% hint style="info" %}
Important behavior:

* Only `name` is updatable via this endpoint.
* `mode` is not updatable here.
* Attempting to update an inaccessible organisation returns `404`.
  {% endhint %}

### Example request

```bash
curl -X PATCH "https://api.chargebackstop.com/v1/organisations/org_abc123" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Payments UK"
  }'
```

### Example response

```json
{
  "id": "org_abc123",
  "name": "Acme Payments UK",
  "mode": "LIVE",
  "created_at": "2026-01-10T10:45:12Z",
  "updated_at": "2026-02-16T12:10:00Z",
  "links": [
    {
      "rel": "self",
      "uri": "/v1/organisations/org_abc123"
    }
  ]
}
```

***

## DELETE /v1/organisations/{organisation\_id} - Delete organisation

Delete an organisation if it has no related dependent records.

**API level:** Partner-level only\
**Authentication:** `organisations:write`

### URL parameters

| Parameter         | Type   | Description               |
| ----------------- | ------ | ------------------------- |
| `organisation_id` | string | Organisation ID to delete |

{% hint style="warning" %}
Important behavior:

* Deletion is blocked if the organisation still has related data. In those cases, API returns `422`.

Blocking checks include:

* Merchants
* Network alert enrolments
* Integrations
* Data providers
* Alerts
  {% endhint %}

### Example request

```bash
curl -X DELETE "https://api.chargebackstop.com/v1/organisations/org_abc123" \
  -H "Authorization: Bearer <api_key>"
```

### Example response

`204 No Content`

***

## Common error responses

### 400 Invalid request

```json
{
  "errors": [
    {
      "code": "INVALID_REQUEST",
      "message": "Invalid request"
    }
  ]
}
```

### 401 Unauthorised

Returned for invalid/expired API keys, and for organisation-level keys calling this partner-only API.

```json
{
  "errors": [
    {
      "code": "UNAUTHORISED",
      "message": "Unauthorised"
    }
  ]
}
```

### 403 Forbidden (missing ability)

Returned when the API key is valid but missing required ability (`organisations:read` or `organisations:write`).

```json
{
  "errors": [
    {
      "code": "FORBIDDEN",
      "message": "Forbidden"
    }
  ]
}
```

### 404 Not found

Returned when the organisation does not exist or is outside the key's accessible scope.

```json
{
  "errors": [
    {
      "code": "NOT_FOUND",
      "message": "Not found"
    }
  ]
}
```

### 422 Unprocessable Entity

Used for business-rule and schema validation errors.

Example: business validation (delete blocked):

```json
{
  "errors": [
    {
      "code": "HAS_MERCHANTS",
      "message": "The organisation has merchants. Please contact customer support to delete this organisation."
    }
  ]
}
```

Other business validation codes you may receive:

| Code                 | Meaning                                                  |
| -------------------- | -------------------------------------------------------- |
| `HAS_MERCHANTS`      | Organisation has merchants and cannot be deleted         |
| `HAS_ENROLMENTS`     | Organisation has active enrolments and cannot be deleted |
| `HAS_INTEGRATIONS`   | Organisation has integrations and cannot be deleted      |
| `HAS_DATA_PROVIDERS` | Organisation has data providers and cannot be deleted    |
| `HAS_ALERTS`         | Organisation has alerts and cannot be deleted            |

Schema validation errors are also returned as `422` with `VALIDATION_*` codes (for example, invalid field values or empty `name`).


# Alerts

List, retrieve, and action network alerts for accessible organisations.

{% hint style="info" %}
**Verifi RDR alerts are resolved automatically.** Verifi RDR alerts arrive with status `RESOLVED` or `INVALID` and do not require actioning. The `ACTION_REQUIRED` status and the PATCH endpoint for actioning alerts apply to Ethoca alerts only.
{% endhint %}

{% hint style="danger" %}
**Ethoca alerts in `ACTION_REQUIRED` status must be actioned.** Refunding the transaction alone is not enough to prevent a chargeback. You must also action the alert using the PATCH endpoint so that Ethoca is notified and can stop the dispute. Unactioned alerts provide no value — the chargeback will proceed regardless of whether you refunded the customer. If you have resolution rules configured, matching alerts are resolved automatically. In rare cases, an alert may still appear as `ACTION_REQUIRED` if the system cannot match the alert to a transaction or cannot process the refund.
{% endhint %}

**Base URL:** `https://api.chargebackstop.com/v1/alerts/`

**Authentication:** Bearer token via API key.

Required abilities:

* `alerts:read` for GET endpoints
* `alerts:write` for PATCH endpoints

**Access scope model:**

* Organisation-level keys can access alerts for their own organisation only.
* Admin partner-group keys can access alerts across all organisations for their partner.
* Non-admin partner-group keys can access alerts only for organisations assigned to their group.

{% hint style="warning" %}
**Use `merchant_ids`, not `merchant_id`.** An alert's enrolment can have more than one merchant account attached. `merchant_ids` lists all of them, ordered by when they were added. The singular `merchant_id` is a legacy, deprecated field: it always contains only the earliest-added merchant, so when you filter by `?merchant_id=`, the `merchant_id` in the response can be a different merchant than the one you filtered by. It is safer to read `merchant_ids`; its first entry always equals `merchant_id`.
{% endhint %}

***

## GET /v1/alerts - list alerts

Returns a paginated list of alerts accessible to the authenticated key.

**API level:** Organisation-level and partner-level\
**Authentication:** `alerts:read`

### Query parameters

| Parameter               | Type     | Description                                                                                                              |
| ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------ |
| `status`                | string   | Filter by alert status: `ACTION_REQUIRED` (Ethoca only), `RESOLVED`, `INVALID`                                           |
| `organisation_id`       | string   | Filter by organisation ID                                                                                                |
| `merchant_id`           | string   | Filter by merchant ID. Matches alerts whose enrolment includes this merchant, in any position of `merchant_ids`          |
| `alert_received_at_gte` | datetime | Include alerts received at or after this timestamp (ISO 8601, inclusive)                                                 |
| `alert_received_at_lte` | datetime | Include alerts received at or before this timestamp (ISO 8601, inclusive)                                                |
| `sort`                  | string   | Sort field: `-alert_received_at` (default), `alert_received_at`, `-action_required_deadline`, `action_required_deadline` |
| `limit`                 | integer  | Number of results per page                                                                                               |
| `offset`                | integer  | Number of results to skip                                                                                                |

### Response codes

| Code | Description                                     |
| ---- | ----------------------------------------------- |
| 200  | Success                                         |
| 400  | Invalid request                                 |
| 401  | Unauthorised                                    |
| 403  | Forbidden — API key lacks `alerts:read` ability |
| 422  | Validation error (e.g. invalid date range)      |

### Important behaviour

* If both `alert_received_at_gte` and `alert_received_at_lte` are sent, `alert_received_at_gte` must be less than or equal to `alert_received_at_lte`.
* Default sort order is newest alert received first (`-alert_received_at`).

<details>

<summary>Example 1 request</summary>

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

</details>

<details>

<summary>Example 1 response</summary>

```json
{
  "items": [
    {
      "id": "netalrt_abc123",
      "alert_network_id": "1234567890",
      "organisation_id": "org_xyz456",
      "merchant_id": "mrch_abc123",
      "merchant_ids": ["mrch_abc123", "mrch_def789"],
      "enrolment_id": "enrl_def456",
      "enrolment_type": "ETHOCA_ALERT",
      "status": "ACTION_REQUIRED",
      "chargeback_reason_code": "10.4",
      "transaction_amount_in_cents": 4999,
      "transaction_currency_code": "USD",
      "transaction_authorised_at": "2026-02-10T10:30:00Z",
      "action_required_deadline": "2026-02-17T10:30:00Z",
      "transaction_authorisation_code": "ABC123",
      "transaction_acquirer_reference_number": "74027012345678901234567",
      "transaction_statement_descriptor": "EXAMPLE STORE",
      "transaction_card_bin": "424242",
      "transaction_card_last4": "4242",
      "transaction_card_scheme": "VISA",
      "transaction_card_issuer": "Example Bank",
      "transaction_refund_outcome": null,
      "transaction_network_id": null,
      "subscription_cancel_outcome": null,
      "note": "Customer requested immediate review",
      "created_at": "2026-02-11T10:30:00Z",
      "updated_at": "2026-02-11T10:30:00Z",
      "integration_id": null,
      "integration_transaction_id": null,
      "links": [
        {
          "rel": "self",
          "uri": "/v1/alerts/netalrt_abc123"
        }
      ]
    }
  ],
  "count": 1
}
```

</details>

***

## GET /v1/alerts/{alert\_id} - get alert by ID

Returns one alert if it is visible to the authenticated key.

**API level:** Organisation-level and partner-level\
**Authentication:** `alerts:read`

### URL parameters

| Parameter  | Type   | Description |
| ---------- | ------ | ----------- |
| `alert_id` | string | Alert ID    |

### Response codes

| Code | Description                                     |
| ---- | ----------------------------------------------- |
| 200  | Success                                         |
| 400  | Invalid request                                 |
| 401  | Unauthorised                                    |
| 403  | Forbidden — API key lacks `alerts:read` ability |
| 404  | Alert not found                                 |

### Important behaviour

* The same eligibility rules as list alerts apply.
* Non-existent alerts return `404 Not found`.

<details>

<summary>Example 2 request</summary>

```bash
curl -X GET "https://api.chargebackstop.com/v1/alerts/netalrt_abc123" \
  -H "Authorization: Bearer <api_key>"
```

</details>

<details>

<summary>Example 2 response</summary>

```json
{
  "id": "netalrt_abc123",
  "alert_network_id": "1234567890",
  "organisation_id": "org_xyz456",
  "merchant_id": "mrch_abc123",
  "merchant_ids": ["mrch_abc123", "mrch_def789"],
  "enrolment_id": "enrl_def456",
  "enrolment_type": "ETHOCA_ALERT",
  "status": "ACTION_REQUIRED",
  "chargeback_reason_code": "10.4",
  "transaction_amount_in_cents": 4999,
  "transaction_currency_code": "USD",
  "transaction_authorised_at": "2026-02-10T10:30:00Z",
  "action_required_deadline": "2026-02-17T10:30:00Z",
  "transaction_authorisation_code": "ABC123",
  "transaction_acquirer_reference_number": "74027012345678901234567",
  "transaction_statement_descriptor": "EXAMPLE STORE",
  "transaction_card_bin": "424242",
  "transaction_card_last4": "4242",
  "transaction_card_scheme": "VISA",
  "transaction_card_issuer": "Example Bank",
  "transaction_refund_outcome": null,
  "transaction_network_id": null,
  "subscription_cancel_outcome": null,
  "note": "Customer requested immediate review",
  "created_at": "2026-02-11T10:30:00Z",
  "updated_at": "2026-02-11T10:30:00Z",
  "integration_id": null,
  "integration_transaction_id": null,
  "links": [
    {
      "rel": "self",
      "uri": "/v1/alerts/netalrt_abc123"
    }
  ]
}
```

</details>

***

## PATCH /v1/alerts/{alert\_id} - update alert

Updates the alert note and/or actions the alert.

**API level:** Organisation-level and partner-level\
**Authentication:** `alerts:write`

### URL parameters

| Parameter  | Type   | Description        |
| ---------- | ------ | ------------------ |
| `alert_id` | string | Alert ID to update |

### Request body

All fields are optional.

| Field    | Type   | Required | Description                                                                |
| -------- | ------ | -------- | -------------------------------------------------------------------------- |
| `action` | string | No       | Action to apply: `REFUND`, `CANCEL`, `REFUND_AND_CANCEL`, `ACCEPT_DISPUTE` |
| `note`   | string | No       | Customer-facing note saved on the alert                                    |

### Response codes

| Code | Description                                                    |
| ---- | -------------------------------------------------------------- |
| 200  | Success                                                        |
| 400  | Invalid request                                                |
| 401  | Unauthorised                                                   |
| 403  | Forbidden — API key lacks `alerts:write` ability               |
| 404  | Alert not found                                                |
| 422  | Validation error (e.g. invalid action, alert already resolved) |

### Important behaviour

* This endpoint applies to Ethoca alerts only. Verifi RDR alerts are resolved automatically and cannot be actioned.
* If `note` is provided, it is saved on the alert.
* If `action` is omitted, only the note is updated.
* Once an alert is in `RESOLVED`, it cannot be actioned again.
* Action values map to available underlying actions at runtime. For detailed behaviour, see [Actioning alerts](/developer/partner-integration-guide/api-integration-guide):
  * `REFUND` prioritises refund actions.
  * `CANCEL` prioritises cancel/dismiss actions.
  * `REFUND_AND_CANCEL` prioritises combined refund-and-cancel, then refund-only if combined action is unavailable.
  * `ACCEPT_DISPUTE` maps to dismissing the alert.

<details>

<summary>Example 3 request</summary>

```bash
curl -X PATCH "https://api.chargebackstop.com/v1/alerts/netalrt_abc123" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "ACCEPT_DISPUTE",
    "note": "We decided to accept this dispute"
  }'
```

</details>

<details>

<summary>Example 3 response</summary>

```json
{
  "id": "netalrt_abc123",
  "alert_network_id": "1234567890",
  "organisation_id": "org_xyz456",
  "merchant_id": "mrch_abc123",
  "merchant_ids": ["mrch_abc123", "mrch_def789"],
  "enrolment_id": "enrl_def456",
  "enrolment_type": "ETHOCA_ALERT",
  "status": "RESOLVED",
  "chargeback_reason_code": "10.4",
  "transaction_amount_in_cents": 4999,
  "transaction_currency_code": "USD",
  "transaction_authorised_at": "2026-02-10T10:30:00Z",
  "action_required_deadline": "2026-02-17T10:30:00Z",
  "transaction_authorisation_code": "ABC123",
  "transaction_acquirer_reference_number": "74027012345678901234567",
  "transaction_statement_descriptor": "EXAMPLE STORE",
  "transaction_card_bin": "424242",
  "transaction_card_last4": "4242",
  "transaction_card_scheme": "VISA",
  "transaction_card_issuer": "Example Bank",
  "transaction_refund_outcome": "NOT_REFUNDED",
  "transaction_network_id": null,
  "subscription_cancel_outcome": null,
  "note": "We decided to accept this dispute",
  "created_at": "2026-02-11T10:30:00Z",
  "updated_at": "2026-02-11T10:32:00Z",
  "integration_id": null,
  "integration_transaction_id": null,
  "links": [
    {
      "rel": "self",
      "uri": "/v1/alerts/netalrt_abc123"
    }
  ]
}
```

</details>

***

## Common error responses

Error responses use this shape:

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

<details>

<summary>Example 4: 401 Unauthorised</summary>

Returned for invalid, expired, or missing API keys.

```json
{
  "errors": [
    {
      "code": "UNAUTHORISED",
      "message": "Unauthorised"
    }
  ]
}
```

</details>

<details>

<summary>Example 5: 403 Forbidden</summary>

Returned when the API key is valid but missing required ability.

```json
{
  "errors": [
    {
      "code": "FORBIDDEN",
      "message": "Forbidden"
    }
  ]
}
```

</details>

<details>

<summary>Example 6: 404 Not found</summary>

Returned when the alert is not found or not accessible.

```json
{
  "errors": [
    {
      "code": "NOT_FOUND",
      "message": "Not found"
    }
  ]
}
```

</details>

<details>

<summary>Example 7: 422 Unprocessable Entity (invalid date range)</summary>

Returned when `alert_received_at_gte` is greater than `alert_received_at_lte`.

```json
{
  "errors": [
    {
      "code": "INVALID_DATE_RANGE",
      "message": "alert_received_at_gte cannot be greater than alert_received_at_lte"
    }
  ]
}
```

</details>

<details>

<summary>Example 8: 422 Unprocessable Entity (invalid enum value)</summary>

Returned when `action` is not one of the supported values.

```json
{
  "errors": [
    {
      "code": "VALIDATION_ENUM",
      "message": "Validation error at ('body', 'update', 'action'). Input should be 'REFUND', 'CANCEL', 'REFUND_AND_CANCEL' or 'ACCEPT_DISPUTE'."
    }
  ]
}
```

</details>

<details>

<summary>Example 9: 422 Unprocessable Entity (resolved alert cannot be actioned)</summary>

```json
{
  "errors": [
    {
      "code": "INVALID_ACTION",
      "message": "Failed to update netalrt_abc123 alert. This alert is in RESOLVED status and cannot be actioned."
    }
  ]
}
```

</details>

<details>

<summary>Example 10: 422 Unprocessable Entity (no available action)</summary>

Returned when the requested action cannot be mapped to any available action for the alert.

```json
{
  "errors": [
    {
      "code": "INVALID_ACTION",
      "message": "Failed to update netalrt_abc123 alert. No available action found."
    }
  ]
}
```

</details>

<details>

<summary>Example 11: 422 Unprocessable Entity (action execution failed)</summary>

Returned when the underlying action execution is rejected.

```json
{
  "errors": [
    {
      "code": "ACTION_FAILED",
      "message": "Failed to update netalrt_abc123 alert. The requested action is not allowed."
    }
  ]
}
```

</details>

<details>

<summary>Additional reference: 500 Server error</summary>

Unexpected server-side failures return:

```json
{
  "errors": [
    {
      "code": "SERVER_ERROR",
      "message": "Server error"
    }
  ]
}
```

</details>


# Enrolments

Create, list, retrieve, update, and cancel network alert enrolments for one or more merchants.

**Versioning notice (important):**

* This is the first `v2` API documentation for enrolments.
* `/v1/enrolments` is deprecated and should not be used for new integrations.
* Use `/v2/enrolments` with `enrolments_v2:*` abilities.
* New API keys are provisioned with `enrolments_v2:*` abilities for enrolments access.
* Some legacy keys created before `/v1/enrolments` was deprecated may include both `enrolments:*` and `enrolments_v2:*` abilities.

**Base URL:** `https://api.chargebackstop.com/v2/enrolments/`

**Authentication:** Bearer token via API key.

Required abilities:

* `enrolments_v2:read` for GET endpoints
* `enrolments_v2:write` for POST, PATCH, and DELETE endpoints

**Access scope model:**

* This API is partner-level only.
* Admin partner-group keys can access enrolments across organisations available to their partner.
* Non-admin partner-group keys can access enrolments only for organisations assigned to their group.
* Organisation-level keys receive `401 Unauthorised` on all endpoints.

***

## Migration from `/v1/enrolments` to `/v2/enrolments`

Use this section to migrate existing enrolments integrations from v1 to v2.

### At a glance

| Area                                    | v1                                    | v2                                                                                                                                                                                 | Action                         |
| --------------------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ |
| Base path                               | `/v1/enrolments`                      | `/v2/enrolments`                                                                                                                                                                   | Update request URLs            |
| Create request merchant field           | `merchant_id` (string)                | `merchant_ids` (array of strings)                                                                                                                                                  | Update payload shape           |
| Create response merchant field          | `merchant_id`                         | `merchant_ids`                                                                                                                                                                     | Update response parsing        |
| List/get/delete response merchant field | `merchant_id`                         | `merchant_ids`                                                                                                                                                                     | Update response parsing        |
| New create validation                   | Not applicable                        | Rejects duplicate merchant IDs and mixed-organisation merchant sets                                                                                                                | Handle new `422` codes         |
| Merchant reference                      | `merchant_id` identifies one merchant | `merchant_ids` supports one or more merchants; missing merchant references return `404 Not found`; mixed-organisation merchant sets return `422 MERCHANTS_DIFFERENT_ORGANISATIONS` | Update merchant error handling |

### Endpoint mapping

All operations map 1:1 to v2 routes:

* `POST /v1/enrolments` -> `POST /v2/enrolments`
* `GET /v1/enrolments` -> `GET /v2/enrolments`
* `GET /v1/enrolments/{enrolment_id}` -> `GET /v2/enrolments/{enrolment_id}`
* `DELETE /v1/enrolments/{enrolment_id}` -> `DELETE /v2/enrolments/{enrolment_id}`

`PATCH /v2/enrolments/{enrolment_id}` is v2 only. It has no v1 equivalent.

### Authentication and abilities changes

* v2 is still partner-level only.
* Organisation-level keys are still not allowed.
* v2 endpoints require `enrolments_v2:read` and `enrolments_v2:write`.
* New API keys are provisioned with v2 enrolments abilities.
* Some legacy keys created before v1 deprecation may include both `enrolments:*` and `enrolments_v2:*`.

### Request and response schema differences

v1 create body (single merchant):

```json
{
  "merchant_id": "mrch_abc123",
  "type": "ETHOCA_ALERT",
  "ethoca_alert": {
    "descriptors": [
      {
        "descriptor": "ECOMM.COM",
        "match_type": "STARTS_WITH"
      }
    ]
  }
}
```

v2 create body (one or more merchants):

```json
{
  "merchant_ids": ["mrch_abc123", "mrch_def456"],
  "type": "ETHOCA_ALERT",
  "ethoca_alert": {
    "descriptors": [
      {
        "descriptor": "ECOMM.COM",
        "match_type": "STARTS_WITH"
      }
    ]
  }
}
```

v1 response field:

```json
{
  "merchant_id": "mrch_abc123"
}
```

v2 response field:

```json
{
  "merchant_ids": ["mrch_abc123", "mrch_def456"]
}
```

Other core fields (`id`, `organisation_id`, `type`, `status`, `ethoca_alert`, `verifi_rdr`, timestamps, `note`) stay the same contract shape, with one addition: v2 responses include `ethoca_alert.descriptors[].status`, which v1 responses do not return.

### Behaviour differences to handle

* `merchant_ids` must not contain duplicates.
* All merchants in one create request must be accessible to the key.
* All merchants in one create request must belong to the same organisation.
* Out-of-scope organisation and merchant references are returned as `404 Not found`.
* Handle additional `422` business codes: `DUPLICATE_MERCHANT_IDS` and `MERCHANTS_DIFFERENT_ORGANISATIONS`.

### What does not change

* `type` values used by this API remain `ETHOCA_ALERT` and `VERIFI_RDR`.
* `GET /v2/enrolments` still supports `merchant_id`, `organisation_id`, `type`, and `status` filters.
* Deleting an enrolment still cancels it, and only `PENDING` enrolments can be cancelled.

### Migration checklist

{% stepper %}
{% step %}

#### Switch routes

Switch all enrolments routes from `/v1/enrolments` to `/v2/enrolments`.
{% endstep %}

{% step %}

#### Update API key abilities

Ensure API keys include `enrolments_v2:read` and `enrolments_v2:write`.
{% endstep %}

{% step %}

#### Update payloads

Update create payload builders from `merchant_id` to `merchant_ids`.
{% endstep %}

{% step %}

#### Update response parsing

Update response parsing from `merchant_id` to `merchant_ids` across create/list/get/delete flows.
{% endstep %}

{% step %}

#### Handle new 422 codes

Add handling for new `422` codes: `DUPLICATE_MERCHANT_IDS` and `MERCHANTS_DIFFERENT_ORGANISATIONS`.
{% endstep %}

{% step %}

#### Re-run tests

Re-run integration tests for enrolment create, list filters, detail, and cancel flows.
{% endstep %}
{% endstepper %}

***

## POST /v2/enrolments - Create enrolment

Create a new enrolment for one or more merchants.

**API level:** Partner-level only\
**Authentication:** `enrolments_v2:write`

### Request body

| Field          | Type           | Required      | Validation                                                                                                     | Description                               |
| -------------- | -------------- | ------------- | -------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| `merchant_ids` | array\[string] | Yes           | At least one ID; duplicates are rejected; all merchants must be accessible and belong to the same organisation | Merchants to associate with the enrolment |
| `type`         | string         | Yes           | `ETHOCA_ALERT` or `VERIFI_RDR`                                                                                 | Enrolment type                            |
| `ethoca_alert` | object         | Conditionally | Required when `type` is `ETHOCA_ALERT`                                                                         | Ethoca Alert configuration                |
| `verifi_rdr`   | object         | Conditionally | Required when `type` is `VERIFI_RDR`                                                                           | Verifi RDR configuration                  |

### Ethoca Alert configuration

| Field                      | Type           | Required | Validation                     | Description                          |
| -------------------------- | -------------- | -------- | ------------------------------ | ------------------------------------ |
| `descriptors`              | array\[object] | Yes      | At least one descriptor        | Descriptor rules for Ethoca matching |
| `descriptors[].descriptor` | string         | Yes      | Minimum length 1               | Descriptor text                      |
| `descriptors[].match_type` | string         | Yes      | `STARTS_WITH` or `EXACT_MATCH` | Descriptor matching mode             |

### Verifi RDR configuration

| Field  | Type           | Required      | Validation                                                    | Description |
| ------ | -------------- | ------------- | ------------------------------------------------------------- | ----------- |
| `bin`  | string         | Conditionally | Provide together with `caid`, or omit both and provide `arns` | Visa BIN    |
| `caid` | string         | Conditionally | Provide together with `bin`, or omit both and provide `arns`  | Visa CAID   |
| `arns` | array\[string] | Conditionally | At least one item when used                                   | Visa ARNs   |

### Important behaviour

* The API rejects mixed-organisation merchant lists with `MERCHANTS_DIFFERENT_ORGANISATIONS`.
* The enrolment is created in `PENDING` status.
* Responses return `merchant_ids` (plural), not `merchant_id`.
* In responses, `ethoca_alert.descriptors[].status` is read-only and set by the platform. Its value is `PENDING`, `ACTIVE`, or `SUPPRESSED`. Do not send it in a create request. This field is v2 only — the v1 enrolments endpoint does not return it.

#### Ethoca descriptor validation

Ethoca validates descriptors asynchronously after the enrolment is created. This can take a few days. If the descriptor is too generic or already enrolled by another merchant globally across Ethoca's network, the enrolment moves to `ACTION_REQUIRED`. When this happens, our support team reaches out to the merchant to agree on an alternative descriptor. This typically takes 1–2 business days. Once resolved, the enrolment is resubmitted. To track enrolment status, poll the GET endpoint every 2–3 hours or subscribe to a webhook to watch for status changes.

Each descriptor carries its own `status` that tracks this lifecycle: `PENDING` while Ethoca is still validating it, `ACTIVE` once it is live, and `SUPPRESSED` when it is no longer matched. A descriptor becoming `ACTIVE` emits an `enrolment.updated` webhook, so you can follow individual descriptors instead of polling.

### Example 1 request

```bash
curl -X POST "https://api.chargebackstop.com/v2/enrolments/" \
  -H "Authorization: Bearer <partner_api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "merchant_ids": ["mrch_abc123", "mrch_def456"],
    "type": "ETHOCA_ALERT",
    "ethoca_alert": {
      "descriptors": [
        {
          "descriptor": "ECOMM.COM",
          "match_type": "STARTS_WITH"
        }
      ]
    }
  }'
```

### Example 1 response

```json
{
  "id": "enrl_abc123",
  "organisation_id": "org_xyz456",
  "merchant_ids": ["mrch_abc123", "mrch_def456"],
  "type": "ETHOCA_ALERT",
  "status": "PENDING",
  "ethoca_alert": {
    "descriptors": [
      {
        "descriptor": "ECOMM.COM",
        "match_type": "STARTS_WITH",
        "status": "PENDING"
      }
    ]
  },
  "created_at": "2026-02-16T12:00:00Z",
  "updated_at": "2026-02-16T12:00:00Z",
  "note": null,
  "links": [
    {
      "rel": "self",
      "uri": "/v2/enrolments/enrl_abc123"
    }
  ]
}
```

### Example 2 request

```bash
curl -X POST "https://api.chargebackstop.com/v2/enrolments/" \
  -H "Authorization: Bearer <partner_api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "merchant_ids": ["mrch_abc123"],
    "type": "VERIFI_RDR",
    "verifi_rdr": {
      "bin": "424242",
      "caid": "ABC1234567"
    }
  }'
```

### Example 2 response

```json
{
  "id": "enrl_def456",
  "organisation_id": "org_xyz456",
  "merchant_ids": ["mrch_abc123"],
  "type": "VERIFI_RDR",
  "status": "PENDING",
  "verifi_rdr": {
    "bin": "424242",
    "caid": "ABC1234567",
    "arns": null
  },
  "created_at": "2026-02-16T12:05:00Z",
  "updated_at": "2026-02-16T12:05:00Z",
  "note": null,
  "links": [
    {
      "rel": "self",
      "uri": "/v2/enrolments/enrl_def456"
    }
  ]
}
```

***

## GET /v2/enrolments - List enrolments

Return enrolments accessible to the authenticated partner-group key.

**API level:** Partner-level only\
**Authentication:** `enrolments_v2:read`

### Query parameters

| Parameter         | Type    | Description                              |
| ----------------- | ------- | ---------------------------------------- |
| `merchant_id`     | string  | Filter by one accessible merchant ID     |
| `organisation_id` | string  | Filter by one accessible organisation ID |
| `type`            | string  | Filter by enrolment type                 |
| `status`          | string  | Filter by enrolment status               |
| `limit`           | integer | Number of results per page               |
| `offset`          | integer | Number of results to skip                |

### Important behaviour

* Filtering by a non-existent organisation returns `404 Not found`.
* Filtering by a non-existent merchant returns `404 Not found`.

### Example 3 request

```bash
curl -X GET "https://api.chargebackstop.com/v2/enrolments/?merchant_id=mrch_abc123&type=ETHOCA_ALERT&status=PENDING&limit=20&offset=0" \
  -H "Authorization: Bearer <partner_api_key>"
```

### Example 3 response

```json
{
  "items": [
    {
      "id": "enrl_abc123",
      "organisation_id": "org_xyz456",
      "merchant_ids": ["mrch_abc123", "mrch_def456"],
      "type": "ETHOCA_ALERT",
      "status": "PENDING",
      "ethoca_alert": {
        "descriptors": [
          {
            "descriptor": "ECOMM.COM",
            "match_type": "STARTS_WITH",
            "status": "PENDING"
          }
        ]
      },
      "created_at": "2026-02-16T12:00:00Z",
      "updated_at": "2026-02-16T12:00:00Z",
      "note": null,
      "links": [
        {
          "rel": "self",
          "uri": "/v2/enrolments/enrl_abc123"
        }
      ]
    }
  ],
  "count": 1
}
```

***

## GET /v2/enrolments/{enrolment\_id} - Get enrolment by ID

Retrieve one accessible enrolment by ID.

**API level:** Partner-level only\
**Authentication:** `enrolments_v2:read`

### URL parameters

| Parameter      | Type   | Description  |
| -------------- | ------ | ------------ |
| `enrolment_id` | string | Enrolment ID |

### Example 4 request

```bash
curl -X GET "https://api.chargebackstop.com/v2/enrolments/enrl_abc123" \
  -H "Authorization: Bearer <partner_api_key>"
```

### Example 4 response

```json
{
  "id": "enrl_abc123",
  "organisation_id": "org_xyz456",
  "merchant_ids": ["mrch_abc123", "mrch_def456"],
  "type": "ETHOCA_ALERT",
  "status": "PENDING",
  "ethoca_alert": {
    "descriptors": [
      {
        "descriptor": "ECOMM.COM",
        "match_type": "STARTS_WITH",
        "status": "PENDING"
      }
    ]
  },
  "created_at": "2026-02-16T12:00:00Z",
  "updated_at": "2026-02-16T12:00:00Z",
  "note": null,
  "links": [
    {
      "rel": "self",
      "uri": "/v2/enrolments/enrl_abc123"
    }
  ]
}
```

***

## PATCH /v2/enrolments/{enrolment\_id} - Append Ethoca descriptors

Append one or more Ethoca Alert descriptors to an existing enrolment. This endpoint never removes or modifies existing descriptors.

**API level:** Partner-level only\
**Authentication:** `enrolments_v2:write`

### URL parameters

| Parameter      | Type   | Description            |
| -------------- | ------ | ---------------------- |
| `enrolment_id` | string | Enrolment ID to update |

### Request body

| Field          | Type   | Required      | Validation                             | Description                        |
| -------------- | ------ | ------------- | -------------------------------------- | ---------------------------------- |
| `ethoca_alert` | object | Conditionally | Required for `ETHOCA_ALERT` enrolments | Ethoca Alert descriptors to append |

### Ethoca Alert configuration

| Field                      | Type           | Required | Validation                     | Description                |
| -------------------------- | -------------- | -------- | ------------------------------ | -------------------------- |
| `descriptors`              | array\[object] | Yes      | At least one descriptor        | Descriptor rules to append |
| `descriptors[].descriptor` | string         | Yes      | Minimum length 1               | Descriptor text            |
| `descriptors[].match_type` | string         | Yes      | `STARTS_WITH` or `EXACT_MATCH` | Descriptor matching mode   |

### Important behaviour

* Currently only `ETHOCA_ALERT` enrolments can be updated. Other types return `422 ENROLMENT_TYPE_NOT_SUPPORTED`.
* Descriptors can be appended when enrolment status is `PENDING`, `IN_PROGRESS`, `ACTION_REQUIRED`, or `ENABLED`. Other statuses return `422 ENROLMENT_STATUS_NOT_APPENDABLE`.
* The enrolment-level `status` does not change.
* New descriptors are created with a read-only `status` of `PENDING`.
* A descriptor with the same `descriptor` and `match_type` as an existing descriptor is rejected with `422 DUPLICATE_DESCRIPTOR`.
* Duplicate `descriptor` and `match_type` pairs in the same request are also rejected with `422 DUPLICATE_DESCRIPTOR`.
* Appending descriptors emits an `enrolment.updated` webhook with `previous_attributes.ethoca_alert.descriptors`.

### Example 5 request

```bash
curl -X PATCH "https://api.chargebackstop.com/v2/enrolments/enrl_abc123" \
  -H "Authorization: Bearer <partner_api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "ethoca_alert": {
      "descriptors": [
        {
          "descriptor": "SHOP.COM",
          "match_type": "EXACT_MATCH"
        }
      ]
    }
  }'
```

### Example 5 response

```json
{
  "id": "enrl_abc123",
  "organisation_id": "org_xyz456",
  "merchant_ids": ["mrch_abc123", "mrch_def456"],
  "type": "ETHOCA_ALERT",
  "status": "ENABLED",
  "ethoca_alert": {
    "descriptors": [
      {
        "descriptor": "ECOMM.COM",
        "match_type": "STARTS_WITH",
        "status": "ACTIVE"
      },
      {
        "descriptor": "SHOP.COM",
        "match_type": "EXACT_MATCH",
        "status": "PENDING"
      }
    ]
  },
  "created_at": "2026-02-16T12:00:00Z",
  "updated_at": "2026-02-16T13:00:00Z",
  "note": null,
  "links": [
    {
      "rel": "self",
      "uri": "/v2/enrolments/enrl_abc123"
    }
  ]
}
```

***

## DELETE /v2/enrolments/{enrolment\_id} - Cancel enrolment

Cancel one accessible enrolment.

**API level:** Partner-level only\
**Authentication:** `enrolments_v2:write`

### URL parameters

| Parameter      | Type   | Description            |
| -------------- | ------ | ---------------------- |
| `enrolment_id` | string | Enrolment ID to cancel |

### Important behaviour

* Only enrolments in `PENDING` status can be cancelled.
* Cancellation sets status to `CANCELLED` and sets `note` to `Enrolment cancelled via API request`.

### Example 6 request

```bash
curl -X DELETE "https://api.chargebackstop.com/v2/enrolments/enrl_abc123" \
  -H "Authorization: Bearer <partner_api_key>"
```

### Example 6 response

```json
{
  "id": "enrl_abc123",
  "organisation_id": "org_xyz456",
  "merchant_ids": ["mrch_abc123", "mrch_def456"],
  "type": "ETHOCA_ALERT",
  "status": "CANCELLED",
  "ethoca_alert": {
    "descriptors": [
      {
        "descriptor": "ECOMM.COM",
        "match_type": "STARTS_WITH",
        "status": "PENDING"
      }
    ]
  },
  "created_at": "2026-02-16T12:00:00Z",
  "updated_at": "2026-02-16T12:10:00Z",
  "note": "Enrolment cancelled via API request",
  "links": [
    {
      "rel": "self",
      "uri": "/v2/enrolments/enrl_abc123"
    }
  ]
}
```

***

## Common error responses

All error responses use this shape:

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

<details>

<summary>401 Unauthorised</summary>

Returned for invalid or expired API keys, and for organisation-level keys calling this partner-only API.

```json
{
  "errors": [
    {
      "code": "UNAUTHORISED",
      "message": "Unauthorised"
    }
  ]
}
```

</details>

<details>

<summary>403 Forbidden (missing v2 ability)</summary>

Returned when the key is valid but does not have the required v2 ability.

```json
{
  "errors": [
    {
      "code": "FORBIDDEN",
      "message": "Forbidden"
    }
  ]
}
```

</details>

<details>

<summary>404 Not found</summary>

Returned when the requested resource does not exist. This includes non-existent enrolments and non-existent `organisation_id` or `merchant_id` filters.

```json
{
  "errors": [
    {
      "code": "NOT_FOUND",
      "message": "Not found"
    }
  ]
}
```

</details>

<details>

<summary>422 Unprocessable Entity (mixed organisation merchants)</summary>

```json
{
  "errors": [
    {
      "code": "MERCHANTS_DIFFERENT_ORGANISATIONS",
      "message": "All merchants must belong to the same organisation"
    }
  ]
}
```

</details>

<details>

<summary>422 Unprocessable Entity (enrolment not pending)</summary>

```json
{
  "errors": [
    {
      "code": "ENROLMENT_NOT_PENDING",
      "message": "Only enrolments in PENDING state can be cancelled"
    }
  ]
}
```

</details>

Other business validation codes you may receive:

| Code                              | Meaning                                                                  |
| --------------------------------- | ------------------------------------------------------------------------ |
| `DUPLICATE_MERCHANT_IDS`          | `merchant_ids` contains duplicate values                                 |
| `MISSING_ETHOCA_ALERT_CONFIG`     | `ethoca_alert` is required for `ETHOCA_ALERT` enrolments                 |
| `MISSING_VERIFI_RDR_CONFIG`       | `verifi_rdr` is required for `VERIFI_RDR` enrolments                     |
| `INVALID_MATCH_TYPE`              | Ethoca descriptor `match_type` is invalid                                |
| `INVALID_VERIFI_RDR_CONFIG`       | Verifi payload does not provide valid BIN/CAID or ARNs                   |
| `ETHOCA_ALERT_ENROLLMENT_ERROR`   | Upstream Ethoca enrolment validation failed                              |
| `VERIFI_RDR_ENROLLMENT_ERROR`     | Upstream Verifi enrolment validation failed                              |
| `ENROLMENT_TYPE_NOT_SUPPORTED`    | PATCH is not supported for this enrolment type                           |
| `ENROLMENT_STATUS_NOT_APPENDABLE` | Enrolment status does not allow appending descriptors                    |
| `DUPLICATE_DESCRIPTOR`            | Descriptor and match type already exist or are duplicated in the request |


# Orders

Submit order data to ChargebackStop for chargeback prevention and dispute resolution. This API powers integrations with Mastercard Consumer Clarity, Visa Order Insight, and chargeback alert matching.

**Base URL:** `https://api.chargebackstop.com/v1/orders/`

**Authentication:** Bearer token via API key. Required abilities vary by endpoint (all newly created API keys include these Orders abilities by default):

* `orders:read` for GET endpoints
* `orders:write` for POST endpoints
* `orders:update` for PATCH endpoints

**Rate limits:**

* **100 requests per minute** per endpoint, per organisation/partner group
* Each endpoint has an independent rate limit pool (e.g., hitting the list limit doesn't affect create)
* Rate limits are applied by owner (organisation or partner group), not by API key
* Exceeding the limit returns `429 Too Many Requests`

**Batch Limit:** Maximum 100 orders per request (POST only).

***

## GET /v1/orders - List Orders

Retrieve a paginated list of orders with optional filtering and sorting.

**Authentication:** Bearer token via API key with `orders:read` ability.

### Query parameters

| Parameter         | Type     | Description                                                                                                         |
| ----------------- | -------- | ------------------------------------------------------------------------------------------------------------------- |
| `organisation_id` | string   | Filter by organisation ID                                                                                           |
| `integration_id`  | string   | Filter by integration ID                                                                                            |
| `reference_id`    | string   | Filter by exact reference\_id                                                                                       |
| `type`            | string   | Filter by order type: `COMPLETE` or `PARTIAL`                                                                       |
| `order_status`    | string   | Filter by status: `OPEN_PENDING`, `OPEN_PENDING_RETURN`, `CLOSED_COMPLETE`, `CLOSED_CANCELLED`, `OTHER`             |
| `created_at_gte`  | datetime | Orders created on or after this timestamp (ISO 8601)                                                                |
| `created_at_lte`  | datetime | Orders created on or before this timestamp (ISO 8601)                                                               |
| `sort`            | string   | Sort field: `created_at` (oldest first), `-created_at` (newest first, default), `order_datetime`, `-order_datetime` |
| `limit`           | integer  | Number of results per page (default: 100)                                                                           |
| `offset`          | integer  | Number of results to skip for pagination                                                                            |

### Example request

```bash
curl -X GET "https://api.chargebackstop.com/v1/orders/?reference_id=order-2024-001&sort=-created_at&limit=10" \
  -H "Authorization: Bearer <api_key>"
```

### Example response

```json
{
  "items": [
    {
      "id": "ord_abc123",
      "reference_id": "order-2024-001",
      "type": "COMPLETE",
      "organisation_id": "org_xyz",
      "integration_id": "int_123",
      "created_at": "2024-01-15T10:30:00Z",
      "order_datetime": "2024-01-15T10:30:00Z",
      "order_number": "ORD-2024-001",
      "order_status": "CLOSED_COMPLETE",
      "order_currency": "USD",
      "order_total_amount_in_cents": 9720,
      "transactions": [...],
      "deliveries": [...],
      "items": [...],
      "refunds": [...],
      "subscriptions": [...],
      "disputes": [...],
      "links": [
        {
          "rel": "self",
          "uri": "/v1/orders/ord_abc123"
        }
      ]
    }
  ],
  "count": 1
}
```

***

## GET /v1/orders/{order\_id} - Get Order by ID

Retrieve a single order by its internal `id` (the `id` field returned in JSON responses), not by `reference_id`.

To retrieve an order by `reference_id`, use the list endpoint: `GET /v1/orders?reference_id=<your_reference_id>`.

**Authentication:** Bearer token via API key with `orders:read` ability.

### URL parameters

| Parameter  | Type   | Description                                |
| ---------- | ------ | ------------------------------------------ |
| `order_id` | string | The internal order ID (e.g., `ord_abc123`) |

### Example request

```bash
curl -X GET "https://api.chargebackstop.com/v1/orders/ord_abc123" \
  -H "Authorization: Bearer <api_key>"
```

### Example response

Returns the full order object with all nested relations (transactions, deliveries, items, refunds, subscriptions, disputes).

```json
{
  "id": "ord_abc123",
  "reference_id": "order-2024-001",
  "type": "COMPLETE",
  "organisation_id": "org_xyz",
  "integration_id": "int_123",
  "created_at": "2024-01-15T10:30:00Z",
  "order_datetime": "2024-01-15T10:30:00Z",
  "order_number": "ORD-2024-001",
  "order_status": "CLOSED_COMPLETE",
  "order_currency": "USD",
  "order_total_amount_in_cents": 9720,
  "customer_email": "john.doe@example.com",
  "transactions": [...],
  "deliveries": [...],
  "items": [...],
  "refunds": [...],
  "subscriptions": [...],
  "disputes": [...],
  "links": [
    {
      "rel": "self",
      "uri": "/v1/orders/ord_abc123"
    }
  ]
}
```

### Error responses

**404 Not Found** - Order does not exist or is not accessible:

```json
{
  "errors": [
    {
      "code": "NOT_FOUND",
      "message": "Not found"
    }
  ]
}
```

***

## POST /v1/orders - Create Orders

Submit order data to ChargebackStop for chargeback prevention and dispute resolution.

**Authentication:** Bearer token via API key with `orders:write` ability.

**Rate Limit:** 100 requests per minute per organisation/partner group.

**Batch Limit:** Maximum 100 orders per request.

**Request Body:** JSON array of order objects (even when submitting a single order).

> **Schema validation before partial success:** Partial success applies after request schema validation. If the top-level request is invalid (for example, body is not an array or fields have invalid types), the API returns a full-request `422` and no orders are processed.

***

## Which mode should I use?

| Mode         | Use Case                                                                                                                                                              | Integration Type                   |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
| **PARTIAL**  | You have an existing payment processor integration (Stripe, Adyen, etc.) and want to submit enrichment data linked to those processor transactions                    | Any non-CUSTOM\_ORDERS integration |
| **COMPLETE** | You do not have an existing payment processor integration to enrich and want to submit full order data directly (for digital receipts, deflection, or alert matching) | CUSTOM\_ORDERS integration only    |

> **Important: Order type and integration type must match**
>
> * **COMPLETE orders** can ONLY be created with `CUSTOM_ORDERS` integrations. Attempting to create a COMPLETE order on a Stripe, Adyen, or other payment processor integration will return `INVALID_ORDER_TYPE`.
> * **PARTIAL orders** can ONLY be created with non-CUSTOM\_ORDERS integrations (Stripe, Adyen, Shopify, etc.) and require orders enrichment to be enabled by ChargebackStop support. Attempting to create a PARTIAL order on a CUSTOM\_ORDERS integration will return `INVALID_ORDER_TYPE`.

***

## PARTIAL mode - enrichment for existing integrations

Use PARTIAL mode when you already have a payment processor integration (Stripe, Adyen, Shopify, etc.) connected to ChargebackStop and want to enrich your orders with additional data.

### Getting started

**Prerequisites:**

* An existing integration (Stripe, Adyen, etc.) with orders enrichment enabled by the ChargebackStop team
* API key with `orders:write` ability

**Minimal required fields:**

| Field                 | Description                                          |
| --------------------- | ---------------------------------------------------- |
| `type`                | Must be `"PARTIAL"`                                  |
| `organisation_id`     | Your organisation ID                                 |
| `integration_id`      | Your integration ID                                  |
| `reference_id`        | Unique order identifier                              |
| `order_email`         | Email associated with the order                      |
| `customer_account_id` | Customer identifier (email, username, or account ID) |

> **Device identifier required:** PARTIAL orders must include at least one of: `device_ip_address`, `device_id`, or `device_fingerprint`. This is used for fraud prevention and dispute evidence.

**Example 1: Basic PARTIAL Order**

```json
[
  {
    "type": "PARTIAL",
    "organisation_id": "org_abc123",
    "integration_id": "int_stripe456",
    "reference_id": "order-2024-001",
    "order_email": "customer@example.com",
    "customer_account_id": "customer@example.com",
    "device_ip_address": "216.24.60.17"
  }
]
```

***

## Digital receipts and deflection field sets

These field sets are required for PARTIAL mode, and are required for all order modes when digital receipts or deflection is enabled.

Examples below use PARTIAL payloads, but the same field sets can be included in COMPLETE orders.

### Mastercard Consumer Clarity

Consumer Clarity helps reduce chargebacks by providing cardholders with detailed purchase information in their banking app, making transactions more recognisable.

**Required fields for Consumer Clarity:**

| Field                            | Description                          |
| -------------------------------- | ------------------------------------ |
| `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`                 | Currency code (ISO 4217)             |
| `order_total_amount_in_cents`    | Total amount including tax           |

**Example 2: Consumer Clarity (Mastercard)**

```json
[
  {
    "type": "PARTIAL",
    "organisation_id": "org_abc123",
    "integration_id": "int_stripe456",
    "reference_id": "order-2024-001",
    "order_email": "customer@example.com",
    "customer_account_id": "cust-12345",
    "device_ip_address": "216.24.60.83",

    "order_datetime": "2024-01-15T10:30:00Z",
    "order_number": "ORD-2024-001",
    "order_subtotal_amount_in_cents": 4500,
    "order_currency": "USD",
    "order_total_amount_in_cents": 4950
  }
]
```

***

### Visa Order Insight

Order Insight provides Visa cardholders with purchase details directly in their banking app, helping them recognise legitimate transactions.

**Required fields for Order Insight:**

| Field   | Description                                    |
| ------- | ---------------------------------------------- |
| `items` | Array with at least one item containing `name` |

**Example 3: Order Insight (Visa)**

```json
[
  {
    "type": "PARTIAL",
    "organisation_id": "org_abc123",
    "integration_id": "int_stripe456",
    "reference_id": "order-2024-001",
    "order_email": "customer@example.com",
    "customer_account_id": "cust-12345",
    "device_ip_address": "216.24.60.142",

    "items": [
      {
        "reference_id": "item-001",
        "name": "Widget Pro",
        "price_in_cents": 4500,
        "quantity": 1
      }
    ]
  }
]
```

***

### Product and merchant information

Some merchants (particularly Merchant of Record platforms) may need to include product and merchant information with each order if default settings are not configured in the platform.

> **Note:** If product and merchant information varies per order, include it in each order submission. If the values are stable across all orders, you can set defaults in the ChargebackStop platform instead.

**Product information fields:**

| Field                    | Description      |
| ------------------------ | ---------------- |
| `items[].name`           | Product name     |
| `items[].price_in_cents` | Item price       |
| `items[].quantity`       | Quantity ordered |

**Merchant information fields:**

| Field                    | Description                           |
| ------------------------ | ------------------------------------- |
| `merchant_name`          | Corporate/parent company name         |
| `merchant_contact_phone` | Customer service phone (E.164 format) |
| `merchant_store_name`    | Store name recognisable to customer   |
| `merchant_url`           | Merchant website URL                  |

**Example 4: Product and merchant information**

```json
[
  {
    "type": "PARTIAL",
    "organisation_id": "org_abc123",
    "integration_id": "int_stripe456",
    "reference_id": "order-2024-001",
    "order_email": "customer@example.com",
    "customer_account_id": "cust-12345",
    "device_ip_address": "216.24.60.201",

    "merchant_name": "Example Corporation",
    "merchant_contact_phone": "+14155551234",
    "merchant_store_name": "Example Store",
    "merchant_url": "https://example.com",

    "items": [
      {
        "reference_id": "item-001",
        "name": "Widget Pro",
        "price_in_cents": 4500,
        "quantity": 1
      }
    ]
  }
]
```

***

### First-Party Trust (FPT) - Mastercard

First-Party Trust is a Mastercard programme that helps identify legitimate transactions by linking customer identity data (email, device) across purchases. This strengthens fraud prevention and reduces first-party fraud chargebacks.

**Required fields for FPT:**

| Field             | Description                                                                |
| ----------------- | -------------------------------------------------------------------------- |
| `customer_email`  | Customer's email address                                                   |
| `order_email`     | Email associated with the order                                            |
| Device identifier | At least one of: `device_ip_address`, `device_id`, or `device_fingerprint` |

**Example 5: First-Party Trust (Mastercard)**

```json
[
  {
    "type": "PARTIAL",
    "organisation_id": "org_abc123",
    "integration_id": "int_stripe456",
    "reference_id": "order-2024-001",

    "customer_email": "john.doe@example.com",
    "order_email": "john.doe@example.com",
    "customer_account_id": "cust-12345",

    "device_ip_address": "216.24.60.58",
    "device_id": "device-abc123",
    "device_fingerprint": "fp-xyz789"
  }
]
```

***

### Compelling Evidence 3.0 (CE3.0) - Visa

Compelling Evidence 3.0 is a Visa programme that helps merchants win disputes by proving the cardholder has a history of legitimate purchases. By submitting customer and device data with each order, you build evidence that can be used in dispute responses.

**Required fields for CE3.0:**

| Field             | Description                                                                |
| ----------------- | -------------------------------------------------------------------------- |
| `customer_email`  | Customer's email address                                                   |
| Device identifier | At least one of: `device_ip_address`, `device_id`, or `device_fingerprint` |

**Example 6: Compelling Evidence 3.0 (Visa)**

```json
[
  {
    "type": "PARTIAL",
    "organisation_id": "org_abc123",
    "integration_id": "int_stripe456",
    "reference_id": "order-2024-001",

    "customer_email": "john.doe@example.com",
    "order_email": "john.doe@example.com",
    "customer_account_id": "cust-12345",

    "device_ip_address": "216.24.60.229",
    "device_fingerprint": "fp-xyz789"
  }
]
```

***

## COMPLETE mode

Use COMPLETE mode when you do not have an existing payment processor integration to enrich and want to submit full order data directly to ChargebackStop. This mode supports digital receipts, deflection, and alert matching, and is for merchants using the `CUSTOM_ORDERS` integration type.

### Getting started

**Prerequisites:**

* A `CUSTOM_ORDERS` integration
* API key with `orders:write` ability

**Required fields for COMPLETE orders:**

| Field                            | Description                        |
| -------------------------------- | ---------------------------------- |
| `type`                           | Must be `"COMPLETE"`               |
| `organisation_id`                | Your organisation ID               |
| `integration_id`                 | Your CUSTOM\_ORDERS integration ID |
| `reference_id`                   | Unique order identifier            |
| `order_datetime`                 | When the order was placed          |
| `order_number`                   | Customer-facing order number       |
| `order_subtotal_amount_in_cents` | Subtotal before tax                |
| `order_currency`                 | Currency code (ISO 4217)           |
| `order_total_amount_in_cents`    | Total amount                       |
| `order_status`                   | Order status                       |

***

### Alert matching

To match orders with chargeback alerts, you must include at least one transaction with specific fields that enable accurate matching. The richer the data set you provide, the better the matching accuracy will be — we recommend including as many transaction fields as possible.

**Required transaction fields:**

| Field                  | Description                   |
| ---------------------- | ----------------------------- |
| `amount_in_cents`      | Transaction amount            |
| `currency`             | Currency code                 |
| `authorised_at`        | When authorisation occurred   |
| `payment_method_type`  | Payment method (e.g., `CARD`) |
| `authorisation_status` | Status (e.g., `SETTLED`)      |

**Additional required fields for card payments:**

| Field                        | Description               |
| ---------------------------- | ------------------------- |
| `payment_method_card_last_4` | Last 4 digits of card     |
| `payment_method_card_brand`  | Card brand (e.g., `VISA`) |

**Matching identifiers (send all three whenever possible):**

| Identifier                  | Recommendation          | Notes                                                                                     |
| --------------------------- | ----------------------- | ----------------------------------------------------------------------------------------- |
| `acquirer_reference_number` | Send whenever available | Most reliable; directly links to the network transaction                                  |
| `authorisation_code`        | Send whenever available | Improves matching when paired with card details (especially `payment_method_card_last_4`) |
| `payment_method_card_bin`   | Send whenever available | Improves matching when combined with card details and amount/currency                     |

For card payments, at least one matching identifier is required. Best practice is to send all three identifiers whenever available, or as many as possible.

**Why these fields matter:**

* **ARN (Acquirer Reference Number):** The most reliable identifier for matching - directly links to the network transaction
* **Authorisation Code:** The 6-digit code from the issuer, used with card last 4 when ARN is not available
* **Card BIN:** The first 6-8 digits of the card, helps narrow down matches when combined with other card details and amount

> **Note:** It is worth sending any related refunds and disputes for the order. Including these records helps us detect invalid alerts more accurately.

**Example 7: Alert matching (COMPLETE)**

```json
[
  {
    "type": "COMPLETE",
    "organisation_id": "org_abc123",
    "integration_id": "int_custom789",
    "reference_id": "order-2024-001",

    "order_datetime": "2024-01-15T10:30:00Z",
    "order_number": "ORD-2024-001",
    "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",

    "customer_email": "john.doe@example.com",
    "customer_first_name": "John",
    "customer_last_name": "Doe",

    "transactions": [
      {
        "reference_id": "txn-001",
        "amount_in_cents": 9720,
        "currency": "USD",
        "payment_method_type": "CARD",
        "authorisation_status": "SETTLED",
        "payment_method_reference_id": "pm_card_abc123",
        "authorised_at": "2024-01-15T10:30:05Z",
        "descriptor": "EXAMPLE STORE",
        "descriptor_prefix": "EXAMPLE",
        "descriptor_suffix": "STORE",
        "settlement_datetime": "2024-01-16T00:00:00Z",
        "cvc_verified": true,
        "three_d_secure_verified": true,

        "acquirer_reference_number": "74027012345678901234567",
        "authorisation_code": "123456",
        "network_id": "network-txn-123",

        "payment_method_card_brand": "VISA",
        "payment_method_card_last_4": "4242",
        "payment_method_card_bin": "424242",
        "payment_method_card_exp_month": 12,
        "payment_method_card_exp_year": 2026,
        "payment_method_card_wallet_type": "APPLE_PAY",
        "payment_method_card_issuer": "Chase Bank",
        "billing_address": {
          "line_1": "123 Main St",
          "line_2": "Apt 4B",
          "city": "New York",
          "country_subdivision": "NY",
          "postal_code": "10001",
          "country": "US"
        }
      }
    ],

    "refunds": [
      {
        "reference_id": "refund-001",
        "amount_in_cents": 4500,
        "currency": "USD",
        "status": "SUCCEEDED",
        "original_transaction_reference_id": "txn-001",
        "refund_datetime": "2024-01-17T12:00:00Z"
      }
    ],

    "disputes": [
      {
        "reference_id": "dispute-001",
        "amount_in_cents": 4500,
        "currency": "USD",
        "stage": "1ST_CHARGEBACK",
        "status": "OPEN",
        "type": "CHARGEBACK",
        "network_reason_code": "10.4",
        "is_rapid_dispute_resolution": false,
        "evidence_due_by": "2024-02-15T23:59:59Z",
        "payment_method_type": "CARD",
        "card_brand": "VISA"
      }
    ]
  }
]
```

***

## Example 8: Full COMPLETE Order

This example includes all available fields for a COMPLETE order.

```json
[
  {
    "type": "COMPLETE",
    "organisation_id": "org_abc123",
    "integration_id": "int_custom789",
    "reference_id": "order-2024-001",

    "order_datetime": "2024-01-15T10:30:00Z",
    "order_number": "ORD-2024-001",
    "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_is_adult_content": false,

    "order_request_refund_url": "https://example.com/orders/ORD-2024-001/refund",
    "order_buy_again_url": "https://example.com/orders/ORD-2024-001/reorder",
    "order_write_review_url": "https://example.com/orders/ORD-2024-001/review",
    "order_view_url": "https://example.com/orders/ORD-2024-001",

    "order_proof_of_consent": "Customer agreed to Terms of Service on 2024-01-15 via checkout checkbox",
    "order_communications": "Order confirmation email sent 2024-01-15. Shipping notification sent 2024-01-16.",

    "customer_email": "john.doe@example.com",
    "customer_first_name": "John",
    "customer_last_name": "Doe",
    "customer_account_id": "john.doe@example.com",
    "order_email": "john.doe@example.com",

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

    "merchant_reference_id": "merchant-001",
    "merchant_name": "Example Corporation",
    "merchant_store_name": "Example Store",
    "merchant_store_description": "Your one-stop shop for widgets and gadgets",
    "merchant_contact_email": "receipts@example.com",
    "merchant_customer_service_email": "support@example.com",
    "merchant_contact_phone": "+14155559876",
    "merchant_address": {
      "line_1": "100 Commerce St",
      "line_2": "Suite 500",
      "line_3": "Building A",
      "city": "San Francisco",
      "country_subdivision": "CA",
      "postal_code": "94105",
      "country": "US"
    },
    "merchant_store_url": "https://store.example.com",
    "merchant_url": "https://example.com",
    "merchant_terms_and_conditions_url": "https://example.com/terms",
    "merchant_logo_url": "https://example.com/logo.png",
    "merchant_refund_policy_url": "https://example.com/refund-policy",

    "transactions": [
      {
        "reference_id": "txn-001",
        "amount_in_cents": 9720,
        "currency": "USD",
        "payment_method_type": "CARD",
        "authorisation_status": "SETTLED",
        "payment_method_reference_id": "pm_card_abc123",
        "authorised_at": "2024-01-15T10:30:05Z",
        "descriptor": "EXAMPLE STORE",
        "descriptor_prefix": "EXAMPLE",
        "descriptor_suffix": "STORE",
        "authorisation_code": "123456",
        "acquirer_reference_number": "74027012345678901234567",
        "network_id": "network-txn-123",
        "settlement_datetime": "2024-01-16T00:00:00Z",
        "cvc_verified": true,
        "three_d_secure_verified": true,
        "payment_method_card_brand": "VISA",
        "payment_method_card_last_4": "4242",
        "payment_method_card_exp_month": 12,
        "payment_method_card_exp_year": 2026,
        "payment_method_card_bin": "424242",
        "payment_method_card_wallet_type": "APPLE_PAY",
        "payment_method_card_issuer": "Chase Bank",
        "billing_address": {
          "line_1": "123 Main St",
          "line_2": "Apt 4B",
          "city": "New York",
          "country_subdivision": "NY",
          "postal_code": "10001",
          "country": "US"
        }
      }
    ],

    "deliveries": [
      {
        "reference_id": "dlv-physical-001",
        "type": "PHYSICAL",
        "physical_shipping_carrier": "UPS",
        "physical_shipping_tracking_number": "1Z999AA10123456784",
        "physical_shipping_status": "DELIVERED",
        "physical_shipping_datetime_shipped": "2024-01-16T08:00:00Z",
        "physical_shipping_datetime_delivered": "2024-01-18T14:30:00Z",
        "physical_shipping_address": {
          "line_1": "123 Main St",
          "line_2": "Apt 4B",
          "city": "New York",
          "country_subdivision": "NY",
          "postal_code": "10001",
          "country": "US"
        }
      },
      {
        "reference_id": "dlv-digital-001",
        "type": "DIGITAL",
        "digital_delivery_datetime": "2024-01-15T10:35:00Z",
        "digital_download_start_datetime": "2024-01-15T10:36:00Z",
        "digital_download_end_datetime": "2024-01-15T10:37:00Z",
        "digital_delivery_ip_address": "216.24.60.175",
        "digital_notification_sent": true,
        "digital_notification_sent_datetime": "2024-01-15T10:35:00Z",
        "digital_notification_method": "EMAIL"
      }
    ],

    "items": [
      {
        "reference_id": "item-001",
        "name": "Widget Pro",
        "price_in_cents": 4500,
        "quantity": 2,
        "product_url": "https://example.com/products/widget-pro",
        "product_reference_id": "prod-widget-pro",
        "sku": "WIDGET-PRO-001",
        "delivery_reference_id": "dlv-physical-001"
      },
      {
        "reference_id": "item-002",
        "name": "Digital License Key",
        "price_in_cents": 0,
        "quantity": 1,
        "product_url": "https://example.com/products/license",
        "product_reference_id": "prod-license",
        "sku": "LICENSE-001",
        "delivery_reference_id": "dlv-digital-001",
        "subscription_reference_id": "sub-001"
      }
    ],

    "refunds": [
      {
        "reference_id": "refund-001",
        "amount_in_cents": 4500,
        "currency": "USD",
        "status": "SUCCEEDED",
        "original_transaction_reference_id": "txn-001",
        "refund_datetime": "2024-01-17T12:00:00Z"
      }
    ],

    "subscriptions": [
      {
        "reference_id": "sub-001",
        "interval": "MONTH",
        "interval_price_in_cents": 2999,
        "interval_currency": "USD",
        "status": "ACTIVE",
        "display_name": "Pro Plan Monthly",
        "trial_start_date": "2024-01-01T00:00:00Z",
        "trial_end_date": "2024-01-14T23:59:59Z",
        "trial_price_in_cents": 0,
        "trial_currency": "USD",
        "start_date": "2024-01-15T00:00:00Z",
        "next_charge_date": "2024-02-15T00:00:00Z"
      }
    ],

    "disputes": [
      {
        "reference_id": "dispute-001",
        "amount_in_cents": 4500,
        "currency": "USD",
        "stage": "1ST_CHARGEBACK",
        "status": "OPEN",
        "type": "CHARGEBACK",
        "network_reason_code": "10.4",
        "is_rapid_dispute_resolution": false,
        "evidence_due_by": "2024-02-15T23:59:59Z",
        "payment_method_type": "CARD",
        "card_brand": "VISA"
      }
    ]
  }
]
```

***

## API reference

**Required column abbreviations:**

| Abbreviation | Feature                        |
| ------------ | ------------------------------ |
| CC           | Consumer Clarity (Mastercard)  |
| FPT          | First Party Trust (Mastercard) |
| OI           | Order Insight (Visa)           |
| CE3.0        | Compelling Evidence 3.0 (Visa) |

**Validation wording note:**

* `Required` means the field must be present in the request payload. It does not, by itself, imply a non-empty string value.
* `1-255 chars` means the API enforces a non-empty string.
* `Up to 255 chars` means the API enforces only a maximum length, and an empty string may still be accepted when the field is otherwise optional.

***

### Order fields

#### Core fields

| Field             | Type   | Required | Validation              | Description              |
| ----------------- | ------ | -------- | ----------------------- | ------------------------ |
| `type`            | string | Yes      | `COMPLETE` or `PARTIAL` | Order type discriminator |
| `organisation_id` | string | Yes      | Valid org ID            | Your organisation ID     |
| `integration_id`  | string | Yes      | Valid integration ID    | Integration ID           |
| `reference_id`    | string | Yes      | 1-255 chars             | Unique order identifier  |

#### Order details

| Field                            | Type     | Required                                   | Validation                                                                            | Description                      |
| -------------------------------- | -------- | ------------------------------------------ | ------------------------------------------------------------------------------------- | -------------------------------- |
| `order_datetime`                 | datetime | `COMPLETE` orders, and for CC when enabled | ISO 8601                                                                              | When the order was placed        |
| `order_number`                   | string   | `COMPLETE` orders, and for CC when enabled | Up to 255 chars                                                                       | Customer-facing order number     |
| `order_subtotal_amount_in_cents` | integer  | `COMPLETE` orders, and for CC when enabled | >= 0                                                                                  | Subtotal before tax              |
| `order_currency`                 | string   | `COMPLETE` orders, and for CC when enabled | ISO 4217                                                                              | Currency code                    |
| `order_tax_amount_in_cents`      | integer  | No                                         | >= 0                                                                                  | Tax amount                       |
| `order_total_amount_in_cents`    | integer  | `COMPLETE` orders, and for CC when enabled | >= 0                                                                                  | Total amount                     |
| `order_status`                   | enum     | `COMPLETE` orders                          | `OPEN_PENDING`, `OPEN_PENDING_RETURN`, `CLOSED_COMPLETE`, `CLOSED_CANCELLED`, `OTHER` | Order status                     |
| `order_status_other_description` | string   | If `order_status` is `OTHER`               | 1-255 chars                                                                           | Description when status is OTHER |
| `order_phone`                    | string   | No                                         | E.164 format                                                                          | Order contact phone              |
| `order_is_adult_content`         | boolean  | No                                         | -                                                                                     | Adult content flag               |

#### Order URLs

| Field                      | Type   | Required | Validation                 | Description        |
| -------------------------- | ------ | -------- | -------------------------- | ------------------ |
| `order_request_refund_url` | string | No       | Valid URL, up to 255 chars | Refund request URL |
| `order_buy_again_url`      | string | No       | Valid URL, up to 255 chars | Reorder URL        |
| `order_write_review_url`   | string | No       | Valid URL, up to 255 chars | Review URL         |
| `order_view_url`           | string | No       | Valid URL, up to 255 chars | Order view URL     |

#### Order evidence

| Field                    | Type   | Required | Validation       | Description               |
| ------------------------ | ------ | -------- | ---------------- | ------------------------- |
| `order_proof_of_consent` | string | No       | Up to 500 chars  | Proof of customer consent |
| `order_communications`   | string | No       | Up to 1000 chars | Communications history    |

***

### Customer fields

| Field                 | Type   | Required                                   | Validation      | Description         |
| --------------------- | ------ | ------------------------------------------ | --------------- | ------------------- |
| `customer_email`      | string | For FPT, CE3.0                             | Valid email     | Customer's email    |
| `customer_first_name` | string | No                                         | Up to 255 chars | First name          |
| `customer_last_name`  | string | No                                         | Up to 255 chars | Last name           |
| `customer_account_id` | string | `PARTIAL` orders                           | Up to 255 chars | Customer identifier |
| `order_email`         | string | `PARTIAL` orders, and for FPT when enabled | Valid email     | Order email         |

***

### Device fields

| Field                | Type   | Required                                                        | Validation      | Description        |
| -------------------- | ------ | --------------------------------------------------------------- | --------------- | ------------------ |
| `device_ip_address`  | string | `PARTIAL` orders, and for FPT/CE3.0 when enabled (at least one) | IPv4/IPv6       | Customer's IP      |
| `device_id`          | string | `PARTIAL` orders, and for FPT/CE3.0 when enabled (at least one) | Up to 255 chars | Device ID          |
| `device_fingerprint` | string | `PARTIAL` orders, and for FPT/CE3.0 when enabled (at least one) | Up to 255 chars | Device fingerprint |

At least one device identifier is required for `PARTIAL` orders, and for FPT/CE3.0 when enabled.

***

### Merchant fields

| Field                               | Type   | Required                             | Validation      | Description                 |
| ----------------------------------- | ------ | ------------------------------------ | --------------- | --------------------------- |
| `merchant_reference_id`             | string | No                                   | Up to 255 chars | Internal merchant reference |
| `merchant_name`                     | string | When default merchant not configured | Up to 255 chars | Corporate name              |
| `merchant_store_name`               | string | When default merchant not configured | Up to 255 chars | Store name                  |
| `merchant_store_description`        | string | No                                   | Up to 255 chars | Store description           |
| `merchant_contact_email`            | string | No                                   | Valid email     | Receipt email               |
| `merchant_customer_service_email`   | string | No                                   | Valid email     | Support email               |
| `merchant_contact_phone`            | string | When default merchant not configured | E.164           | Support phone               |
| `merchant_url`                      | string | When default merchant not configured | URL             | Merchant URL                |
| `merchant_store_url`                | string | No                                   | URL             | Store URL                   |
| `merchant_terms_and_conditions_url` | string | No                                   | URL             | T\&C URL                    |
| `merchant_logo_url`                 | string | No                                   | URL             | Logo URL                    |
| `merchant_refund_policy_url`        | string | No                                   | URL             | Refund policy URL           |

#### Merchant address

| Field                                  | Type   | Required                                                       | Description    |
| -------------------------------------- | ------ | -------------------------------------------------------------- | -------------- |
| `merchant_address.line_1`              | string | At least one line required when `merchant_address` is provided | Address line 1 |
| `merchant_address.line_2`              | string | At least one line required when `merchant_address` is provided | Address line 2 |
| `merchant_address.line_3`              | string | At least one line required when `merchant_address` is provided | Address line 3 |
| `merchant_address.city`                | string | Required when `merchant_address` is provided                   | City           |
| `merchant_address.country_subdivision` | string | Required when `merchant_address` is provided                   | State/province |
| `merchant_address.postal_code`         | string | Required when `merchant_address` is provided                   | Postal code    |
| `merchant_address.country`             | string | Required when `merchant_address` is provided                   | Country code   |

> **Request/response shape:** Both requests and responses use nested `merchant_address`.

***

### Transactions

**Limit:** Maximum 10 transactions per order.

At least one transaction is required for Alert Matching.

| Field                         | Type     | Required                                | Validation                                      | Description                                    |
| ----------------------------- | -------- | --------------------------------------- | ----------------------------------------------- | ---------------------------------------------- |
| `reference_id`                | string   | Always                                  | 1-255 chars                                     | Transaction ID (unique within the integration) |
| `amount_in_cents`             | integer  | Always                                  | >= 0                                            | Amount                                         |
| `currency`                    | string   | Always                                  | ISO 4217                                        | Currency                                       |
| `payment_method_type`         | enum     | Always                                  | `CARD`, `STRIPE_LINK`, `BANK_ACCOUNT`, `OTHER`  | Payment method                                 |
| `authorisation_status`        | enum     | Always                                  | `AUTHORISED`, `CAPTURED`, `SETTLED`, `REVERTED` | Auth status                                    |
| `payment_method_reference_id` | string   | Always                                  | Up to 255 chars                                 | Payment method ID                              |
| `authorised_at`               | datetime | For Alert Matching                      | ISO 8601                                        | Auth timestamp                                 |
| `descriptor`                  | string   | No                                      | Up to 255 chars                                 | Billing descriptor                             |
| `descriptor_prefix`           | string   | No                                      | Up to 255 chars                                 | Descriptor prefix                              |
| `descriptor_suffix`           | string   | No                                      | Up to 255 chars                                 | Descriptor suffix                              |
| `authorisation_code`          | string   | For Alert Matching (CARD, at least one) | Up to 6 chars                                   | Auth code                                      |
| `acquirer_reference_number`   | string   | For Alert Matching (CARD, at least one) | 50 chars                                        | ARN                                            |
| `network_id`                  | string   | No                                      | 50 chars                                        | Network transaction identifier                 |
| `settlement_datetime`         | datetime | No                                      | ISO 8601                                        | Settlement time                                |
| `cvc_verified`                | boolean  | No                                      | -                                               | CVC verified                                   |
| `three_d_secure_verified`     | boolean  | No                                      | -                                               | 3DS verified                                   |

#### Card payment fields

When `payment_method_type` is `CARD`, the following fields are required:

| Field                             | Type    | Required                                     | Description                                                                                                                      |
| --------------------------------- | ------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `payment_method_card_brand`       | enum    | If `payment_method_type` is `CARD`           | `AMEX`, `DINERS`, `DISCOVER`, `EFTPOS_AU`, `JCB`, `MASTERCARD`, `UNIONPAY`, `VISA`, `CARTES_BANCAIRES`, `OTHER`                  |
| `payment_method_card_last_4`      | string  | If `payment_method_type` is `CARD`           | Last 4 digits                                                                                                                    |
| `payment_method_card_exp_month`   | integer | No                                           | Exp month                                                                                                                        |
| `payment_method_card_exp_year`    | integer | No                                           | Exp year                                                                                                                         |
| `payment_method_card_bin`         | string  | For Alert Matching (at least one identifier) | Card BIN                                                                                                                         |
| `payment_method_card_wallet_type` | enum    | No                                           | `AMEX_EXPRESS_CHECKOUT`, `APPLE_PAY`, `GOOGLE_PAY`, `LINK`, `MASTERPASS`, `SAMSUNG_PAY`, `VISA_CHECKOUT`, `REVOLUT_PAY`, `OTHER` |
| `payment_method_card_issuer`      | string  | No                                           | Card issuer                                                                                                                      |

> **Note:** For alert matching with card payments, send all three identifiers whenever available (`acquirer_reference_number`, `authorisation_code`, and `payment_method_card_bin`). If you cannot provide all three, provide at least one of the following combinations:
>
> * `acquirer_reference_number` (works on its own)
> * `authorisation_code` + `payment_method_card_last_4`
> * `payment_method_card_bin` + `payment_method_card_last_4` + `payment_method_card_brand` + `amount_in_cents` + `currency`

#### Billing address

| Field                                 | Type   | Required                                                      | Description    |
| ------------------------------------- | ------ | ------------------------------------------------------------- | -------------- |
| `billing_address.line_1`              | string | At least one line required when `billing_address` is provided | Address line 1 |
| `billing_address.line_2`              | string | At least one line required when `billing_address` is provided | Address line 2 |
| `billing_address.line_3`              | string | At least one line required when `billing_address` is provided | Address line 3 |
| `billing_address.city`                | string | Required when `billing_address` is provided                   | City           |
| `billing_address.country_subdivision` | string | Required when `billing_address` is provided                   | State/province |
| `billing_address.postal_code`         | string | Required when `billing_address` is provided                   | Postal code    |
| `billing_address.country`             | string | Required when `billing_address` is provided                   | Country code   |

***

### Deliveries

**Limit:** Maximum 10 deliveries per order.

| Field          | Type   | Required | Description                                 |
| -------------- | ------ | -------- | ------------------------------------------- |
| `reference_id` | string | Always   | Delivery ID (unique within the integration) |
| `type`         | enum   | No       | `DIGITAL` or `PHYSICAL`                     |

> **Create uniqueness scope:** On POST, deliveries are unique per integration by `reference_id`. Reusing a delivery `reference_id` for the same integration returns `DUPLICATE_DELIVERY`.

#### Digital delivery fields

| Field                                | Type     | Required | Description                     |
| ------------------------------------ | -------- | -------- | ------------------------------- |
| `digital_delivery_datetime`          | datetime | No       | Delivery time                   |
| `digital_download_start_datetime`    | datetime | No       | Download start                  |
| `digital_download_end_datetime`      | datetime | No       | Download end                    |
| `digital_delivery_ip_address`        | string   | No       | Download IP                     |
| `digital_notification_sent`          | boolean  | No       | Notification sent               |
| `digital_notification_sent_datetime` | datetime | No       | Notification time               |
| `digital_notification_method`        | enum     | No       | `EMAIL`, `SMS`, `PUSH`, `OTHER` |

#### Physical delivery fields

When `type` is `PHYSICAL`, the following fields are required:

| Field                                        | Type     | Required                                 | Description                                                                                                                                                |
| -------------------------------------------- | -------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `physical_shipping_carrier`                  | string   | No                                       | Carrier name                                                                                                                                               |
| `physical_shipping_tracking_number`          | string   | No                                       | Tracking number                                                                                                                                            |
| `physical_shipping_status`                   | enum     | If `type` is `PHYSICAL`                  | `NOT_SHIPPED`, `BACKORDERED`, `IN_TRANSIT`, `PARTIAL_SHIPPED`, `SHIPPED`, `CANCELLED`, `SHIPPING_EXCEPTION`, `PICKED_UP_BY_CUSTOMER`, `DELIVERED`, `OTHER` |
| `physical_shipping_status_other_description` | string   | If `physical_shipping_status` is `OTHER` | Description when status is OTHER                                                                                                                           |
| `physical_shipping_datetime_shipped`         | datetime | If `type` is `PHYSICAL`                  | Ship date                                                                                                                                                  |
| `physical_shipping_datetime_delivered`       | datetime | No                                       | Delivery date                                                                                                                                              |

#### Physical shipping address

| Field                                           | Type   | Required                                                                | Description    |
| ----------------------------------------------- | ------ | ----------------------------------------------------------------------- | -------------- |
| `physical_shipping_address.line_1`              | string | At least one line required when `physical_shipping_address` is provided | Address line 1 |
| `physical_shipping_address.line_2`              | string | At least one line required when `physical_shipping_address` is provided | Address line 2 |
| `physical_shipping_address.line_3`              | string | At least one line required when `physical_shipping_address` is provided | Address line 3 |
| `physical_shipping_address.city`                | string | Required when `physical_shipping_address` is provided                   | City           |
| `physical_shipping_address.country_subdivision` | string | Required when `physical_shipping_address` is provided                   | State/province |
| `physical_shipping_address.postal_code`         | string | Required when `physical_shipping_address` is provided                   | Postal code    |
| `physical_shipping_address.country`             | string | Required when `physical_shipping_address` is provided                   | Country code   |

***

### Items

**Limit:** Maximum 10 items per order.

| Field                       | Type    | Required | Description                                                               |
| --------------------------- | ------- | -------- | ------------------------------------------------------------------------- |
| `reference_id`              | string  | Yes      | Item ID (unique within the integration)                                   |
| `name`                      | string  | Yes      | Product name                                                              |
| `price_in_cents`            | integer | Yes      | Price in cents (≥ 0)                                                      |
| `quantity`                  | integer | Yes      | Quantity (≥ 1)                                                            |
| `product_url`               | string  | No       | Product URL                                                               |
| `product_reference_id`      | string  | No       | Internal product ID                                                       |
| `sku`                       | string  | No       | SKU                                                                       |
| `delivery_reference_id`     | string  | No       | Links to delivery (must reference a delivery in the same request)         |
| `subscription_reference_id` | string  | No       | Links to subscription (must reference a subscription in the same request) |

***

### Refunds

**Limit:** Maximum 10 refunds per order.

| Field                               | Type     | Required            | Description                               |
| ----------------------------------- | -------- | ------------------- | ----------------------------------------- |
| `reference_id`                      | string   | Always              | Refund ID (unique within the integration) |
| `amount_in_cents`                   | integer  | Always              | Refund amount                             |
| `currency`                          | string   | Always              | Currency                                  |
| `status`                            | enum     | Always              | `PENDING`, `SUCCEEDED`, `FAILED`          |
| `original_transaction_reference_id` | string   | No                  | Links to transaction                      |
| `refund_datetime`                   | datetime | For CC when enabled | When the refund was processed (ISO 8601)  |

> **Create uniqueness scope:** On POST, refunds are unique per integration by `reference_id`. Reusing a refund `reference_id` for the same integration returns `DUPLICATE_REFUND`.

***

### Subscriptions

**Limit:** Maximum 10 subscriptions per order.

> **Upsert behaviour**: Subscriptions are identified by `reference_id` per integration. If a subscription with the same `reference_id` already exists, it will be **updated** with the new values. The subscription will be linked to the new order while maintaining links to previous orders. This allows the same subscription to span multiple orders (e.g., monthly renewals).
>
> **Warning: Full replace semantics** - When updating a subscription, ALL optional fields are replaced, not merged. If you omit a field in a subsequent request, it will be set to NULL. For example, if Order 1 creates a subscription with `display_name: "Pro Plan"` and Order 2 references the same subscription without `display_name`, the display name will be cleared.

| Field                     | Type     | Required | Description                                   |
| ------------------------- | -------- | -------- | --------------------------------------------- |
| `reference_id`            | string   | Always   | Subscription ID (unique per integration)      |
| `interval`                | enum     | Always   | `DAY`, `WEEK`, `MONTH`, `YEAR`                |
| `interval_price_in_cents` | integer  | Always   | Price per interval                            |
| `interval_currency`       | string   | Always   | Currency                                      |
| `status`                  | enum     | No       | `ACTIVE`, `CANCELLED`, `TRIALING`, `PAST_DUE` |
| `display_name`            | string   | No       | Display name (up to 255 chars)                |
| `trial_start_date`        | datetime | No       | Trial start                                   |
| `trial_end_date`          | datetime | No       | Trial end                                     |
| `trial_price_in_cents`    | integer  | No       | Trial price                                   |
| `trial_currency`          | string   | No       | Trial currency                                |
| `start_date`              | datetime | No       | Subscription start                            |
| `cancellation_date`       | datetime | No       | Cancellation date                             |
| `next_charge_date`        | datetime | No       | Next billing date                             |

***

### Disputes

**Limit:** Maximum 10 disputes per order.

| Field                         | Type     | Required                           | Description                                                                                                     |
| ----------------------------- | -------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `reference_id`                | string   | Always                             | Dispute ID (unique within the integration)                                                                      |
| `amount_in_cents`             | integer  | Always                             | Dispute amount                                                                                                  |
| `currency`                    | string   | Always                             | Currency                                                                                                        |
| `stage`                       | enum     | Always                             | `1ST_CHARGEBACK`, `2ND_CHARGEBACK`                                                                              |
| `status`                      | enum     | Always                             | `OPEN`, `UNDER_REVIEW`, `WON`, `LOST`                                                                           |
| `type`                        | enum     | Always                             | `INQUIRY`, `CHARGEBACK`                                                                                         |
| `network_reason_code`         | string   | No                                 | Network reason code                                                                                             |
| `is_rapid_dispute_resolution` | boolean  | No                                 | RDR flag                                                                                                        |
| `evidence_due_by`             | datetime | No                                 | Evidence deadline                                                                                               |
| `payment_method_type`         | enum     | No                                 | `CARD`, `KLARNA`, `PAYPAL`                                                                                      |
| `card_brand`                  | enum     | If `payment_method_type` is `CARD` | `AMEX`, `DINERS`, `DISCOVER`, `EFTPOS_AU`, `JCB`, `MASTERCARD`, `UNIONPAY`, `VISA`, `CARTES_BANCAIRES`, `OTHER` |

> **Create uniqueness scope:** On POST, disputes are unique per integration by `reference_id`. Reusing a dispute `reference_id` for the same integration returns `DUPLICATE_DISPUTE`.

***

### Address schema

All address objects (billing, shipping, merchant) use this structure:

| Field                 | Type   | Required                   | Validation                        |
| --------------------- | ------ | -------------------------- | --------------------------------- |
| `line_1`              | string | At least one line required | Up to 255 chars                   |
| `line_2`              | string | At least one line required | Up to 255 chars                   |
| `line_3`              | string | At least one line required | Up to 255 chars                   |
| `city`                | string | Always                     | 1-255 chars                       |
| `country_subdivision` | string | Always                     | 1-255 chars                       |
| `postal_code`         | string | Always                     | 1-255 chars                       |
| `country`             | string | Always                     | ISO 3166-1 alpha-2 uppercase code |

> **Note:** When providing an address, include at least one of `line_1`, `line_2`, or `line_3`, plus `city`, `country_subdivision`, `postal_code`, and `country`.

***

## Field validation

### Currency codes

All currency fields must be valid **ISO 4217** codes. Common examples: `USD`, `EUR`, `GBP`, `CAD`, `AUD`, `JPY`. See the [ISO 4217 standard](https://en.wikipedia.org/wiki/ISO_4217) for the full list.

### Phone numbers

Phone fields must be in **E.164 format**:

* Start with `+`
* Followed by 1-15 digits
* No spaces or special characters

**Valid:** `+14155551234`, `+442071234567` **Invalid:** `415-555-1234`, `(415) 555-1234`

### Email addresses

All email fields are validated for proper email format.

### Amounts

All `*_in_cents` fields must be non-negative integers (>= 0).

***

## Error codes

| Code                               | Description                                                                                                                                         |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `RATE_LIMITED`                     | Rate limit exceeded (100 requests/min per endpoint)                                                                                                 |
| `BATCH_SIZE_EXCEEDED`              | More than 100 orders in request                                                                                                                     |
| `DUPLICATE_DELIVERY`               | Delivery with same `reference_id` already exists for this integration                                                                               |
| `DUPLICATE_DELIVERY_REFERENCE`     | Duplicate `reference_id` in deliveries array (within same request)                                                                                  |
| `DUPLICATE_DISPUTE`                | Dispute with same `reference_id` already exists for this integration                                                                                |
| `DUPLICATE_DISPUTE_REFERENCE`      | Duplicate `reference_id` in disputes array (within same request)                                                                                    |
| `DUPLICATE_ITEM`                   | Item with same `reference_id` already exists for this integration                                                                                   |
| `DUPLICATE_ITEM_REFERENCE`         | Duplicate `reference_id` in items array (within same request)                                                                                       |
| `DUPLICATE_ORDER`                  | Order with same `reference_id` already exists for this integration                                                                                  |
| `DUPLICATE_REFERENCE_ID`           | Fallback error when a duplicate `reference_id` is detected                                                                                          |
| `DUPLICATE_REFUND`                 | Refund with same `reference_id` already exists for this integration                                                                                 |
| `DUPLICATE_REFUND_REFERENCE`       | Duplicate `reference_id` in refunds array (within same request)                                                                                     |
| `DUPLICATE_SUBSCRIPTION_REFERENCE` | Duplicate `reference_id` in subscriptions array (within same request)                                                                               |
| `DUPLICATE_TRANSACTION`            | Transaction with same `reference_id` already exists for this integration                                                                            |
| `DUPLICATE_TRANSACTION_REFERENCE`  | Duplicate `reference_id` in transactions array (within same request)                                                                                |
| `ENRICHMENT_NOT_ENABLED`           | Orders enrichment is not enabled for a non-`CUSTOM_ORDERS` integration when submitting a `PARTIAL` order                                            |
| `INTERNAL_ERROR`                   | Internal server error                                                                                                                               |
| `INVALID_DELIVERY_REFERENCE`       | Item references a non-existent delivery                                                                                                             |
| `INVALID_INTEGRATION`              | Integration not found or not accessible                                                                                                             |
| `INVALID_ORDER_TYPE`               | Order type doesn't match integration type                                                                                                           |
| `INVALID_SUBSCRIPTION_REFERENCE`   | Item references a non-existent subscription                                                                                                         |
| `MISSING_FIELD`                    | Required field is missing for the configured integration                                                                                            |
| `DELIVERY_NOT_FOUND`               | Referenced delivery not found in order (PATCH only)                                                                                                 |
| `IMMUTABLE_FIELD`                  | Attempting to modify immutable fields on existing refund/dispute records (`amount_in_cents`, `currency`, and for disputes also `type`) (PATCH only) |
| `INVALID_FIELD_FOR_TYPE`           | Updating wrong type fields on delivery (PATCH only)                                                                                                 |
| `SUBSCRIPTION_NOT_FOUND`           | Referenced subscription not found (PATCH only)                                                                                                      |
| `TOO_MANY_DELIVERIES`              | Maximum 10 deliveries allowed per order                                                                                                             |
| `TOO_MANY_DISPUTES`                | Maximum 10 disputes allowed per order                                                                                                               |
| `TOO_MANY_ITEMS`                   | Maximum 10 items allowed per order                                                                                                                  |
| `TOO_MANY_REFUNDS`                 | Maximum 10 refunds allowed per order                                                                                                                |
| `TOO_MANY_SUBSCRIPTIONS`           | Maximum 10 subscriptions allowed per order                                                                                                          |
| `TOO_MANY_TRANSACTIONS`            | Maximum 10 transactions allowed per order                                                                                                           |

***

## Response format

The API uses **partial success** semantics for per-order processing: valid orders are created even if other orders in the same batch fail. Each order is processed independently, and failures do not cause a rollback of successfully created orders.

This behaviour applies after request schema validation. Schema-level validation failures return a full-request `422` response and skip per-order processing.

The response includes:

* `created`: Number of orders successfully created
* `failed`: Number of orders that failed validation
* `results`: Array of successfully created orders
* `errors`: Array of error details for failed orders

### Success response

```json
{
  "created": 2,
  "failed": 0,
  "results": [
    {
      "id": "ord_abc123",
      "reference_id": "order-001",
      "type": "COMPLETE",
      "organisation_id": "org_xyz",
      "integration_id": "int_123",
      "created_at": "2024-01-15T10:30:00Z",
      "transactions": [...],
      "items": [...],
      ...,
      "links": [
        {
          "rel": "self",
          "uri": "/v1/orders/ord_abc123"
        }
      ]
    }
  ],
  "errors": []
}
```

### Error response

```json
{
  "created": 1,
  "failed": 1,
  "results": [...],
  "errors": [
    {
      "index": 1,
      "reference_id": "order-002",
      "code": "DUPLICATE_ORDER",
      "message": "Order with reference_id 'order-002' already exists for this integration",
      "field": "reference_id"
    }
  ]
}
```

### Rate-limited response (429)

Returned when you exceed 100 requests per minute for the endpoint.

```json
{
  "errors": [
    {
      "code": "RATE_LIMITED",
      "message": "Too many requests"
    }
  ]
}
```

### Canonical order object

Successful `GET /v1/orders`, `GET /v1/orders/{order_id}`, `POST /v1/orders`, and `PATCH /v1/orders/{order_id}` responses all use the same canonical order object shape.

Each order object includes:

* Core fields: `id`, `reference_id`, `type`, `organisation_id`, `integration_id`, `created_at`
* Any documented order, customer, device, and merchant fields that are present for that order
* Nested arrays: `transactions`, `deliveries`, `items`, `refunds`, `subscriptions`, `disputes`

Each nested object includes its own server-generated `id` plus the documented fields for that resource.

***

## PATCH /v1/orders/{order\_id} - Update an Order

Partially update an existing order and its nested objects.

**Authentication:** Bearer token via API key with `orders:update` ability.

**URL parameters:**

* `order_id` (required): The order ID to update

***

### Request body

All fields are optional. Only provided fields will be updated.

| Field                            | Type   | Description                                |
| -------------------------------- | ------ | ------------------------------------------ |
| `order_status`                   | enum   | New order status                           |
| `order_status_other_description` | string | Required when `order_status` is `OTHER`    |
| `order_communications`           | string | Updated communications history             |
| `deliveries`                     | array  | Deliveries to update (by reference\_id)    |
| `subscriptions`                  | array  | Subscriptions to update (by reference\_id) |
| `refunds`                        | array  | Refunds to create/update                   |
| `disputes`                       | array  | Disputes to create/update                  |

***

### Delivery updates

Update deliveries that belong to the order. Each delivery is identified by `reference_id`.

**Important:**

* Only deliveries belonging to this order can be updated
* Delivery type (`DIGITAL` or `PHYSICAL`) cannot be changed after creation
* Fields are type-constrained: digital fields only work on `DIGITAL` deliveries, physical fields only work on `PHYSICAL` deliveries

| Field                       | Type    | Description                                                                  |
| --------------------------- | ------- | ---------------------------------------------------------------------------- |
| `reference_id`              | string  | **Required.** Delivery to update                                             |
| `digital_*` fields          | various | For DIGITAL deliveries only                                                  |
| `physical_*` fields         | various | For PHYSICAL deliveries only                                                 |
| `physical_shipping_address` | object  | Full address object. If provided, it must satisfy the shared address schema. |

If you include `physical_shipping_address` in a PATCH request, send a complete valid address object rather than a partial address fragment.

**Example 9: Update physical delivery status**

```json
{
  "deliveries": [
    {
      "reference_id": "dlv-001",
      "physical_shipping_status": "DELIVERED",
      "physical_shipping_datetime_delivered": "2024-01-18T14:30:00Z"
    }
  ]
}
```

***

### Subscription updates

Update existing subscriptions by `reference_id`. Only subscriptions that already exist can be updated - to create new subscriptions, use the POST endpoint when creating an order.

| Field                     | Type    | Required | Description                                   |
| ------------------------- | ------- | -------- | --------------------------------------------- |
| `reference_id`            | string  | Yes      | Subscription identifier                       |
| `interval`                | enum    | No       | `DAY`, `WEEK`, `MONTH`, `YEAR`                |
| `interval_price_in_cents` | integer | No       | Price per interval                            |
| `interval_currency`       | string  | No       | ISO 4217 currency code                        |
| `status`                  | enum    | No       | `ACTIVE`, `CANCELLED`, `TRIALING`, `PAST_DUE` |
| `display_name`            | string  | No       | Display name (up to 255 chars)                |
| Other fields              | various | No       | See subscription fields in POST documentation |

**Example 10: Update subscription status**

```json
{
  "subscriptions": [
    {
      "reference_id": "sub-001",
      "status": "CANCELLED"
    }
  ]
}
```

***

### Refund updates

Create new refunds or update existing refunds. For existing refunds, `amount_in_cents` and `currency` are immutable, while `status`, `original_transaction_reference_id`, and `refund_datetime` can be updated.

| Field                               | Type     | Required | Description                              |
| ----------------------------------- | -------- | -------- | ---------------------------------------- |
| `reference_id`                      | string   | Yes      | Refund identifier                        |
| `amount_in_cents`                   | integer  | New only | Refund amount (immutable on existing)    |
| `currency`                          | string   | New only | Currency code (immutable on existing)    |
| `status`                            | enum     | Yes      | `PENDING`, `SUCCEEDED`, `FAILED`         |
| `original_transaction_reference_id` | string   | No       | Related transaction                      |
| `refund_datetime`                   | datetime | No       | When the refund was processed (ISO 8601) |

**Example 11: Create new refund**

```json
{
  "refunds": [
    {
      "reference_id": "ref-001",
      "amount_in_cents": 5000,
      "currency": "USD",
      "status": "SUCCEEDED"
    }
  ]
}
```

**Example 12: Update existing refund status**

```json
{
  "refunds": [
    {
      "reference_id": "ref-001",
      "status": "SUCCEEDED"
    }
  ]
}
```

***

### Dispute updates

Create new disputes or update existing disputes. For existing disputes, `amount_in_cents`, `currency`, and `type` are immutable, while `stage`, `status`, `network_reason_code`, `is_rapid_dispute_resolution`, `evidence_due_by`, `payment_method_type`, and `card_brand` can be updated.

| Field             | Type    | Required | Description                                     |
| ----------------- | ------- | -------- | ----------------------------------------------- |
| `reference_id`    | string  | Yes      | Dispute identifier                              |
| `amount_in_cents` | integer | New only | Dispute amount (immutable on existing)          |
| `currency`        | string  | New only | Currency code (immutable on existing)           |
| `type`            | enum    | New only | `INQUIRY`, `CHARGEBACK` (immutable on existing) |
| `stage`           | enum    | Yes      | `1ST_CHARGEBACK`, `2ND_CHARGEBACK`              |
| `status`          | enum    | Yes      | `OPEN`, `UNDER_REVIEW`, `WON`, `LOST`           |
| Other fields      | various | No       | See dispute fields in POST documentation        |

**Example 13: Update dispute status**

```json
{
  "disputes": [
    {
      "reference_id": "disp-001",
      "stage": "1ST_CHARGEBACK",
      "status": "WON"
    }
  ]
}
```

***

### PATCH error codes

| Code                               | Description                                                                                                                            |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `DELIVERY_NOT_FOUND`               | Referenced delivery not found in this order                                                                                            |
| `DUPLICATE_DELIVERY_REFERENCE`     | Duplicate `reference_id` in deliveries array                                                                                           |
| `DUPLICATE_DISPUTE_REFERENCE`      | Duplicate `reference_id` in disputes array                                                                                             |
| `DUPLICATE_REFUND_REFERENCE`       | Duplicate `reference_id` in refunds array                                                                                              |
| `DUPLICATE_SUBSCRIPTION_REFERENCE` | Duplicate `reference_id` in subscriptions array                                                                                        |
| `IMMUTABLE_FIELD`                  | Attempting to modify immutable fields on existing refund/dispute records (`amount_in_cents`, `currency`, and for disputes also `type`) |
| `INVALID_FIELD_FOR_TYPE`           | Updating wrong type fields on delivery (e.g., physical fields on digital delivery)                                                     |
| `MISSING_FIELD`                    | New refund/dispute missing required fields                                                                                             |
| `SUBSCRIPTION_NOT_FOUND`           | Referenced subscription not found                                                                                                      |
| `TOO_MANY_DELIVERIES`              | Maximum 10 deliveries allowed per order                                                                                                |
| `TOO_MANY_DISPUTES`                | Maximum 10 disputes allowed per order (including existing + new)                                                                       |
| `TOO_MANY_REFUNDS`                 | Maximum 10 refunds allowed per order (including existing + new)                                                                        |
| `TOO_MANY_SUBSCRIPTIONS`           | Maximum 10 subscriptions allowed per order                                                                                             |

***

### PATCH response

Returns the updated order with all nested objects.

**Success Response (200)**

```json
{
  "id": "ord_abc123",
  "reference_id": "order-001",
  "type": "COMPLETE",
  "organisation_id": "org_xyz",
  "integration_id": "int_123",
  "created_at": "2024-01-15T10:30:00Z",
  "order_status": "CLOSED_COMPLETE",
  "transactions": [...],
  "deliveries": [...],
  "items": [...],
  "refunds": [...],
  "subscriptions": [...],
  "disputes": [...],
  "links": [
    {
      "rel": "self",
      "uri": "/v1/orders/ord_abc123"
    }
  ]
}
```

**Error Response (422)**

```json
{
  "errors": [
    {
      "code": "IMMUTABLE_FIELD",
      "message": "Cannot modify amount_in_cents on existing refund"
    }
  ]
}
```

**Not Found Response (404)**

```json
{
  "detail": "Order with ID ord_nonexistent not found"
}
```


# Simulations

Generate test enrolment states, test alerts, and test scheme notices for TEST mode organisations.

**Base URL:** `https://api.chargebackstop.com/v1/simulate/`

**Authentication:** Bearer token via API key.

Required abilities:

* `simulations:enrollments` for `PATCH /v1/simulate/enrolments/{enrolment_id}`
* `simulations:alerts` for `POST /v1/simulate/alerts`
* `simulations:lookups` for `POST /v1/simulate/lookups`
* `simulations:scheme_notices` for `POST /v1/simulate/scheme-notices`

**Access scope model:**

* Organisation-level keys can simulate data for their own organisation only.
* Admin partner-group keys can simulate data across all organisations for their partner.
* Non-admin partner-group keys can simulate data only for organisations assigned to their group.

Typical simulation flow:

{% stepper %}
{% step %}

#### Create an enrolment

Create an enrolment with `/v2/enrolments`.
{% endstep %}

{% step %}

#### Enable the enrolment for simulation

Set that enrolment to `ENABLED` with:

`PATCH /v1/simulate/enrolments/{enrolment_id}`
{% endstep %}

{% step %}

#### Generate a simulated alert

Generate a simulated alert with `POST /v1/simulate/alerts`.
{% endstep %}

{% step %}

#### Generate a simulated lookup

Generate a simulated lookup with `POST /v1/simulate/lookups`.
{% endstep %}

{% step %}

#### Generate a simulated scheme notice

Generate a simulated scheme notice with `POST /v1/simulate/scheme-notices`.
{% endstep %}
{% endstepper %}

***

## PATCH /v1/simulate/enrolments/{enrolment\_id} - update enrolment simulation status

Updates an enrolment status for simulation use.

**API level:** Organisation-level and partner-level\
**Authentication:** `simulations:enrollments`

### URL parameters

| Parameter      | Type   | Description            |
| -------------- | ------ | ---------------------- |
| `enrolment_id` | string | Enrolment ID to update |

### Request body

| Field    | Type   | Required | Description                                     |
| -------- | ------ | -------- | ----------------------------------------------- |
| `status` | string | Yes      | New enrolment status: `ENABLED` or `UNENROLLED` |

### Important behaviour

* This endpoint is only available for TEST mode organisations.
* The enrolment must be accessible to the authenticated API key.
* Although the path is under `/v1`, the response body is the v2 enrolment object, so it includes `ethoca_alert.descriptors[].status`.
* For `ETHOCA_ALERT` enrolments, setting `status` to `ENABLED` activates configured descriptors for simulation matching. Every descriptor moves to `status: "ACTIVE"` in the response.
* The change emits a single `enrolment.updated` webhook whose `previous_attributes` holds both the previous enrolment `status` and the previous descriptors.

### Example 1 request

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

### Example 1 response

```json
{
  "id": "enrl_abc456",
  "organisation_id": "org_test123",
  "merchant_ids": ["mrch_abc123"],
  "type": "ETHOCA_ALERT",
  "status": "ENABLED",
  "ethoca_alert": {
    "descriptors": [
      {
        "descriptor": "TESTMERCHANT.COM",
        "match_type": "EXACT_MATCH",
        "status": "ACTIVE"
      }
    ]
  },
  "verifi_rdr": null,
  "created_at": "2026-02-10T12:00:00Z",
  "updated_at": "2026-02-16T08:15:00Z",
  "note": null,
  "links": [
    {
      "rel": "self",
      "uri": "/v2/enrolments/enrl_abc456"
    }
  ]
}
```

***

## POST /v1/simulate/alerts - create simulated alert

Creates a simulated network alert.

**API level:** Organisation-level and partner-level\
**Authentication:** `simulations:alerts`

### Request body

| Field                                   | Type    | Required | Description                                                                                               |
| --------------------------------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------- |
| `organisation_id`                       | string  | Yes      | Organisation ID. Must be in TEST mode and accessible to the key                                           |
| `enrolment_id`                          | string  | Yes      | Enrolment ID that belongs to `organisation_id` and is an alert enrolment (`ETHOCA_ALERT` or `VERIFI_RDR`) |
| `status`                                | string  | No       | Alert status: `ACTION_REQUIRED`, `RESOLVED`, `INVALID`. Verifi RDR only supports `RESOLVED` or `INVALID`  |
| `transaction_refund_outcome`            | string  | No       | Refund outcome: `REFUNDED` or `NOT_REFUNDED`. Only valid with `status=RESOLVED`                           |
| `card_scheme`                           | string  | No       | Card scheme. Use any supported scheme value, for example `VISA`, `MASTERCARD`, `AMEX`                     |
| `amount_in_cents`                       | integer | No       | Transaction amount in cents. Must be a positive integer                                                   |
| `currency_code`                         | string  | No       | ISO 4217 currency code, for example `USD`, `EUR`, `GBP`                                                   |
| `transaction_acquirer_reference_number` | string  | No       | ARN. Must be exactly 23 characters                                                                        |
| `transaction_authorisation_code`        | string  | No       | Authorisation code. Must be exactly 6 characters                                                          |

### Important behaviour

* This endpoint is only available for TEST mode organisations.
* The enrolment must be an alert enrolment: `ETHOCA_ALERT` or `VERIFI_RDR`.
* If `status` is omitted, the simulator chooses a valid status.
* If `transaction_refund_outcome` is provided without `status`, the created alert is set to `RESOLVED`.
* Simulated alerts trigger the same `alert.created` event flow as non-simulated alerts.

### Example 2 request

```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_abc456",
    "status": "RESOLVED",
    "transaction_refund_outcome": "REFUNDED",
    "card_scheme": "VISA",
    "amount_in_cents": 5000,
    "currency_code": "USD"
  }'
```

### Example 2 response

```json
{
  "id": "netalrt_xyz789",
  "alert_network_id": "SIM123456",
  "organisation_id": "org_test123",
  "merchant_id": "mrch_abc123",
  "enrolment_id": "enrl_abc456",
  "enrolment_type": "ETHOCA_ALERT",
  "status": "RESOLVED",
  "chargeback_reason_code": null,
  "transaction_amount_in_cents": 5000,
  "transaction_currency_code": "USD",
  "transaction_authorised_at": "2026-02-10T10:30:00Z",
  "action_required_deadline": null,
  "transaction_authorisation_code": "ABC123",
  "transaction_acquirer_reference_number": "12345678901234567890123",
  "transaction_statement_descriptor": "TESTMERCHANT.COM",
  "transaction_card_bin": "400000",
  "transaction_card_last4": "1234",
  "transaction_card_scheme": "VISA",
  "transaction_card_issuer": "Bank of America",
  "transaction_refund_outcome": "REFUNDED",
  "transaction_network_id": null,
  "subscription_cancel_outcome": null,
  "note": null,
  "created_at": "2026-02-16T08:20:00Z",
  "updated_at": "2026-02-16T08:20:00Z",
  "integration_id": null,
  "integration_transaction_id": null
}
```

***

## POST /v1/simulate/lookups - create simulated lookup

Creates a simulated lookup.

**API level:** Organisation-level and partner-level\
**Authentication:** `simulations:lookups`

### Request body

| Field                                   | Type    | Required | Description                                                                                                                     |
| --------------------------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `organisation_id`                       | string  | Yes      | Organisation ID. Must be in TEST mode and accessible to the key                                                                 |
| `enrollment_id`                         | string  | Yes      | Enrollment ID that belongs to `organisation_id` and is a lookup enrolment (`ETHOCA_CONSUMER_CLARITY` or `VERIFI_ORDER_INSIGHT`) |
| `type`                                  | string  | Yes      | Lookup type: `ETHOCA_CONSUMER_CLARITY`, `ETHOCA_FIRST_PARTY_TRUST`, `VERIFI_ORDER_INSIGHT`, `VERIFI_COMPELLING_EVIDENCE_3`      |
| `parent_lookup_id`                      | string  | No       | Existing lookup ID in the same organisation                                                                                     |
| `lookup_status`                         | string  | No       | Lookup status: `PENDING`, `SUCCEEDED`, `FAILED`, `TIMEOUT`                                                                      |
| `deflection_status`                     | string  | No       | Deflection status: `NOT_ATTEMPTED`, `PENDING`, `SUCCEEDED`, `FAILED`                                                            |
| `card_scheme`                           | string  | No       | Card scheme. Use any supported scheme value, for example `VISA`, `MASTERCARD`, `AMEX`                                           |
| `amount_in_cents`                       | integer | No       | Transaction amount in cents. Must be a positive integer                                                                         |
| `currency_code`                         | string  | No       | ISO 4217 currency code, for example `USD`, `EUR`, `GBP`                                                                         |
| `transaction_acquirer_reference_number` | string  | No       | ARN. Must be exactly 23 characters                                                                                              |
| `transaction_authorisation_code`        | string  | No       | Authorisation code. Must be exactly 6 characters                                                                                |

### Important behaviour

* This endpoint is only available for TEST mode organisations.
* The enrollment must be a lookup enrolment: `ETHOCA_CONSUMER_CLARITY` or `VERIFI_ORDER_INSIGHT`.
* If `lookup_status` is omitted, the simulator uses `SUCCEEDED`.
* If `deflection_status` is omitted, the simulator uses `NOT_ATTEMPTED`.
* The simulator also stores a standard response summary on the lookup record for downstream UI and internal testing use.
* Simulated lookups trigger the same `lookup.created` event flow as non-simulated lookups.
* Simulated lookups do not create billing events or request-response records.

### Example 4 request

```bash
curl -X POST "https://api.chargebackstop.com/v1/simulate/lookups" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "organisation_id": "org_test123",
    "enrollment_id": "enrl_abc456",
    "type": "ETHOCA_CONSUMER_CLARITY",
    "lookup_status": "SUCCEEDED",
    "deflection_status": "NOT_ATTEMPTED",
    "card_scheme": "VISA",
    "amount_in_cents": 5000,
    "currency_code": "USD",
    "transaction_acquirer_reference_number": "12345678901234567890123",
    "transaction_authorisation_code": "ABC123"
  }'
```

### Example 4 response

```json
{
  "id": "lkup_xyz789",
  "organisation_id": "org_test123",
  "enrollment_id": "enrl_abc456",
  "type": "ETHOCA_CONSUMER_CLARITY",
  "parent_lookup_id": null,
  "lookup_status": "SUCCEEDED",
  "deflection_status": "NOT_ATTEMPTED",
  "transaction_card_bin": "411111",
  "transaction_card_last4": "1234",
  "transaction_arn": "12345678901234567890123",
  "transaction_auth_code": "ABC123",
  "transaction_amount": 5000,
  "transaction_currency": "USD",
  "transaction_date": "2026-02-16T08:20:00Z",
  "transaction_statement_descriptor": "ETHOCA TEST LOOKUP",
  "transaction_network_id": null,
  "integration_id": null,
  "integration_transaction_id": null,
  "created_at": "2026-02-16T08:20:00Z",
  "updated_at": "2026-02-16T08:20:00Z"
}
```

***

## POST /v1/simulate/scheme-notices - create simulated scheme notice

Creates a simulated scheme notice.

**API level:** Organisation-level and partner-level\
**Authentication:** `simulations:scheme_notices`

### Request body

| Field                         | Type     | Required | Description                                                     |
| ----------------------------- | -------- | -------- | --------------------------------------------------------------- |
| `organisation_id`             | string   | Yes      | Organisation ID. Must be in TEST mode and accessible to the key |
| `scheme_notice_type`          | string   | No       | Scheme notice type: `TC15`, `TC40`, or `SAFE`                   |
| `transaction_amount_in_cents` | integer  | No       | Transaction amount in cents. Must be a positive integer         |
| `transaction_currency_code`   | string   | No       | ISO 4217 currency code, for example `USD`, `EUR`, `GBP`         |
| `reported_at`                 | datetime | No       | Reported timestamp in ISO 8601 format                           |

### Important behaviour

* This endpoint is only available for TEST mode organisations.
* If `scheme_notice_type` is omitted, the simulator chooses a valid notice type.
* `TC15` simulations use Verifi dispute-notice semantics: `notice_type=DISPUTE_NOTICE`, `scheme=VISA`.
* Simulated scheme notices trigger the same `scheme_notice.created` event flow as non-simulated scheme notices.
* The response uses the standard public scheme-notice payload, including `fraud_reported_at`.

### Example 3 request

```bash
curl -X POST "https://api.chargebackstop.com/v1/simulate/scheme-notices" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "organisation_id": "org_test123",
    "scheme_notice_type": "TC15",
    "transaction_amount_in_cents": 5000,
    "transaction_currency_code": "USD",
    "reported_at": "2026-02-16T08:20:00Z"
  }'
```

### Example 3 response

```json
{
  "id": "schntc_xyz789",
  "organisation_id": "org_test123",
  "merchant_id": null,
  "scheme_notice_type": "TC15",
  "notice_type": "DISPUTE_NOTICE",
  "scheme": "VISA",
  "is_revoked": false,
  "notice_revoked_at": null,
  "fraud_reported_at": "2026-02-16T08:20:00Z",
  "transaction_amount_in_cents": 5000,
  "transaction_currency_code": "USD",
  "transaction_card_bin": "411111",
  "transaction_card_last4": "1111",
  "transaction_merchant_name": "ECOM-STUFF.COM",
  "transaction_merchant_id": "003500935000093",
  "transaction_acquirer_reference_number": "12345678901234567890123",
  "transaction_authorisation_code": "ABC123",
  "transaction_original_date": "2026-02-01T08:20:00Z",
  "transaction_purchase_date": null,
  "fraud_type": null,
  "fraud_dispute_eligible": null,
  "created_at": "2026-02-16T08:20:00Z",
  "updated_at": "2026-02-16T08:20:00Z"
}
```

***

## Common error responses

Error responses use this shape:

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

### Example 4: 401 Unauthorised

```json
{
  "errors": [
    {
      "code": "UNAUTHORISED",
      "message": "Unauthorised"
    }
  ]
}
```

### Example 5: 403 Forbidden

```json
{
  "errors": [
    {
      "code": "FORBIDDEN",
      "message": "Forbidden"
    }
  ]
}
```

### Example 6: 404 Not found

```json
{
  "errors": [
    {
      "code": "NOT_FOUND",
      "message": "Not found"
    }
  ]
}
```

### Example 7: 422 Unprocessable Entity (simulation not allowed)

```json
{
  "errors": [
    {
      "code": "SIMULATION_NOT_ALLOWED",
      "message": "Alert simulation is only allowed for TEST mode organisations. Production Org is in LIVE mode."
    }
  ]
}
```

### Example 8: 422 Unprocessable Entity (simulation validation error)

```json
{
  "errors": [
    {
      "code": "SIMULATION_VALIDATION_ERROR",
      "message": "Invalid currency_code 'INVALID'. Must be a valid ISO 4217 code."
    }
  ]
}
```

### Example 9: 422 Unprocessable Entity (scheme notice validation error)

```json
{
  "errors": [
    {
      "code": "SIMULATION_VALIDATION_ERROR",
      "message": "Invalid scheme_notice_type 'INVALID'. Must be TC15, TC40, or SAFE."
    }
  ]
}
```

### Example 10: 422 Unprocessable Entity (invalid enum value)

```json
{
  "errors": [
    {
      "code": "VALIDATION_ENUM",
      "message": "Validation error at ('body', 'data', 'status'). Input should be 'ENABLED' or 'UNENROLLED'."
    }
  ]
}
```

### Additional reference: 500 Server error

```json
{
  "errors": [
    {
      "code": "SERVER_ERROR",
      "message": "Server error"
    }
  ]
}
```


# Scheme notices

List and retrieve scheme notices for accessible organisations.

**Base URL:** `https://api.chargebackstop.com/v1/scheme-notices/`

**Authentication:** Bearer token via API key.

Required abilities:

* `scheme_notices:read` for GET endpoints

**Access scope model:**

* Organisation-level keys can access scheme notices for their own organisation only.
* Admin partner-group keys can access scheme notices across all organisations for their partner.
* Non-admin partner-group keys can access scheme notices only for organisations assigned to their group.

***

## GET /v1/scheme-notices - list scheme notices

Returns a paginated list of scheme notices accessible to the authenticated key.

**API level:** Organisation-level and partner-level\
**Authentication:** `scheme_notices:read`

### Query parameters

| Parameter            | Type     | Description                                                                       |
| -------------------- | -------- | --------------------------------------------------------------------------------- |
| `scheme_notice_type` | string   | Filter by notice type: `TC15`, `TC40`, `SAFE`                                     |
| `organisation_id`    | string   | Filter by organisation ID                                                         |
| `merchant_id`        | string   | Filter by merchant ID                                                             |
| `reported_at_gte`    | datetime | Include scheme notices reported at or after this timestamp (ISO 8601, inclusive)  |
| `reported_at_lte`    | datetime | Include scheme notices reported at or before this timestamp (ISO 8601, inclusive) |
| `sort`               | string   | Sort field: `-reported_at` (default), `reported_at`                               |
| `limit`              | integer  | Number of results per page                                                        |
| `offset`             | integer  | Number of results to skip                                                         |

### Important behaviour

* If both `reported_at_gte` and `reported_at_lte` are sent, `reported_at_gte` must be less than or equal to `reported_at_lte`.
* Default sort order is newest reported notice first (`-reported_at`).

### Example 1 request

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

### Example 1 response

```json
{
  "items": [
    {
      "id": "schntc_abc123",
      "organisation_id": "org_xyz456",
      "merchant_id": "mrch_abc123",
      "scheme_notice_type": "TC40",
      "notice_type": "FRAUD_NOTICE",
      "scheme": "VISA",
      "is_revoked": false,
      "notice_revoked_at": null,
      "fraud_reported_at": "2026-02-11T10:30:00Z",
      "transaction_amount_in_cents": 4999,
      "transaction_currency_code": "USD",
      "transaction_card_bin": "424242",
      "transaction_card_last4": "4242",
      "transaction_merchant_name": "EXAMPLE STORE",
      "transaction_merchant_id": "merchant-123",
      "transaction_acquirer_reference_number": "74027012345678901234567",
      "transaction_authorisation_code": "ABC123",
      "transaction_original_date": "2026-02-01T10:30:00Z",
      "transaction_purchase_date": "2026-02-01T10:30:00Z",
      "fraud_type": "CARD_NOT_PRESENT",
      "fraud_dispute_eligible": true,
      "created_at": "2026-02-11T10:31:00Z",
      "updated_at": "2026-02-11T10:31:00Z",
      "parent_notice_id": null,
      "enrolment_id": "enrl_abc123",
      "matched_transaction_entity_id": "dpe_abc123",
      "links": [
        {
          "rel": "self",
          "uri": "/v1/scheme-notices/schntc_abc123"
        }
      ]
    }
  ],
  "count": 1
}
```

***

## GET /v1/scheme-notices/{scheme\_notice\_id} - get scheme notice by ID

Returns one scheme notice if it is visible to the authenticated key.

**API level:** Organisation-level and partner-level\
**Authentication:** `scheme_notices:read`

### URL parameters

| Parameter          | Type   | Description      |
| ------------------ | ------ | ---------------- |
| `scheme_notice_id` | string | Scheme notice ID |

### Important behaviour

* The same access rules as list scheme notices apply.
* List items and individual scheme notice responses use the same item schema.
* Connector `raw_data` is not returned by this API.
* Non-existent scheme notices return `404 Not found`.

### Example 2 request

```bash
curl -X GET "https://api.chargebackstop.com/v1/scheme-notices/schntc_abc123" \
  -H "Authorization: Bearer <api_key>"
```

### Example 2 response

```json
{
  "id": "schntc_abc123",
  "organisation_id": "org_xyz456",
  "merchant_id": "mrch_abc123",
  "scheme_notice_type": "TC40",
  "notice_type": "FRAUD_NOTICE",
  "scheme": "VISA",
  "is_revoked": false,
  "notice_revoked_at": null,
  "fraud_reported_at": "2026-02-11T10:30:00Z",
  "transaction_amount_in_cents": 4999,
  "transaction_currency_code": "USD",
  "transaction_card_bin": "424242",
  "transaction_card_last4": "4242",
  "transaction_merchant_name": "EXAMPLE STORE",
  "transaction_merchant_id": "merchant-123",
  "transaction_acquirer_reference_number": "74027012345678901234567",
  "transaction_authorisation_code": "ABC123",
  "transaction_original_date": "2026-02-01T10:30:00Z",
  "transaction_purchase_date": "2026-02-01T10:30:00Z",
  "fraud_type": "CARD_NOT_PRESENT",
  "fraud_dispute_eligible": true,
  "created_at": "2026-02-11T10:31:00Z",
  "updated_at": "2026-02-11T10:31:00Z",
  "parent_notice_id": null,
  "enrolment_id": "enrl_abc123",
  "matched_transaction_entity_id": "dpe_abc123",
  "links": [
    {
      "rel": "self",
      "uri": "/v1/scheme-notices/schntc_abc123"
    }
  ]
}
```

***

## Common error responses

Error responses use this shape:

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

### Example 3 response - 401 unauthorised

```json
{
  "errors": [
    {
      "code": "UNAUTHORISED",
      "message": "Unauthorised"
    }
  ]
}
```

### Example 4 response - 403 forbidden

```json
{
  "errors": [
    {
      "code": "FORBIDDEN",
      "message": "Forbidden"
    }
  ]
}
```

### Example 5 response - 404 not found

```json
{
  "errors": [
    {
      "code": "NOT_FOUND",
      "message": "Not found"
    }
  ]
}
```

### Example 6 response - 422 invalid date range

```json
{
  "errors": [
    {
      "code": "INVALID_DATE_RANGE",
      "message": "reported_at_gte cannot be greater than reported_at_lte"
    }
  ]
}
```


# Lookups

List and retrieve digital receipt lookups for accessible organisations.

**Base URL:** `https://api.chargebackstop.com/v1/lookups/`

**Authentication:** Bearer token via API key.

Required abilities:

* `lookups:read` for GET endpoints

Access scope model:

* Organisation-level keys can access lookups for their own organisation only.
* Admin partner-group keys can access lookups across all organisations for their partner.
* Non-admin partner-group keys can access lookups only for organisations assigned to their group.

## GET /v1/lookups - list lookups

Returns a paginated list of lookup records accessible to the authenticated key.

**API level:** Organisation-level and partner-level\
**Authentication:** `lookups:read`

### Query parameters

| Parameter           | Type     | Description                                                                    |
| ------------------- | -------- | ------------------------------------------------------------------------------ |
| `organisation_id`   | string   | Filter by organisation ID                                                      |
| `enrollment_id`     | string   | Filter by enrollment ID                                                        |
| `type`              | string   | Filter by lookup type                                                          |
| `parent_lookup_id`  | string   | Filter by parent lookup ID                                                     |
| `lookup_status`     | string   | Filter by lookup status: `PENDING`, `SUCCEEDED`, `FAILED`, `TIMEOUT`           |
| `deflection_status` | string   | Filter by deflection status: `NOT_ATTEMPTED`, `PENDING`, `SUCCEEDED`, `FAILED` |
| `created_at_gte`    | datetime | Include lookups created at or after this timestamp (ISO 8601, inclusive)       |
| `created_at_lte`    | datetime | Include lookups created at or before this timestamp (ISO 8601, inclusive)      |
| `sort`              | string   | Sort field: `-created_at` (default) or `created_at`                            |
| `limit`             | integer  | Number of results per page                                                     |
| `offset`            | integer  | Number of results to skip                                                      |

### Important behaviour

* If both `created_at_gte` and `created_at_lte` are sent, `created_at_gte` must be less than or equal to `created_at_lte`.
* Default sort order is newest lookup first (`-created_at`).
* Responses use the same lookup object shape as lookup webhooks.

### Example request

```bash
curl -X GET "https://api.chargebackstop.com/v1/lookups/?organisation_id=org_xyz456&lookup_status=SUCCEEDED&sort=-created_at&limit=20&offset=0" \
  -H "Authorization: Bearer <api_key>"
```

### Example response

```json
{
  "items": [
    {
      "id": "lkup_abc123",
      "organisation_id": "org_xyz456",
      "enrollment_id": "enrl_def456",
      "type": "ETHOCA_CONSUMER_CLARITY",
      "parent_lookup_id": null,
      "lookup_status": "SUCCEEDED",
      "deflection_status": "NOT_ATTEMPTED",
      "transaction_card_bin": "424242",
      "transaction_card_last4": "4242",
      "transaction_arn": "74027012345678901234567",
      "transaction_auth_code": "AUTH123",
      "transaction_amount": 4999,
      "transaction_currency": "USD",
      "transaction_date": "2026-02-10T10:30:00Z",
      "transaction_statement_descriptor": "EXAMPLE STORE",
      "transaction_network_id": "network_123",
      "integration_id": "int_abc123",
      "integration_transaction_id": "txn_123",
      "created_at": "2026-02-11T10:30:00Z",
      "updated_at": "2026-02-11T10:30:00Z",
      "links": [
        {
          "rel": "self",
          "uri": "/v1/lookups/lkup_abc123"
        }
      ]
    }
  ],
  "count": 1
}
```

***

## GET /v1/lookups/{lookup\_id} - get lookup by ID

Returns one lookup if it is visible to the authenticated key.

**API level:** Organisation-level and partner-level\
**Authentication:** `lookups:read`

### URL parameters

| Parameter   | Type   | Description |
| ----------- | ------ | ----------- |
| `lookup_id` | string | Lookup ID   |

### Important behaviour

* The same access rules as list lookups apply.
* Non-existent or inaccessible lookups return `404 Not found`.

### Example request

```bash
curl -X GET "https://api.chargebackstop.com/v1/lookups/lkup_abc123" \
  -H "Authorization: Bearer <api_key>"
```

### Example response

```json
{
  "id": "lkup_abc123",
  "organisation_id": "org_xyz456",
  "enrollment_id": "enrl_def456",
  "type": "ETHOCA_CONSUMER_CLARITY",
  "parent_lookup_id": null,
  "lookup_status": "SUCCEEDED",
  "deflection_status": "NOT_ATTEMPTED",
  "transaction_card_bin": "424242",
  "transaction_card_last4": "4242",
  "transaction_arn": "74027012345678901234567",
  "transaction_auth_code": "AUTH123",
  "transaction_amount": 4999,
  "transaction_currency": "USD",
  "transaction_date": "2026-02-10T10:30:00Z",
  "transaction_statement_descriptor": "EXAMPLE STORE",
  "transaction_network_id": "network_123",
  "integration_id": "int_abc123",
  "integration_transaction_id": "txn_123",
  "created_at": "2026-02-11T10:30:00Z",
  "updated_at": "2026-02-11T10:30:00Z",
  "links": [
    {
      "rel": "self",
      "uri": "/v1/lookups/lkup_abc123"
    }
  ]
}
```

***

## Error examples

<details>

<summary>Example: unauthorised</summary>

```json
{
  "errors": [
    {
      "code": "UNAUTHORISED",
      "message": "Unauthorised"
    }
  ]
}
```

</details>

<details>

<summary>Example: forbidden</summary>

```json
{
  "errors": [
    {
      "code": "FORBIDDEN",
      "message": "Forbidden"
    }
  ]
}
```

</details>

<details>

<summary>Example: not found</summary>

```json
{
  "errors": [
    {
      "code": "NOT_FOUND",
      "message": "Not found"
    }
  ]
}
```

</details>

<details>

<summary>Example: invalid date range</summary>

```json
{
  "errors": [
    {
      "code": "INVALID_DATE_RANGE",
      "message": "created_at_gte cannot be greater than created_at_lte"
    }
  ]
}
```

</details>


# Rulesets - developer preview

Create and manage resolution rulesets and their child rules.

**Base URL:** `https://api.chargebackstop.com/v1/rulesets/`

**Authentication:** Bearer token via API key.

Required abilities:

* `rulesets:read` for GET endpoints
* `rulesets:write` for POST, PATCH, and DELETE endpoints

**Access scope model:**

* Organisation-level keys can access rulesets for their own organisation only.
* Admin partner-group keys can access rulesets across all organisations for their partner.
* Non-admin partner-group keys can access rulesets only for organisations assigned to their group.

***

## Rule types

A rule is defined by its `type` and a `parameters` object whose shape depends on the type. Each ruleset can contain at most one rule of each type. The same rule types apply to embedded rules on `POST /v1/rulesets` and to the nested `POST` / `PATCH` rule endpoints.

### AMOUNT

Matches the transaction amount against a threshold.

| Field             | Type    | Description                                                                                         |
| ----------------- | ------- | --------------------------------------------------------------------------------------------------- |
| `operator`        | string  | `GREATER_THAN`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN`, `LESS_THAN_OR_EQUAL`, `EQUAL`, or `NOT_EQUAL` |
| `currency_code`   | string  | Three-letter currency code. Must be `USD`.                                                          |
| `amount_in_cents` | integer | Non-negative amount in cents.                                                                       |

{% hint style="info" %}
Only `USD` is accepted as the rule currency. If a transaction is in a different currency, its amount is automatically converted to USD before the rule is evaluated, so the rule still applies to non-USD transactions.
{% endhint %}

Example `parameters`:

```json
{
  "operator": "GREATER_THAN",
  "currency_code": "USD",
  "amount_in_cents": 5000
}
```

### DESCRIPTOR

Matches the transaction's statement descriptor against one or more entries.

| Field         | Type           | Description                    |
| ------------- | -------------- | ------------------------------ |
| `descriptors` | array\[object] | One or more descriptor entries |

Each descriptor entry:

| Field        | Type   | Description                         |
| ------------ | ------ | ----------------------------------- |
| `value`      | string | Descriptor string to match against. |
| `match_type` | string | `STARTS_WITH` or `EXACT_MATCH`      |

Example `parameters`:

```json
{
  "descriptors": [
    {"value": "NETFLIX", "match_type": "STARTS_WITH"},
    {"value": "SPOTIFY", "match_type": "EXACT_MATCH"}
  ]
}
```

***

## POST /v1/rulesets - create ruleset

Creates one ruleset and optionally creates child rules within the same request.

**API level:** Organisation-level and partner-level\
**Authentication:** `rulesets:write`

### Request body

| Field             | Type           | Description                                                                                     |
| ----------------- | -------------- | ----------------------------------------------------------------------------------------------- |
| `organisation_id` | string         | Organisation ID                                                                                 |
| `enrolment_ids`   | array\[string] | One or more enrolment IDs                                                                       |
| `outcome`         | string         | `REFUND`, `CANCEL`, `REFUND_AND_CANCEL`, or `ACCEPT_DISPUTE`                                    |
| `join_operator`   | string         | `AND` or `OR`                                                                                   |
| `rules`           | array\[object] | Optional initial rules. Each entry has `type` and `parameters` — see [Rule types](#rule-types). |

{% hint style="info" %}
All selected enrolments must be accessible and must share the same enrolment type. For example, you cannot combine Ethoca and Verifi RDR enrolments in the same ruleset. If you need the same rules to apply to both, create one ruleset per enrolment type.

Only one rule of each type is allowed per ruleset.

Amount rules must use `USD`.

If any embedded rule fails validation, the entire ruleset creation is rolled back.
{% endhint %}

### Example 1 request

```bash
curl -X POST "https://api.chargebackstop.com/v1/rulesets/" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "organisation_id": "org_xyz456",
    "enrolment_ids": ["enrl_abc123", "enrl_def456"],
    "outcome": "REFUND",
    "join_operator": "AND",
    "rules": [
      {
        "type": "DESCRIPTOR",
        "parameters": {
          "descriptors": [
            {"value": "NETFLIX", "match_type": "STARTS_WITH"},
            {"value": "SPOTIFY", "match_type": "STARTS_WITH"}
          ]
        }
      },
      {
        "type": "AMOUNT",
        "parameters": {
          "operator": "GREATER_THAN",
          "currency_code": "USD",
          "amount_in_cents": 5000
        }
      }
    ]
  }'
```

### Example 1 response

```json
{
  "id": "rset_abc123",
  "organisation_id": "org_xyz456",
  "enrolment_ids": ["enrl_abc123", "enrl_def456"],
  "outcome": "REFUND",
  "join_operator": "AND",
  "rules": [
    {
      "id": "resrule_desc123",
      "type": "DESCRIPTOR",
      "parameters": {
        "descriptors": [
          {"value": "NETFLIX", "match_type": "STARTS_WITH"},
          {"value": "SPOTIFY", "match_type": "STARTS_WITH"}
        ]
      },
      "created_at": "2026-05-05T10:00:00Z",
      "updated_at": "2026-05-05T10:00:00Z"
    },
    {
      "id": "resrule_amt123",
      "type": "AMOUNT",
      "parameters": {
        "operator": "GREATER_THAN",
        "currency_code": "USD",
        "amount_in_cents": 5000
      },
      "created_at": "2026-05-05T10:00:00Z",
      "updated_at": "2026-05-05T10:00:00Z"
    }
  ],
  "created_at": "2026-05-05T10:00:00Z",
  "updated_at": "2026-05-05T10:00:00Z"
}
```

***

## GET /v1/rulesets - list rulesets

Returns a paginated list of accessible rulesets.

**API level:** Organisation-level and partner-level\
**Authentication:** `rulesets:read`

### Query parameters

| Parameter         | Type    | Description                                                           |
| ----------------- | ------- | --------------------------------------------------------------------- |
| `organisation_id` | string  | Filter by organisation ID                                             |
| `sort`            | string  | `-created_at` (default), `created_at`, `-updated_at`, or `updated_at` |
| `limit`           | integer | Number of results per page                                            |
| `offset`          | integer | Number of results to skip                                             |

### Example 2 request

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

### Example 2 response

```json
{
  "items": [
    {
      "id": "rset_def456",
      "organisation_id": "org_xyz456",
      "enrolment_ids": ["enrl_abc123", "enrl_def456"],
      "outcome": "REFUND",
      "join_operator": "AND",
      "rules": [],
      "created_at": "2026-05-05T10:00:00Z",
      "updated_at": "2026-05-05T10:00:00Z"
    }
  ],
  "count": 1
}
```

***

## GET /v1/rulesets/{ruleset\_id} - get ruleset by ID

Returns one accessible ruleset.

**API level:** Organisation-level and partner-level\
**Authentication:** `rulesets:read`

### Example 3 request

```bash
curl -X GET "https://api.chargebackstop.com/v1/rulesets/rset_abc123" \
  -H "Authorization: Bearer <api_key>"
```

### Example 3 response

```json
{
  "id": "rset_abc123",
  "organisation_id": "org_xyz456",
  "enrolment_ids": ["enrl_abc123"],
  "outcome": "ACCEPT_DISPUTE",
  "join_operator": "AND",
  "rules": [
    {
      "id": "resrule_amt123",
      "type": "AMOUNT",
      "parameters": {
        "operator": "GREATER_THAN",
        "currency_code": "USD",
        "amount_in_cents": 5000
      },
      "created_at": "2026-05-05T10:00:00Z",
      "updated_at": "2026-05-05T10:00:00Z"
    }
  ],
  "created_at": "2026-05-05T10:00:00Z",
  "updated_at": "2026-05-05T10:00:00Z"
}
```

***

## PATCH /v1/rulesets/{ruleset\_id} - update ruleset

Updates one accessible ruleset.

**API level:** Organisation-level and partner-level\
**Authentication:** `rulesets:write`

### Request body

| Field           | Type           | Description                                                            |
| --------------- | -------------- | ---------------------------------------------------------------------- |
| `enrolment_ids` | array\[string] | Optional. One or more enrolment IDs                                    |
| `outcome`       | string         | Optional. `REFUND`, `CANCEL`, `REFUND_AND_CANCEL`, or `ACCEPT_DISPUTE` |
| `join_operator` | string         | Optional. `AND` or `OR`                                                |

{% hint style="info" %}
Send only the mutable fields you want to change. Omitted fields keep their existing values.

`rules` is not accepted on this endpoint. Update child rules through the nested rule endpoints instead.

All selected enrolments must be accessible and must share the same enrolment type. For example, you cannot combine Ethoca and Verifi RDR enrolments in the same ruleset.
{% endhint %}

### Example 4 request

```bash
curl -X PATCH "https://api.chargebackstop.com/v1/rulesets/rset_abc123" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "enrolment_ids": ["enrl_abc123", "enrl_def456"],
    "outcome": "CANCEL",
    "join_operator": "OR"
  }'
```

### Example 4 response

```json
{
  "id": "rset_abc123",
  "organisation_id": "org_xyz456",
  "enrolment_ids": ["enrl_abc123", "enrl_def456"],
  "outcome": "CANCEL",
  "join_operator": "OR",
  "rules": [],
  "created_at": "2026-05-05T10:00:00Z",
  "updated_at": "2026-05-05T10:00:00Z"
}
```

***

## DELETE /v1/rulesets/{ruleset\_id} - delete ruleset

Deletes one accessible ruleset and its child rules.

**API level:** Organisation-level and partner-level\
**Authentication:** `rulesets:write`

{% hint style="info" %}
The response body is empty on success.
{% endhint %}

### Example 5 request

```bash
curl -X DELETE "https://api.chargebackstop.com/v1/rulesets/rset_abc123" \
  -H "Authorization: Bearer <api_key>"
```

### Example 5 response

```http
204 No Content
```

***

## POST /v1/rulesets/{ruleset\_id}/rules - add rule

Adds one rule to an existing accessible ruleset.

**API level:** Organisation-level and partner-level\
**Authentication:** `rulesets:write`

### Request body

| Field        | Type   | Description                                                           |
| ------------ | ------ | --------------------------------------------------------------------- |
| `type`       | string | `AMOUNT` or `DESCRIPTOR`. See [Rule types](#rule-types).              |
| `parameters` | object | Parameters for the selected rule type. See [Rule types](#rule-types). |

{% hint style="info" %}
Only one rule of each type is allowed per ruleset.

Amount rules must use `USD`.
{% endhint %}

### Example 6 request

```bash
curl -X POST "https://api.chargebackstop.com/v1/rulesets/rset_abc123/rules" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "AMOUNT",
    "parameters": {
      "operator": "GREATER_THAN",
      "currency_code": "USD",
      "amount_in_cents": 5000
    }
  }'
```

### Example 6 response

```json
{
  "id": "resrule_amt123",
  "type": "AMOUNT",
  "parameters": {
    "operator": "GREATER_THAN",
    "currency_code": "USD",
    "amount_in_cents": 5000
  },
  "created_at": "2026-05-05T10:00:00Z",
  "updated_at": "2026-05-05T10:00:00Z"
}
```

***

## GET /v1/rulesets/{ruleset\_id}/rules/{rule\_id} - get rule by ID

Returns one accessible rule from an accessible ruleset.

**API level:** Organisation-level and partner-level\
**Authentication:** `rulesets:read`

### Example 7 request

```bash
curl -X GET "https://api.chargebackstop.com/v1/rulesets/rset_abc123/rules/resrule_amt123" \
  -H "Authorization: Bearer <api_key>"
```

### Example 7 response

```json
{
  "id": "resrule_amt123",
  "type": "AMOUNT",
  "parameters": {
    "operator": "GREATER_THAN",
    "currency_code": "USD",
    "amount_in_cents": 5000
  },
  "created_at": "2026-05-05T10:00:00Z",
  "updated_at": "2026-05-05T10:00:00Z"
}
```

***

## PATCH /v1/rulesets/{ruleset\_id}/rules/{rule\_id} - update rule

Updates one accessible rule within an accessible ruleset.

**API level:** Organisation-level and partner-level\
**Authentication:** `rulesets:write`

### Request body

| Field        | Type   | Description                                                           |
| ------------ | ------ | --------------------------------------------------------------------- |
| `type`       | string | Must match the existing rule type. See [Rule types](#rule-types).     |
| `parameters` | object | Parameters for the selected rule type. See [Rule types](#rule-types). |

{% hint style="info" %}
Rule type cannot be changed.

Amount rules must use `USD`.
{% endhint %}

### Example 8 request

```bash
curl -X PATCH "https://api.chargebackstop.com/v1/rulesets/rset_abc123/rules/resrule_amt123" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "AMOUNT",
    "parameters": {
      "operator": "GREATER_THAN",
      "currency_code": "USD",
      "amount_in_cents": 7500
    }
  }'
```

### Example 8 response

```json
{
  "id": "resrule_amt123",
  "type": "AMOUNT",
  "parameters": {
    "operator": "GREATER_THAN",
    "currency_code": "USD",
    "amount_in_cents": 7500
  },
  "created_at": "2026-05-05T10:00:00Z",
  "updated_at": "2026-05-05T10:00:00Z"
}
```

***

## DELETE /v1/rulesets/{ruleset\_id}/rules/{rule\_id} - delete rule

Deletes one accessible rule from an accessible ruleset.

**API level:** Organisation-level and partner-level\
**Authentication:** `rulesets:write`

{% hint style="info" %}
The response body is empty on success.
{% endhint %}

### Example 9 request

```bash
curl -X DELETE "https://api.chargebackstop.com/v1/rulesets/rset_abc123/rules/resrule_amt123" \
  -H "Authorization: Bearer <api_key>"
```

### Example 9 response

```http
204 No Content
```

***

## Error examples

### Example 10: forbidden

```json
{
  "errors": [
    {
      "code": "FORBIDDEN",
      "message": "Forbidden"
    }
  ]
}
```

### Example 11: not found

```json
{
  "errors": [
    {
      "code": "NOT_FOUND",
      "message": "Not found"
    }
  ]
}
```

### Example 12: invalid currency

```json
{
  "errors": [
    {
      "code": "INVALID_CURRENCY_CODE",
      "message": "Only USD is allowed.",
      "field": "currency_code"
    }
  ]
}
```

### Example 13: rules not allowed on ruleset update

```json
{
  "errors": [
    {
      "code": "RULES_NOT_ALLOWED_ON_RULESET_UPDATE",
      "message": "Rules cannot be updated via this endpoint. Use PATCH /v1/rulesets/{ruleset_id}/rules/{rule_id} instead.",
      "field": "rules"
    }
  ]
}
```


# Alerts API for organisations

Reference for ChargebackStop REST API.

### Capabilities

ChargebackStop REST API for organisations allows:

* Listing all alerts in the organisation
* Getting individual alert by its unique ID
* Actioning an alert

The API uses JSON both as an input and output data format.

### Actioning alerts

Every alert with `ACTION_REQUIRED` status is expected to be actioned by the user. The deadline for actioning an alert is represented by the `action_required_deadline` field in the alert object, however we recommend sending your desired action as soon as possible. After actioning, the alert status is moved into `RESOLVED`, and cannot be changed anymore.

Behind the scenes, actioning the alert updates the alert provider (e.g. Ethoca) with the outcome. This allows them to stop the dispute if the user's action was to pro-actively refund the transaction, or send the dispute through if you chose to fight it.

For actions that refund the transaction, if your ChargebackStop merchant account is integrated with a payments provider (e.g. Stripe), we will automatically refund the payment. You can also choose to cancel the subscription associated with the payment

The update alert endpoint takes `action` parameter. It can be one of four values:

* `REFUND`
* `CANCEL`
* `REFUND_AND_CANCEL`
* `ACCEPT_DISPUTE`

Let's explore how each of these values works.

#### REFUND

All of the below actions are performed:

1. We let the alert provider know that you refunded the alert.
2. If your Merchant account is integrated with a payment provider, we will automatically refund the payment.

#### CANCEL

All of the below actions are performed:

1. We let the alert provider know that you did not refund the alert.
2. If your Merchant account is integrated with a payment provider and there's a subscription associated with the alert, we will automatically cancel it.

#### REFUND AND CANCEL

All of the below actions are performed:

1. We let the alert provider know that you refunded the alert.
2. If your Merchant account is integrated with a payment provider, we will automatically refund the payment.
3. If your Merchant account is integrated with a payment provider and there's a subscription associated with the alert, we will automatically cancel it.

#### ACCEPT\_DISPUTE

All of the below actions are performed:

1. We let the alert provider know that you did not refund the alert.

### API keys

In order to create an organisation-level API key:

1. Open your ChargebackStop dashboard
2. Select "Settings" from the left-hand menu
3. Select "API Keys" from the tabs at the top\
   \
   ![](/files/RQA6J0s35J9dbHse3Aow)<br>
4. Click the "Create API key" button
   1. In the newly opened form enter your key's name. It's only used for your reference.
   2. Add expiration date if you want the key to stop working at some point in the future. If you want to use the key indefinitely, please leave this field empty.
5. Click "Create key"
6. You will see your API key in the green success box right under the tabs. You will only be able to see it once, so make sure to copy it (including the `cbs_`prefix) and save it securely. If you loose your key, you will need to revoke it and create a new one.\
   ![](/files/yc7sT0uCMyzhZ5DCla9F)<br>


# Partner Integration Guides

Welcome to the ChargebackStop for Partners platform.

We suggest starting with the “Integrating with ChargebackStop” page below, which guides you through the API and webhook documentation, as well as the terminology we use at ChargebackStop.

<table data-view="cards"><thead><tr><th>Title</th><th data-card-target data-type="content-ref">Target</th></tr></thead><tbody><tr><td>Integrating with ChargebackStop</td><td><a href="/pages/18280c80d9013fbc442179360e19af836a1fef00">/pages/18280c80d9013fbc442179360e19af836a1fef00</a></td></tr><tr><td>API</td><td><a href="/pages/abb98af3015d844d5b9a5093d10de5204fd29de1">/pages/abb98af3015d844d5b9a5093d10de5204fd29de1</a></td></tr><tr><td>Webhooks</td><td><a href="/pages/dd110928374624f7d4feef061b718c8aadd409cd">/pages/dd110928374624f7d4feef061b718c8aadd409cd</a></td></tr></tbody></table>


# Integrating with ChargebackStop

ChargebackStop for Partners has extensive API and webhook capabilities.

## Terminology

To understand how our API and platform work, it helps to learn these terms and their relationships.

### Partner

Your partner account.

* In production, you’ll typically only have one.

### Organisation

An organisation within your partner account.

* We recommend setting separate organisations for all your customers.

### Membership

A membership of a user within an organisation.

* This enables your customers to log in to their organisation.
* If your integration does not include your customers logging into ChargebackStop platform in any form (including whitelabel solutions), you will not need to create any memberships.

### Merchant

A merchant account within an organisation.

* If your customer has multiple merchant accounts with payment processors, create a separate merchant account for each one.
* When creating a new merchant, we strongly recommend passing `external_id` as their MID, CAID, or provider account ID. It helps you differentiate merchant accounts later on.

### Integration

A connection with a data source (e.g. payment processor such as Stripe, Adyen, Shopify, etc.), or a custom orders integration if you decide to push orders to ChargebackStop using the [Orders API](/developer/api-documentation/orders).

* Integrations allow ChargebackStop to match incoming alerts to transactions, and (for payment-processor-based integrations) to automatically refund or cancel payments associated with alerts.

### Enrolment

An enrolment with a dispute prevention service (Ethoca or Verifi RDR).

* Creating an enrolment in the ChargebackStop platform starts the process of enrolling the merchant account with the selected service.
* Enrolment can span across multiple merchant accounts, e.g. when a single descriptor enrolled with Ethoca is used across multiple accounts.

### Alert

An individual pre-dispute alert.

* An alert is always tied to an enrolment, as it’s the enrolment that’s the source of the alert.

### Representment

A dispute case ingested via your payment processor.

* Only available if you choose to work with ChargebackStop for representment.

## API

The ChargebackStop platform has an API that supports creating and managing all of the entities above.

You can read more about how to consume the API and onboard customers in our [API integration guide](/developer/partner-integration-guide/api-integration-guide) page.

## Webhooks

The ChargebackStop platform supports webhooks for the following events:

* `alert.created`
* `alert.updated`
* `enrolment.created`
* `enrolment.updated`
* `representment.created`
* `representment.updated`
* `scheme_notice.created`
* `scheme_notice.updated`

Full webhook documentation is available here: [Webhooks](/developer/partner-integration-guide/webhooks)

Webhooks can be tested using our Postman collection: <https://www.postman.com/chargebackstop/chargebackstop/collection/2yu40q8/chargebackstop-webhooks>. You can read more on the page linked above.


# API integration guide

Full API documentation is available here: <https://api.chargebackstop.com/v1/docs>.

V2 API documentation (enrolments endpoint only for now): <https://api.chargebackstop.com/v2/docs>.

## Onboarding a new customer into your partner account

{% stepper %}
{% step %}

#### Create an organisation

Use the `/v1/organisations` endpoint. Save the organisation ID for later use in the memberships and merchants APIs.

Sample request:

```bash
curl -X POST "https://api.chargebackstop.com/v1/organisations/" \
  -H "Authorization: Bearer <partner_api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Fitness"
  }'
```

Sample response:

```json
{
  "id": "org_abc123",
  "name": "Acme Fitness",
  "mode": "LIVE",
  "created_at": "2026-02-17T10:00:00Z",
  "updated_at": "2026-02-17T10:00:00Z"
}
```

Save `organisation_id = org_abc123`. You will use this in memberships, merchants, and integrations requests.
{% endstep %}

{% step %}

#### Add your customer users to their organisation

Use the `/v1/memberships` endpoint.

You can skip this step if you do not intend for your users to log in to ChargebackStop.

Sample request:

```bash
curl -X POST "https://api.chargebackstop.com/v1/memberships/" \
  -H "Authorization: Bearer <partner_api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "organisation_id": "org_abc123",
    "first_name": "Casey",
    "last_name": "Jordan",
    "email": "casey.jordan@acmefitness.com",
    "role": "ADMIN",
    "is_alert_action_required_email_enabled": true,
    "is_alert_resolved_email_enabled": true,
    "is_alert_dismissed_email_enabled": true,
    "is_evidence_requested_email_enabled": true
  }'
```

Sample response:

```json
{
  "id": "mem_abc123",
  "organisation_id": "org_abc123",
  "user_id": "usr_abc123",
  "first_name": "Casey",
  "last_name": "Jordan",
  "email": "casey.jordan@acmefitness.com",
  "role": "ADMIN",
  "is_alert_action_required_email_enabled": true,
  "is_alert_resolved_email_enabled": true,
  "is_alert_dismissed_email_enabled": true,
  "is_evidence_requested_email_enabled": true,
  "created_at": "2026-02-17T10:01:00Z",
  "updated_at": "2026-02-17T10:01:00Z"
}
```

This creates the customer user and links them to the organisation.
{% endstep %}

{% step %}

#### Add their merchant accounts into the newly created organisation

Use the `/v1/merchants` endpoint. Save the merchant account IDs for later use in the integrations and enrolments APIs.

Sample request:

```bash
curl -X POST "https://api.chargebackstop.com/v1/merchants/" \
  -H "Authorization: Bearer <partner_api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "organisation_id": "org_abc123",
    "name": "Acme Fitness Main Store",
    "type": "GENERIC",
    "external_id": "acct_1ABCDEF2345678"
  }'
```

Sample response:

```json
{
  "id": "mrch_abc123",
  "organisation_id": "org_abc123",
  "name": "Acme Fitness Main Store",
  "type": "GENERIC",
  "external_id": "acct_1ABCDEF2345678",
  "default_representment_service_type": null,
  "created_at": "2026-02-17T10:02:00Z",
  "updated_at": "2026-02-17T10:02:00Z"
}
```

Save `merchant_id = mrch_abc123`. You will use this in integrations and enrolments requests.
{% endstep %}

{% step %}

#### If you intend to use the Orders API, create an integration

Use the `/v1/integrations` endpoint. Save the integration ID for later use in the orders API.

Sample request:

```bash
curl -X POST "https://api.chargebackstop.com/v1/integrations/" \
  -H "Authorization: Bearer <partner_api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "organisation_id": "org_abc123",
    "name": "Acme Fitness Orders",
    "type": "CUSTOM_ORDERS",
    "status": "ENABLED",
    "merchant_ids": ["mrch_abc123"]
  }'
```

Sample response:

```json
{
  "id": "int_abc123",
  "organisation_id": "org_abc123",
  "name": "Acme Fitness Orders",
  "type": "CUSTOM_ORDERS",
  "status": "ENABLED",
  "merchant_ids": ["mrch_abc123"]
}
```

Save `integration_id = int_abc123`. You will use this for orders submissions.
{% endstep %}

{% step %}

#### If you wish to enrol the customer with Ethoca or Verifi RDR, create enrolments

Use the `/v2/enrolments` endpoint. Verifi RDR and Ethoca enrolments are separate; create both if the customer wants both services.

See [Enrolments API docs](/developer/api-documentation/enrolments).

**Create Ethoca enrolment**

Sample request:

```bash
curl -X POST "https://api.chargebackstop.com/v2/enrolments/" \
  -H "Authorization: Bearer <partner_api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "merchant_ids": ["mrch_abc123"],
    "type": "ETHOCA_ALERT",
    "ethoca_alert": {
      "descriptors": [
        {
          "descriptor": "ACMEFIT",
          "match_type": "STARTS_WITH"
        }
      ]
    }
  }'
```

Sample response:

```json
{
  "id": "enrl_ethoca123",
  "organisation_id": "org_abc123",
  "merchant_ids": ["mrch_abc123"],
  "type": "ETHOCA_ALERT",
  "status": "PENDING",
  "ethoca_alert": {
    "descriptors": [
      {
        "descriptor": "ACMEFIT",
        "match_type": "STARTS_WITH",
        "status": "PENDING"
      }
    ]
  },
  "created_at": "2026-02-17T10:03:00Z",
  "updated_at": "2026-02-17T10:03:00Z",
  "note": null
}
```

Save `ethoca_enrolment_id = enrl_ethoca123` if you need to track this enrolment status.

**Create Verifi RDR enrolment**

Sample request:

```bash
curl -X POST "https://api.chargebackstop.com/v2/enrolments/" \
  -H "Authorization: Bearer <partner_api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "merchant_ids": ["mrch_abc123"],
    "type": "VERIFI_RDR",
    "verifi_rdr": {
      "bin": "424242",
      "caid": "ABC1234567"
    }
  }'
```

You must provide either both `bin` and `caid`, or at least one ARN in the `arns` array. For example, to enrol using ARNs instead:

```json
{
  "merchant_ids": ["mrch_abc123"],
  "type": "VERIFI_RDR",
  "verifi_rdr": {
    "arns": ["74027012345678901234567"]
  }
}
```

Sample response:

```json
{
  "id": "enrl_verifi123",
  "organisation_id": "org_abc123",
  "merchant_ids": ["mrch_abc123"],
  "type": "VERIFI_RDR",
  "status": "PENDING",
  "verifi_rdr": {
    "bin": "424242",
    "caid": "ABC1234567"
  },
  "created_at": "2026-02-17T10:04:00Z",
  "updated_at": "2026-02-17T10:04:00Z",
  "note": null
}
```

Save `verifi_enrolment_id = enrl_verifi123` if you need to track this enrolment status.

Note: Once the enrolment is enabled (2-3 business days for Ethoca, and up to 5-14 business days for Verifi RDR), your customer will start receiving pre-dispute alerts in their organisation.
{% endstep %}

{% step %}

#### Optional: Configure resolution rules for the new enrolments

Use the `/v1/rulesets` endpoint to control what happens when a pre-dispute alert matches a transaction. A ruleset is scoped to one or more enrolments of the **same type** — you cannot combine Ethoca and Verifi RDR enrolments in the same ruleset. If you need the same logic for both, create one ruleset per enrolment type.

See [Rulesets API docs](/developer/api-documentation/rulesets-developer-preview).

**Verifi RDR**

By default, every Verifi RDR alert results in a refund of the matching transaction. Verifi RDR alerts are resolved at the network level, so they arrive already resolved and have no `ACTION_REQUIRED` state — they are not actionable after the fact.

If you want to limit which transactions are refunded — for example, never auto-refund transactions above a certain amount — create a ruleset with an `ACCEPT_DISPUTE` outcome and add rules describing the transactions that should bypass the default refund.

Sample request — accept dispute (skip auto-refund) for any Verifi RDR transaction above $50:

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

**Ethoca**

Resolution rules for Ethoca alerts only make sense if you have a payment processor integration in place (e.g. Stripe, Adyen) and ChargebackStop is matching incoming alerts to transactions. Ethoca alerts are actioned by issuing a refund through the original payment processor, so without a matched transaction there is nothing for a rule to act on.

Once matching is in place, a ruleset can auto-action matched Ethoca alerts that meet your criteria, so the user does not have to handle them manually.

Sample request — automatically refund and cancel matched Ethoca alerts whose descriptor starts with `ACMEFIT`:

```bash
curl -X POST "https://api.chargebackstop.com/v1/rulesets/" \
  -H "Authorization: Bearer <partner_api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "organisation_id": "org_abc123",
    "enrolment_ids": ["enrl_ethoca123"],
    "outcome": "REFUND_AND_CANCEL",
    "join_operator": "AND",
    "rules": [
      {
        "type": "DESCRIPTOR",
        "parameters": {
          "descriptors": [
            {"value": "ACMEFIT", "match_type": "STARTS_WITH"}
          ]
        }
      }
    ]
  }'
```

{% endstep %}

{% step %}

#### Start pushing orders to us using the Orders API

Use the `/v1/orders` endpoint.

Send a COMPLETE order that satisfies alert matching requirements. COMPLETE orders can be sent directly to your `CUSTOM_ORDERS` integration.

Sample request:

```bash
curl -X POST "https://api.chargebackstop.com/v1/orders/" \
  -H "Authorization: Bearer <partner_api_key>" \
  -H "Content-Type: application/json" \
  -d '[
    {
      "type": "COMPLETE",
      "organisation_id": "org_abc123",
      "integration_id": "int_abc123",
      "reference_id": "order-10001",
      "order_datetime": "2026-02-17T09:59:00Z",
      "order_number": "AF-10001",
      "order_subtotal_amount_in_cents": 4900,
      "order_currency": "USD",
      "order_total_amount_in_cents": 4900,
      "order_status": "CLOSED_COMPLETE",
      "customer_email": "casey.jordan@acmefitness.com",
      "transactions": [
        {
          "reference_id": "txn-10001",
          "amount_in_cents": 4900,
          "currency": "USD",
          "payment_method_type": "CARD",
          "authorisation_status": "SETTLED",
          "payment_method_reference_id": "pi_3QJ2ABCD12345678",
          "authorised_at": "2026-02-17T09:59:05Z",
          "payment_method_card_brand": "VISA",
          "payment_method_card_last_4": "4242",
          "acquirer_reference_number": "74027012345678901234567"
        }
      ]
    }
  ]'
```

Sample response:

```json
{
  "created": 1,
  "failed": 0,
  "results": [
    {
      "id": "ord_abc123",
      "reference_id": "order-10001",
      "type": "COMPLETE",
      "organisation_id": "org_abc123",
      "integration_id": "int_abc123",
      "created_at": "2026-02-17T10:05:00Z",
      "transactions": [
        {
          "reference_id": "txn-10001",
          "amount_in_cents": 4900,
          "currency": "USD",
          "payment_method_type": "CARD",
          "authorisation_status": "SETTLED",
          "payment_method_card_brand": "VISA",
          "payment_method_card_last_4": "4242",
          "acquirer_reference_number": "74027012345678901234567"
        }
      ]
    }
  ],
  "errors": []
}
```

This creates a COMPLETE order with the core card transaction fields required for alert matching.
{% endstep %}

{% step %}

#### Optional: Run this flow with a Node.js script

If you want to run the same onboarding flow end-to-end and print all created IDs automatically, use:

```bash
node onboarding-flow.js
```

Before running it, save the script below as `onboarding-flow.js` and set `API_KEY`.

Full onboarding flow script (Node.js):

```javascript
/*
 * Simple onboarding flow example for partner API keys.
 *
 * Update the variables in the CONFIG section, then run:
 * node onboarding-flow.js
 */

// -----------------------------------------------------------------------------
// CONFIG
// -----------------------------------------------------------------------------
const API_BASE_URL = "https://api.chargebackstop.com";
const API_KEY = "REPLACE_WITH_PARTNER_API_KEY";

const MEMBER_USER_EMAIL = "test@example.com";
const MEMBER_FIRST_NAME = "Casey";
const MEMBER_LAST_NAME = "Jordan";

const ORGANISATION_NAME = "Acme Fitness";
const MERCHANT_NAME = "Acme Fitness Main Store";
const MERCHANT_TYPE = "GENERIC";
const MERCHANT_EXTERNAL_ID = "acct_1ABCDEF2345678";

const INTEGRATION_NAME = "Acme Fitness Orders";

const ORDER_REFERENCE_ID = "order-10001";
const ORDER_NUMBER = "AF-10001";
const ORDER_CUSTOMER_EMAIL = MEMBER_USER_EMAIL;
const ORDER_TRANSACTION_REFERENCE_ID = "txn-10001";

// -----------------------------------------------------------------------------
// HELPERS
// -----------------------------------------------------------------------------
function fail(message, details) {
  console.error(`\nERROR: ${message}`);
  if (details !== undefined) {
    console.error(JSON.stringify(details, null, 2));
  }
  process.exit(1);
}

function logStep(stepNumber, title) {
  console.log(`\n[Step ${stepNumber}] ${title}`);
}

function printJson(label, data) {
  console.log(`${label}:`);
  console.log(JSON.stringify(data, null, 2));
}

async function apiRequest({ method, path, body }) {
  const response = await fetch(`${API_BASE_URL}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
    body: body ? JSON.stringify(body) : undefined,
  });

  const rawText = await response.text();
  let data = null;

  if (rawText.length > 0) {
    try {
      data = JSON.parse(rawText);
    } catch (error) {
      data = { raw: rawText };
    }
  }

  if (!response.ok) {
    fail(`Request failed: ${method} ${path} (${response.status})`, data);
  }

  return data;
}

// -----------------------------------------------------------------------------
// FLOW
// -----------------------------------------------------------------------------
async function main() {
  if (API_KEY.includes("REPLACE_WITH_PARTNER_API_KEY")) {
    fail("Set API_KEY at the top of this file before running the script.");
  }

  const createdIds = {
    organisation_id: null,
    membership_id: null,
    merchant_id: null,
    integration_id: null,
    ethoca_enrolment_id: null,
    verifi_enrolment_id: null,
    order_id: null,
  };

  logStep(1, "Create organisation");
  const organisation = await apiRequest({
    method: "POST",
    path: "/v1/organisations/",
    body: {
      name: ORGANISATION_NAME,
    },
  });
  createdIds.organisation_id = organisation.id;
  printJson("Response", organisation);
  console.log(`Saved organisation_id: ${createdIds.organisation_id}`);

  logStep(2, "Create membership");
  const membership = await apiRequest({
    method: "POST",
    path: "/v1/memberships/",
    body: {
      organisation_id: createdIds.organisation_id,
      first_name: MEMBER_FIRST_NAME,
      last_name: MEMBER_LAST_NAME,
      email: MEMBER_USER_EMAIL,
      role: "ADMIN",
      is_alert_action_required_email_enabled: true,
      is_alert_resolved_email_enabled: true,
      is_alert_dismissed_email_enabled: true,
      is_evidence_requested_email_enabled: true,
    },
  });
  createdIds.membership_id = membership.id;
  printJson("Response", membership);
  console.log(`Saved membership_id: ${createdIds.membership_id}`);

  logStep(3, "Create merchant");
  const merchant = await apiRequest({
    method: "POST",
    path: "/v1/merchants/",
    body: {
      organisation_id: createdIds.organisation_id,
      name: MERCHANT_NAME,
      type: MERCHANT_TYPE,
      external_id: MERCHANT_EXTERNAL_ID,
    },
  });
  createdIds.merchant_id = merchant.id;
  printJson("Response", merchant);
  console.log(`Saved merchant_id: ${createdIds.merchant_id}`);

  logStep(4, "Create custom orders integration");
  const integration = await apiRequest({
    method: "POST",
    path: "/v1/integrations/",
    body: {
      organisation_id: createdIds.organisation_id,
      name: INTEGRATION_NAME,
      type: "CUSTOM_ORDERS",
      status: "ENABLED",
      merchant_ids: [createdIds.merchant_id],
    },
  });
  createdIds.integration_id = integration.id;
  printJson("Response", integration);
  console.log(`Saved integration_id: ${createdIds.integration_id}`);

  logStep(5, "Create Ethoca enrolment");
  const ethocaEnrolment = await apiRequest({
    method: "POST",
    path: "/v2/enrolments/",
    body: {
      merchant_ids: [createdIds.merchant_id],
      type: "ETHOCA_ALERT",
      ethoca_alert: {
        descriptors: [
          {
            descriptor: "ACMEFIT",
            match_type: "STARTS_WITH",
          },
        ],
      },
    },
  });
  createdIds.ethoca_enrolment_id = ethocaEnrolment.id;
  printJson("Response", ethocaEnrolment);
  console.log(`Saved ethoca_enrolment_id: ${createdIds.ethoca_enrolment_id}`);

  logStep(6, "Create Verifi RDR enrolment");
  const verifiEnrolment = await apiRequest({
    method: "POST",
    path: "/v2/enrolments/",
    body: {
      merchant_ids: [createdIds.merchant_id],
      type: "VERIFI_RDR",
      verifi_rdr: {
        bin: "424242",
        caid: "ABC1234567",
      },
    },
  });
  createdIds.verifi_enrolment_id = verifiEnrolment.id;
  printJson("Response", verifiEnrolment);
  console.log(`Saved verifi_enrolment_id: ${createdIds.verifi_enrolment_id}`);

  logStep(7, "Submit COMPLETE order for alert matching");
  const orderResponse = await apiRequest({
    method: "POST",
    path: "/v1/orders/",
    body: [
      {
        type: "COMPLETE",
        organisation_id: createdIds.organisation_id,
        integration_id: createdIds.integration_id,
        reference_id: ORDER_REFERENCE_ID,
        order_datetime: "2026-02-17T09:59:00Z",
        order_number: ORDER_NUMBER,
        order_subtotal_amount_in_cents: 4900,
        order_currency: "USD",
        order_total_amount_in_cents: 4900,
        order_status: "CLOSED_COMPLETE",
        customer_email: ORDER_CUSTOMER_EMAIL,
        transactions: [
          {
            reference_id: ORDER_TRANSACTION_REFERENCE_ID,
            amount_in_cents: 4900,
            currency: "USD",
            payment_method_type: "CARD",
            authorisation_status: "SETTLED",
            payment_method_reference_id: "pi_3QJ2ABCD12345678",
            authorised_at: "2026-02-17T09:59:05Z",
            payment_method_card_brand: "VISA",
            payment_method_card_last_4: "4242",
            acquirer_reference_number: "74027012345678901234567",
          },
        ],
      },
    ],
  });
  printJson("Response", orderResponse);

  if (orderResponse.failed > 0) {
    console.log("\nOrder was not created. API returned validation errors.");
  } else if (orderResponse.results && orderResponse.results.length > 0) {
    createdIds.order_id = orderResponse.results[0].id;
  }

  console.log("\nDone. IDs to save for future requests:");
  printJson("Created IDs", createdIds);
}

main().catch((error) => {
  fail("Unexpected script error", {
    message: error.message,
    stack: error.stack,
  });
});
```

{% endstep %}
{% endstepper %}

## Actioning alerts

{% hint style="danger" %}
**Ethoca alerts in `ACTION_REQUIRED` status must be actioned.** Refunding the transaction alone is not enough — you must also action the alert so that Ethoca is notified and can stop the dispute. Unactioned alerts provide no value and the chargeback will proceed regardless of whether you refunded the customer. If you have resolution rules configured, matching alerts are resolved automatically. In rare cases, an alert may still appear as `ACTION_REQUIRED` if the system cannot match the alert to a transaction or cannot process the refund. Verifi RDR alerts are resolved automatically and do not need to be actioned.
{% endhint %}

Every Ethoca alert with `ACTION_REQUIRED` status is expected to be actioned by the user. The deadline is represented by the `action_required_deadline` field in the alert object. However, we recommend sending your chosen action as soon as possible. After actioning, the alert status moves to `RESOLVED` and can no longer be changed. Verifi RDR alerts arrive already resolved and are not actionable.

Behind the scenes, actioning an alert updates Ethoca with the outcome. This allows them to stop the dispute if the user proactively refunded the transaction, or let the dispute proceed if you chose to fight it.

For actions that refund the transaction, if the merchant account is integrated with a payment provider (e.g. Stripe), we automatically refund the payment. You can also choose to cancel the subscription associated with the payment.

The update alert endpoint (`PATCH /v1/alerts/{alert_id}`) takes the following parameters:

* `action` - the action to perform on the alert (see below)
* `note` - an optional customer-facing comment/note about the alert

The `action` parameter can be one of four values:

* `REFUND`
* `CANCEL`
* `REFUND_AND_CANCEL`
* `ACCEPT_DISPUTE`

Let's explore how each of these values works.

#### REFUND

* We let the alert provider know that you refunded the alert.
* If your merchant account is integrated with a payment provider, we automatically refund the payment.

#### CANCEL

* We let the alert provider know that you did not refund the alert.
* If your merchant account is integrated with a payment provider and there is a subscription associated with the alert, we automatically cancel it.

#### REFUND\_AND\_CANCEL

* We let the alert provider know that you refunded the alert.
* If your merchant account is integrated with a payment provider, we automatically refund the payment.
* If your merchant account is integrated with a payment provider and there is a subscription associated with the alert, we automatically cancel it.

#### ACCEPT\_DISPUTE

* We let the alert provider know that you did not refund the alert.

If you have not integrated ChargebackStop with any payment processing services, you will most likely use only `REFUND` and `ACCEPT_DISPUTE`.

This API is also available to your customers. They can generate API keys for their own organisations in the organisation dashboard and action alerts via API.

### Matching an alert to a transaction

If you enabled payment processor integrations in your organisations and enabled automatic matching, `GET /v1/alerts` responses include two additional fields:

* `integration_id` - ChargebackStop integration ID that contains the transaction matched to this alert
* `integration_transaction_id` - transaction ID matched to this alert in your payment processor. For Stripe, this looks like `pi_3SPJO4KRFSLReU4y04XJUvLN`. For other processors it varies.

## Resolution models: Ethoca vs Verifi RDR

Ethoca and Verifi RDR resolve alerts through fundamentally different mechanisms. Both flows are documented in detail in the sections above — use this contrast as a quick reference.

* **Ethoca (`ETHOCA_ALERT`)** — Alerts enter an `ACTION_REQUIRED` state with a deadline (`action_required_deadline`). You (or your customer) must call `PATCH /v1/alerts/{alert_id}` with one of `REFUND`, `CANCEL`, `REFUND_AND_CANCEL`, or `ACCEPT_DISPUTE` before the deadline. If no action is taken, the dispute proceeds. Resolution rulesets can auto-action matched alerts when a payment processor integration is in place, but the underlying mechanism is still the same `PATCH` call applied on the customer's behalf.
* **Verifi RDR (`VERIFI_RDR`)** — Alerts are resolved synchronously at the network level. Each incoming alert is evaluated against the enrolment's resolution ruleset and the chosen outcome is applied automatically. There is no `ACTION_REQUIRED` state, no deadline, and no `PATCH /v1/alerts/{alert_id}` call to make — the action API is Ethoca-only. With no ruleset configured, every RDR alert is refunded by default (see [step 6](#optional-configure-resolution-rules-for-the-new-enrolments) of the onboarding flow).

## Test organisation simulations

In TEST mode, you can use simulation endpoints to test webhooks and actioning alerts.

[Simulations API docs](/developer/api-documentation/simulations).


# Webhooks

## Event types

ChargebackStop currently supports ten types of webhook events:

* `alert.created`
* `alert.updated`
* `enrolment.created`
* `enrolment.updated`
* `representment.created`
* `representment.updated`
* `scheme_notice.created`
* `scheme_notice.updated`
* `lookup.created`
* `lookup.updated`

## Adding webhook endpoints

### In the partner dashboard

To add a webhook endpoint, go to the Partner settings page and click “Add Endpoint” in the section at the bottom of the page.

<figure><img src="/files/IyOo22JGgJfzIJT2ARGX" alt=""><figcaption></figcaption></figure>

In the modal window, enter the URL ChargebackStop should send events to, and select the event types you want to receive.

<figure><img src="/files/tR5TNKV0hWenVnzxCHoG" alt=""><figcaption></figcaption></figure>

Once the endpoint is saved, it appears in the table at the bottom of the page. You can use the eye icon to reveal the webhook secret that the ChargebackStop platform uses to sign webhooks.

<figure><img src="/files/eHvQKVOCQO6P7LOqckQP" alt=""><figcaption></figcaption></figure>

### In the organisation dashboard

Open your ChargebackStop dashboard, then select “Settings” from the left-hand menu. From the tabs at the top, pick “Webhooks”.

<figure><img src="/files/0461lBKoIp2c5a9D40Qg" alt=""><figcaption></figcaption></figure>

To add a webhook, click the “Add webhook” button and fill in the form with your endpoint URL and the events you want to receive.

Once the endpoint is saved, it appears in the table at the bottom of the page. You can use the eye icon to reveal the webhook secret that the ChargebackStop platform uses to sign webhooks.

<figure><img src="/files/t4BQdcn4DEJdWgr0sQWf" alt=""><figcaption></figcaption></figure>

## Delivery mechanism

### Request Details

* The payload will be a JSON object (see below for details).
* We include an `X-Signature` header, which is an HMAC-SHA512 hash of the timestamp and payload, allowing you to verify request authenticity. The signing secret is unique to your webhook subscription.
* We include an `X-Idempotency-Key` header with the unique delivery ID.
* We expect a successful HTTP response (2xx status code) within 20 seconds.

### Reliability and retries

* If the initial delivery attempt fails (e.g., due to a network error or a 4xx/5xx response from your server), we automatically retry delivery.
* Our retry strategy includes 5 attempts over approximately 2 days, with increasing delays between attempts: 1 minute, 5 minutes, 30 minutes, 2 hours, and finally 12 hours.
  * Retries for the same delivery keep the same X-Idempotency-Key value (the delivery ID). The header helps prevent processing duplicate deliveries. To ensure you process each event only once, rely on the event ID from the payload (`id`), which stays the same across retries of that event.
* If all retries are exhausted and the delivery remains unsuccessful, the delivery will be marked as FAILED.
* If multiple delivery attempts to a given endpoint start failing, the system will automatically disable the webhook endpoint due to poor health.

### Signature

To ensure webhook security and authenticity, we include a unique signature with each request. This signature allows you to verify that the webhook originated from us and that its payload was not tampered with in transit.

{% stepper %}
{% step %}
**How the X-Signature header is generated**

* Timestamp: We take the current Unix timestamp (the number of seconds since January 1, 1970 UTC). This timestamp is included directly in the signature header.
* Payload: This is the raw JSON body of the webhook request.
* Concatenation: We create a string by concatenating the timestamp, a period (.), and the UTF-8 decoded payload. For example: "{timestamp}.{payload\_string}".
* HMAC-SHA512: We then use your unique webhook secret (provided to you when you set up the webhook subscription) to create an HMAC of the concatenated string using the SHA512 hashing algorithm.
* Hex Digest: The resulting HMAC is encoded into a hexadecimal string.
* Formatted signature: Finally, the signature is formatted as `t={timestamp},v1={hex_digest}`.

Notes on the formatted signature:

* `t={timestamp}`: Unix timestamp when the signature was generated. You can compare this with your current time and discard webhooks that are too old to help prevent replay attacks. We recommend tolerating reasonable clock skew (e.g., a few minutes) between your servers and ours.
* `v1={hex_digest}`: Signature scheme version (currently `v1`) followed by the hexadecimal HMAC-SHA512 digest.
  {% endstep %}

{% step %}
**How to verify a received signature**

1. Verify the timestamp (`t=`): Extract the timestamp from the `X-Signature` header and compare it with your current server time. If the difference is too large (outside your acceptable tolerance for clock skew and message delivery times), you may choose to discard the request.
2. Verify the signature (`v1=`): If the timestamp is acceptable, construct the signature string on your end:
   * Use the timestamp from the `t=` part of the received `X-Signature` header.
   * Use the raw request body as the payload.
   * Concatenate them: "{timestamp\_from\_header}.{raw\_request\_body}".
   * Calculate the HMAC-SHA512 hash of this concatenated string using your webhook secret.
   * Convert the hash to its hexadecimal representation.
   * Compare this generated signature with the `v1=` value from the received `X-Signature` header. They must match exactly.

If the timestamp is recent enough and your generated signature matches the one provided in the header, you can be confident the webhook is genuine, untampered, and not a replayed request.
{% endstep %}
{% endstepper %}

### JavaScript signature verification function

{% code title="verifySignature.js" %}

```javascript
import crypto from 'crypto';

/**
 * Verify a webhook signature.
 *
 * @param {string} rawBody            – the exact request body
 * @param {string} signatureHeader    – the entire value of the x-signature header
 * @param {string} secret             – your webhook signing secret
 * @throws {Error} on any verification failure
 */
function verifySignature(rawBody, signatureHeader, secret) {
  if (!secret) {
    throw new Error('Missing webhook secret');
  }
  if (!signatureHeader) {
    throw new Error('Missing signature header');
  }

  // e.g. "t=1618884471,v1=abcdef123456…"
  const parts = signatureHeader.split(',');
  let timestamp = null;
  let receivedSig = null;

  for (const part of parts) {
    const [key, val] = part.split('=');
    if (key === 't') {
      timestamp = val;
    } else if (key === 'v1') {
      receivedSig = val;
    }
  }

  if (!timestamp) {
    throw new Error('Signature header missing timestamp (t)');
  }
  if (!receivedSig) {
    throw new Error('Signature header missing signature (v1)');
  }

  // Enforce 5 minute tolerance
  const sentTime = Number(timestamp);
  if (Number.isNaN(sentTime)) {
    throw new Error('Invalid timestamp in signature header');
  }
  const now = Math.floor(Date.now() / 1000);
  const FIVE_MINUTES = 5 * 60;
  if (Math.abs(now - sentTime) > FIVE_MINUTES) {
    throw new Error('Timestamp is outside the allowed 5 minute window');
  }

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

  const receivedBuf = Buffer.from(receivedSig, 'hex');
  const expectedBuf = Buffer.from(expectedSig, 'hex');
  // Protect against timing attacks
  if (receivedBuf.length !== expectedBuf.length ||
      !crypto.timingSafeEqual(receivedBuf, expectedBuf)) {
    throw new Error('Invalid signature');
  }

  // All checks passed
  return true;
}
```

{% endcode %}

### PHP signature verification function

{% code title="verifySignature.php" %}

```php
/**
 * Verify a webhook signature.
 *
 * @param  string  $rawBody  The exact request body
 * @param  string  $signatureHeader  The value of the X-Signature header
 * @param  string  $secret  Your webhook signing secret
 *
 * @throws Exception on any verification failure
 */
function verifySignature(string $rawBody, string $signatureHeader, string $secret) : bool
{
    if (empty($secret)) {
        throw new Exception('Missing webhook secret');
    }
    if (empty($signatureHeader)) {
        throw new Exception('Missing signature header');
    }

    // Split header like "t=1618884471,v1=abcdef123456…"
    $parts = explode(',', $signatureHeader);
    $timestamp = null;
    $receivedSig = null;

    foreach ($parts as $part) {
        [$key, $val] = array_pad(explode('=', $part, 2), 2, null);
        if ('t' === $key) {
            $timestamp = $val;
        } elseif ('v1' === $key) {
            $receivedSig = $val;
        }
    }

    if (null === $timestamp) {
        throw new Exception('Signature header missing timestamp (t)');
    }
    if (null === $receivedSig) {
        throw new Exception('Signature header missing signature (v1)');
    }
    if (! ctype_digit($timestamp)) {
        throw new Exception('Invalid timestamp in signature header');
    }

    // Enforce 5 minute skew
    $sentTime = (int) $timestamp;
    $now = time();
    if (abs($now - $sentTime) > 5 * 60) {
        throw new Exception('Timestamp is outside the allowed 5 minute window');
    }

    // Compute expected signature
    $payload = $timestamp . '.' . $rawBody;
    $expectedSig = hash_hmac('sha512', $payload, $secret);

    // Timing-safe comparison
    if (! hash_equals($expectedSig, $receivedSig)) {
        throw new Exception('Invalid signature');
    }

    return true;
}
```

{% endcode %}

### Python signature verification function

{% code title="verify\_signature.py" %}

```python
import hmac
import hashlib
import time

def verify_signature(raw_body: bytes, signature_header: str, secret: str) -> None:
    if not secret:
        raise ValueError('Missing webhook secret')
    if not signature_header:
        raise ValueError('Missing signature header')

    parts = dict(p.split('=', 1) for p in signature_header.split(',') if '=' in p)
    t = parts.get('t')
    v1 = parts.get('v1')

    if t is None:
        raise ValueError('Signature header missing timestamp (t)')
    if v1 is None:
        raise ValueError('Signature header missing signature (v1)')
    if not t.isdigit():
        raise ValueError('Invalid timestamp in signature header')

    ts = int(t)
    now = int(time.time())
    FIVE_MINUTES = 5 * 60
    if abs(now - ts) > FIVE_MINUTES:
        raise ValueError('Timestamp is outside the allowed 5 minute 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')
```

{% endcode %}

## Payloads

Webhook payload contains the following fields:

* `id` - unique event ID.
* `type` - event type, currently one of `alert.created`, `alert.updated`, `enrolment.created`, `enrolment.updated`, `representment.created`, `representment.updated`, `scheme_notice.created`, `scheme_notice.updated`, `lookup.created`, or `lookup.updated`.
* `created_at` - date and time of when event was created.
* `data` - object containing the main body of the event.
  * `object` - a snapshot of the entity the webhook was generated for. For alert events it matches the alert object from the API (<https://api.chargebackstop.com/v1/docs#/Alerts/src_api_alerts_get_alerts>); representment and scheme notice events follow their corresponding public event payload schemas.
  * `previous_attributes` - present only in ‘updated’ events, contains a list of fields that changed and their previous values.

### Examples

Sample `alert.created` payload. data→object body matches the alert object from the API (<https://api.chargebackstop.com/v1/docs#/Alerts/src_api_alerts_get_alerts>).

```python
x-signature: t=1746901125,v1=71d294712c109472c92e6afc2f0fcafce611bda73ba027f7221f8910d37b29de6fdd65489d28b28678c74d3ea62edb76f13631286d661833b0c4d4880be4d2ba
x-idempotency-key: whdl_CXwgJJye6EoZYxtFbx78Q
```

```json
{
  "id": "evt_dbXKdyUWLzSP98HMVdoFW",
  "type": "alert.created",
  "created_at": "2025-05-10T18:17:35.635870+00:00",
  "data": {
    "object": {
      "id": "netalrt_yxMihZ4JhB7h5unn36F18",
      "note": null,
      "status": "ACTION_REQUIRED",
      "created_at": "2025-05-10T13:56:56.312045Z",
      "updated_at": "2025-05-10T13:56:58.111532Z",
      "merchant_id": "mrch_YoaBvAq2H2R8JYXjXodkE",
      "merchant_ids": ["mrch_YoaBvAq2H2R8JYXjXodkE"],
      "enrolment_type": "ETHOCA_ALERT",
      "organisation_id": "org_RoExcMDGGjmmGXKxAUAkR",
      "transaction_card_bin": null,
      "chargeback_reason_code": null,
      "transaction_card_last4": "5455",
      "transaction_network_id": null,
      "transaction_card_issuer": "BofA",
      "transaction_card_scheme": "MASTERCARD",
      "action_required_deadline": "2025-05-12T13:56:56.300274Z",
      "transaction_authorised_at": "2025-05-05T13:56:56.300274Z",
      "transaction_currency_code": "USD",
      "transaction_refund_outcome": null,
      "subscription_cancel_outcome": null,
      "transaction_amount_in_cents": 6606,
      "transaction_authorisation_code": "7XP81U",
      "transaction_statement_descriptor": "ECOM-STUFF.COM",
      "transaction_acquirer_reference_number": "012533471273304331125644612",
      "integration_id": "int_ZSHVZukGYs5SdVcwkEZrL",
		  "integration_transaction_id": "pi_3SPJO4KRFSLReU4y04XJUvLN"
    }
  },
  "api_version": "v1"
}
```

Sample `alert.updated` body. Please note the `previous_attributes` object.

```python
x-signature: t=1746901218,v1=5ef9423caa3366f28eaf2411c3c3b3b6d660ddcc1a03c190ac124038e04d927bcbaae622f21c011feb27a9bae1ad96da4f74407cb1533ba1cdf8c0d8e1733b37
x-idempotency-key: whdl_ghRygm8zYiHD4vjNSQ9uT
```

```json
{
  "id": "evt_NUpgzGLGJTj5j1MZ6jb1d",
  "type": "alert.updated",
  "created_at": "2025-05-10T18:20:18.430390+00:00",
  "data": {
    "object": {
      "id": "netalrt_yxMihZ4JhB7h5unn36F18",
      "note": null,
      "status": "RESOLVED",
      "created_at": "2025-05-10T13:56:56.312045Z",
      "updated_at": "2025-05-10T18:20:18.419298Z",
      "merchant_id": "mrch_YoaBvAq2H2R8JYXjXodkE",
      "merchant_ids": ["mrch_YoaBvAq2H2R8JYXjXodkE"],
      "enrolment_type": "ETHOCA_ALERT",
      "organisation_id": "org_RoExcMDGGjmmGXKxAUAkR",
      "transaction_card_bin": null,
      "chargeback_reason_code": null,
      "transaction_card_last4": "5455",
      "transaction_network_id": null,
      "transaction_card_issuer": "BofA",
      "transaction_card_scheme": "MASTERCARD",
      "action_required_deadline": "2025-05-12T13:56:56Z",
      "transaction_authorised_at": "2025-05-05T13:56:56Z",
      "transaction_currency_code": "USD",
      "transaction_refund_outcome": "REFUNDED",
      "subscription_cancel_outcome": null,
      "transaction_amount_in_cents": 6606,
      "transaction_authorisation_code": "7XP81U",
      "transaction_statement_descriptor": "ECOM-STUFF.COM",
      "transaction_acquirer_reference_number": "012533471273304331125644612",
      "integration_id": "int_ZSHVZukGYs5SdVcwkEZrL",
		  "integration_transaction_id": "pi_3SPJO4KRFSLReU4y04XJUvLN"
    },
    "previous_attributes": {
      "status": "ACTION_REQUIRED",
      "transaction_refund_outcome": null
    }
  },
  "api_version": "v1"
}
```

### Enrolment webhooks (`enrolment.created`, `enrolment.updated`)

* `enrolment.created` is sent when a new enrolment is created.
* `enrolment.updated` is currently emitted when an enrolment `status` changes or when its Ethoca alert descriptors change. Descriptor changes include adding or removing a descriptor, editing its text, changing its `match_type`, or changing its network `status`.
* Appending descriptors through `PATCH /v2/enrolments/{enrolment_id}` emits `enrolment.updated` with `previous_attributes.ethoca_alert.descriptors`.
* This behavior is the current implementation and is not a strict long-term contract. Additional enrolment field changes may also emit `enrolment.updated` in the future.
* Status updates performed through simulation tooling (for example, `PATCH /v1/simulate/enrolments/{enrolment_id}` for TEST organisations) follow the same behavior and can emit `enrolment.updated`.
* Enrolment webhook payloads are versioned as `api_version: "v2"`.
* For update events, `data.previous_attributes` mirrors the shape of `data.object` and includes tracked fields with their previous values at event creation time. A descriptor change therefore appears as `previous_attributes.ethoca_alert.descriptors`.

Sample `enrolment.created` payload.

```json
{
  "id": "evt_hxgqT7vA8am77QbJCMiFA",
  "type": "enrolment.created",
  "created_at": "2026-02-25T15:02:49.256560+00:00",
  "data": {
    "object": {
      "id": "enrl_pfNupxFzfYDaEf1UrD6wU",
      "organisation_id": "org_MSALEuBsAywqDA3SQVqc8",
      "merchant_ids": [
        "mrch_XXiP48XqnnYbRvnUFVH6G"
      ],
      "type": "VERIFI_ORDER_INSIGHT",
      "status": "IN_PROGRESS",
      "created_at": "2026-02-25T15:02:49.256560Z",
      "updated_at": "2026-02-25T15:03:14.460982Z",
      "note": ""
    }
  },
  "api_version": "v2"
}
```

Sample `enrolment.updated` payload. Please note the `previous_attributes` object.

```json
{
  "id": "evt_aXCsMEEaxP3Jvk9SmxBaQ",
  "type": "enrolment.updated",
  "created_at": "2026-02-25T15:05:31.102891+00:00",
  "data": {
    "object": {
      "id": "enrl_pfNupxFzfYDaEf1UrD6wU",
      "organisation_id": "org_MSALEuBsAywqDA3SQVqc8",
      "merchant_ids": [
        "mrch_XXiP48XqnnYbRvnUFVH6G"
      ],
      "type": "VERIFI_ORDER_INSIGHT",
      "status": "ENABLED",
      "created_at": "2026-02-25T15:02:49.256560Z",
      "updated_at": "2026-02-25T15:05:31.083449Z",
      "note": ""
    },
    "previous_attributes": {
      "status": "IN_PROGRESS"
    }
  },
  "api_version": "v2"
}
```

An Ethoca descriptor change uses the same nested shape in `previous_attributes`:

```json
{
  "id": "evt_cRt7YbNqLm2Xw8FdKp3vJ",
  "type": "enrolment.updated",
  "created_at": "2026-02-26T09:14:02.551204+00:00",
  "data": {
    "object": {
      "id": "enrl_tQ9wKzPvHn4Bs2LmXcR7D",
      "organisation_id": "org_MSALEuBsAywqDA3SQVqc8",
      "merchant_ids": [
        "mrch_XXiP48XqnnYbRvnUFVH6G"
      ],
      "type": "ETHOCA_ALERT",
      "status": "ENABLED",
      "ethoca_alert": {
        "descriptors": [
          {
            "descriptor": "ACME-FIT",
            "match_type": "EXACT_MATCH",
            "status": "ACTIVE"
          }
        ]
      },
      "created_at": "2026-02-20T11:41:07.204118Z",
      "updated_at": "2026-02-26T09:14:02.498771Z",
      "note": ""
    },
    "previous_attributes": {
      "ethoca_alert": {
        "descriptors": [
          {
            "descriptor": "ACMEFIT",
            "match_type": "STARTS_WITH",
            "status": "PENDING"
          }
        ]
      }
    }
  },
  "api_version": "v2"
}
```

Sample `representment.created` event

```json
{
  "id": "evt_S4VJrD42E1mVRVuCeapmt",
  "type": "representment.created",
  "created_at": "2025-05-22T19:09:09.512272+00:00",
  "data": {
    "object": {
      "id": "rep_DenAQk14kzDmwKSJn7cU3",
      "created_at": "2025-05-22T19:09:09.495869Z",
      "managed_by": "PARTNER",
      "updated_at": "2025-05-22T19:09:09.496523Z",
      "disputed_at": "2024-11-19T00:00:00",
      "merchant_id": "mrch_bQXg83B18qgACnp5Y4qU8",
      "service_type": "ONLINE_SERVICES",
      "dispute_stage": "CHARGEBACK",
      "dispute_due_by": "2024-12-03T00:00:00",
      "dispute_reason": "SUBSCRIPTION_CANCELED",
      "dispute_schema": "MASTERCARD",
      "dispute_status": "OPEN",
      "organisation_id": "org_tmo8Dq484yRUiiAcJuRPd",
      "dispute_reason_code": null,
      "dispute_reference_id": null,
      "dispute_currency_code": "USD",
      "dispute_amount_in_cents": 4444,
      "transaction_reference_id": null,
      "transaction_currency_code": "USD",
      "transaction_amount_in_cents": 4444,
      "transaction_authorisation_date": "2024-11-12T00:00:00",
      "transaction_acquirer_reference_number": null
    }
  },
  "api_version": "v1"
}
```

Sample `representment.updated` event

```json
{
  "id": "evt_2rzszUnkDUNFiBKqe6DdT",
  "type": "representment.updated",
  "created_at": "2025-05-22T20:14:51.041381+00:00",
  "data": {
    "object": {
      "id": "rep_wMxBaE4ivxQ7zvPy1dmNx",
      "created_at": "2025-05-22T19:08:25.245116Z",
      "managed_by": "PARTNER",
      "updated_at": "2025-05-22T20:14:51.004632Z",
      "disputed_at": "2024-11-19T00:00:00",
      "merchant_id": "mrch_bQXg83B18qgACnp5Y4qU8",
      "service_type": "ONLINE_SERVICES",
      "dispute_stage": "CHARGEBACK",
      "dispute_due_by": "2024-12-03T00:00:00",
      "dispute_reason": "SUBSCRIPTION_CANCELED",
      "dispute_schema": "MASTERCARD",
      "dispute_status": "LOST",
      "organisation_id": "org_tmo8Dq484yRUiiAcJuRPd",
      "dispute_reason_code": null,
      "dispute_reference_id": null,
      "dispute_currency_code": "USD",
      "dispute_amount_in_cents": 4444,
      "transaction_reference_id": null,
      "transaction_currency_code": "USD",
      "transaction_amount_in_cents": 4444,
      "transaction_authorisation_date": "2024-11-12T00:00:00",
      "transaction_acquirer_reference_number": null
    },
    "previous_attributes": {
      "dispute_status": "OPEN"
    }
  },
  "api_version": "v1"
}
```

#### Scheme notice webhooks (`scheme_notice.created`, `scheme_notice.updated`)

* `scheme_notice.created` is sent when a new scheme notice is created.
* `scheme_notice.updated` is emitted when a tracked field on a scheme notice changes. Currently tracked fields are: `is_revoked`, `notice_revoked_at`, `fraud_reported_at`, and `transaction_purchase_date`.
* A `scheme_notice.updated` event is only emitted after a `scheme_notice.created` event exists for that notice.
* For update events, `data.previous_attributes` includes only the tracked fields that changed and their previous values.
* Scheme notice webhook payloads are versioned as `api_version: "v1"`.

Sample `scheme_notice.created` payload.

```json
{
  "id": "evt_dbXKdyUWLzSP98HMVdoFW",
  "type": "scheme_notice.created",
  "created_at": "2026-03-01T10:30:45.123456+00:00",
  "data": {
    "object": {
      "id": "schntc_NFSPZDSTv3QgfU8GDhXKK",
      "organisation_id": "org_WVJ7aJzpT32FED9BqKPpM",
      "merchant_id": "mrch_bQXg83B18qgACnp5Y4qU8",
      "scheme_notice_type": "TC40",
      "notice_type": "FRAUD_NOTICE",
      "scheme": "VISA",
      "is_revoked": false,
      "notice_revoked_at": null,
      "fraud_reported_at": "2026-02-21T08:13:50.360977Z",
      "transaction_amount_in_cents": 14760,
      "transaction_currency_code": "USD",
      "transaction_card_bin": "411798",
      "transaction_card_last4": "3508",
      "transaction_merchant_name": "ECOM-STUFF OUTLET",
      "transaction_merchant_id": "274953873887879",
      "transaction_acquirer_reference_number": "77198913101798678449413",
      "transaction_authorisation_code": "96JNEP",
      "transaction_original_date": "2026-02-11T08:13:50.360977Z",
      "transaction_purchase_date": "2026-02-11T08:13:50.360977Z",
      "fraud_type": "CARD_NOT_PRESENT",
      "fraud_dispute_eligible": true,
      "created_at": "2026-03-01T10:30:45.123456Z",
      "updated_at": "2026-03-01T10:30:45.123456Z"
    }
  },
  "api_version": "v1"
}
```

Sample `scheme_notice.updated` payload. In this example, the notice was revoked. Please note the `previous_attributes` object.

```json
{
  "id": "evt_NUpgzGLGJTj5j1MZ6jb1d",
  "type": "scheme_notice.updated",
  "created_at": "2026-03-01T12:00:00.541381+00:00",
  "data": {
    "object": {
      "id": "schntc_NFSPZDSTv3QgfU8GDhXKK",
      "organisation_id": "org_WVJ7aJzpT32FED9BqKPpM",
      "merchant_id": "mrch_bQXg83B18qgACnp5Y4qU8",
      "scheme_notice_type": "TC40",
      "notice_type": "FRAUD_NOTICE",
      "scheme": "VISA",
      "is_revoked": true,
      "notice_revoked_at": "2026-03-01T12:00:00.000000Z",
      "fraud_reported_at": "2026-02-21T08:13:50.360977Z",
      "transaction_amount_in_cents": 14760,
      "transaction_currency_code": "USD",
      "transaction_card_bin": "411798",
      "transaction_card_last4": "3508",
      "transaction_merchant_name": "ECOM-STUFF OUTLET",
      "transaction_merchant_id": "274953873887879",
      "transaction_acquirer_reference_number": "77198913101798678449413",
      "transaction_authorisation_code": "96JNEP",
      "transaction_original_date": "2026-02-11T08:13:50.360977Z",
      "transaction_purchase_date": "2026-02-11T08:13:50.360977Z",
      "fraud_type": "CARD_NOT_PRESENT",
      "fraud_dispute_eligible": true,
      "created_at": "2026-03-01T10:30:45.123456Z",
      "updated_at": "2026-03-01T12:00:00.541381Z"
    },
    "previous_attributes": {
      "is_revoked": false,
      "notice_revoked_at": null
    }
  },
  "api_version": "v1"
}
```

Scheme notice field definitions:

* `scheme_notice_type`: TC15, TC40, or SAFE
* `notice_type`: FRAUD\_NOTICE or DISPUTE\_NOTICE
* `scheme`: VISA, MASTERCARD, or OTHER
* `fraud_reported_at`: Date when the fraud was reported
* `fraud_type`: Free-form string from the upstream network (e.g., CARD\_NOT\_PRESENT, STOLEN, COUNTERFEIT, ACCOUNT\_TAKEOVER, etc.)
* `fraud_dispute_eligible`: Whether the fraud is eligible for a dispute

#### Lookup webhooks (`lookup.created`, `lookup.updated`)

* `lookup.created` is sent when a new digital receipt lookup is created (from Ethoca Consumer Clarity or Verifi Order Insight).
* `lookup.updated` is emitted when a tracked field on a lookup changes. Currently tracked fields are: `lookup_status` and `deflection_status`.
* A `lookup.updated` event is only emitted after a `lookup.created` event exists for that lookup.
* For update events, `data.previous_attributes` includes only the tracked fields that changed and their previous values.
* Lookup webhook payloads are versioned as `api_version: "v1"`.

Sample `lookup.created` payload.

```json
{
  "id": "evt_dbXKdyUWLzSP98HMVdoFW",
  "type": "lookup.created",
  "created_at": "2026-03-12T10:30:45.123456+00:00",
  "data": {
    "object": {
      "id": "lkup_NFSPZDSTv3QgfU8GDhXKK",
      "organisation_id": "org_WVJ7aJzpT32FED9BqKPpM",
      "enrollment_id": "enrl_pfNupxFzfYDaEf1UrD6wU",
      "type": "ETHOCA_CONSUMER_CLARITY",
      "parent_lookup_id": null,
      "lookup_status": "SUCCEEDED",
      "deflection_status": "NOT_ATTEMPTED",
      "transaction_card_bin": "411798",
      "transaction_card_last4": "3508",
      "transaction_arn": "77198913101798678449413",
      "transaction_auth_code": "96JNEP",
      "transaction_amount": 14760,
      "transaction_currency": "USD",
      "transaction_date": "2026-03-10T08:13:50.360977Z",
      "transaction_statement_descriptor": "ECOM-STUFF.COM",
      "transaction_network_id": null,
      "integration_id": "int_ZSHVZukGYs5SdVcwkEZrL",
      "integration_transaction_id": "pi_3SPJO4KRFSLReU4y04XJUvLN",
      "created_at": "2026-03-12T10:30:45.123456Z",
      "updated_at": "2026-03-12T10:30:45.123456Z"
    }
  },
  "api_version": "v1"
}
```

Sample `lookup.updated` payload. In this example, the deflection status changed from PENDING to SUCCEEDED. Please note the `previous_attributes` object.

```json
{
  "id": "evt_NUpgzGLGJTj5j1MZ6jb1d",
  "type": "lookup.updated",
  "created_at": "2026-03-12T12:00:00.541381+00:00",
  "data": {
    "object": {
      "id": "lkup_NFSPZDSTv3QgfU8GDhXKK",
      "organisation_id": "org_WVJ7aJzpT32FED9BqKPpM",
      "enrollment_id": "enrl_pfNupxFzfYDaEf1UrD6wU",
      "type": "ETHOCA_FIRST_PARTY_TRUST",
      "parent_lookup_id": null,
      "lookup_status": "SUCCEEDED",
      "deflection_status": "SUCCEEDED",
      "transaction_card_bin": "411798",
      "transaction_card_last4": "3508",
      "transaction_arn": "77198913101798678449413",
      "transaction_auth_code": "96JNEP",
      "transaction_amount": 14760,
      "transaction_currency": "USD",
      "transaction_date": "2026-03-10T08:13:50.360977Z",
      "transaction_statement_descriptor": "ECOM-STUFF.COM",
      "transaction_network_id": null,
      "integration_id": "int_ZSHVZukGYs5SdVcwkEZrL",
      "integration_transaction_id": "pi_3SPJO4KRFSLReU4y04XJUvLN",
      "created_at": "2026-03-12T10:30:45.123456Z",
      "updated_at": "2026-03-12T12:00:00.541381Z"
    },
    "previous_attributes": {
      "deflection_status": "PENDING"
    }
  },
  "api_version": "v1"
}
```

Lookup field definitions:

* `type`: ETHOCA\_CONSUMER\_CLARITY, ETHOCA\_FIRST\_PARTY\_TRUST, VERIFI\_ORDER\_INSIGHT, or VERIFI\_COMPELLING\_EVIDENCE\_3
* `lookup_status`: PENDING, SUCCEEDED, FAILED, or TIMEOUT
* `deflection_status`: NOT\_ATTEMPTED, PENDING, SUCCEEDED, or FAILED
* `parent_lookup_id`: ID of the parent lookup (e.g., for HISTORICAL lookups linked to a DISPUTED lookup)
* `transaction_amount`: Transaction amount in cents
* `integration_id`: Platform Integration ID for the matched transaction's payment processor integration
* `integration_transaction_id`: Payment processor's transaction ID (e.g., Stripe charge ID, Adyen PSP reference)

## Postman collection

We created a Postman collection that allows you to send sample webhooks.

It contains a pre-request script that generates a correct signature every time you send the request.

Postman collection: <https://www.postman.com/chargebackstop/chargebackstop/collection/2yu40q8/chargebackstop-webhooks>


# Migration Guide: fraud\_notification.created -> scheme\_notice.created

This guide explains how to migrate webhook consumers from the deprecated `fraud_notification.created` event to `scheme_notice.created`.

## What is changing

* `fraud_notification.created` is deprecated and will be removed on March 31.
* `scheme_notice.created` is the replacement event for new scheme notice records.
* Event envelope stays the same: `id`, `type`, `created_at`, `data.object`, `api_version`.

## Compatibility and rollout notes

* Do not listen to `fraud_notification.created` and `scheme_notice.created` at the same time in production.
* A dual subscription can create duplicate processing for the same business signal.
* `fraud_notification.created` events are not remapped onto existing `SchemeNotice` rows.
* Historical backfill/migration does not emit webhook side effects (no replay).
* Use a strict cutover plan: stop legacy consumption, then start new event consumption.

## Payload changes (`data.object`)

The new event payload is based on `SchemeNotice`.

### Field mapping

| Deprecated event (`fraud_notification.created`) | Replacement (`scheme_notice.created`)   | Notes                                                                                             |
| ----------------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `id` (`frdnt_*`)                                | `id` (`schntc_*`)                       | Object id prefix changes                                                                          |
| `organisation_id`                               | `organisation_id`                       | Same                                                                                              |
| `fraud_notification_type`                       | `scheme_notice_type`                    | Renamed; allowed values: `TC15`, `TC40`, `SAFE` (`fraud_notification.created` used `TC40`/`SAFE`) |
| `fraud_reported_at`                             | `fraud_reported_at`                     | Same semantic meaning                                                                             |
| `fraud_type`                                    | `fraud_type`                            | Same                                                                                              |
| `transaction_amount_in_cents`                   | `transaction_amount_in_cents`           | Same                                                                                              |
| `transaction_currency_code`                     | `transaction_currency_code`             | Same                                                                                              |
| `transaction_card_bin`                          | `transaction_card_bin`                  | Same                                                                                              |
| `transaction_card_last4`                        | `transaction_card_last4`                | Same                                                                                              |
| `transaction_merchant_name`                     | `transaction_merchant_name`             | Same                                                                                              |
| `transaction_merchant_id`                       | `transaction_merchant_id`               | Same                                                                                              |
| `transaction_acquirer_reference_number`         | `transaction_acquirer_reference_number` | Same                                                                                              |
| `transaction_authorisation_code`                | `transaction_authorisation_code`        | Same                                                                                              |
| `transaction_original_date`                     | `transaction_original_date`             | Same                                                                                              |
| *(not available)*                               | `merchant_id`                           | New, nullable                                                                                     |
| *(not available)*                               | `notice_type`                           | New (`FRAUD_NOTICE` or `DISPUTE_NOTICE`)                                                          |
| *(not available)*                               | `scheme`                                | New (`VISA`, `MASTERCARD`, `OTHER`)                                                               |
| *(not available)*                               | `is_revoked`                            | New lifecycle field                                                                               |
| *(not available)*                               | `notice_revoked_at`                     | New lifecycle timestamp                                                                           |
| *(not available)*                               | `fraud_dispute_eligible`                | New optional boolean                                                                              |
| *(not available)*                               | `transaction_purchase_date`             | New optional timestamp                                                                            |
| *(not available)*                               | `created_at`, `updated_at`              | Object-level timestamps                                                                           |

## Example: new event format

Timestamps are RFC 3339 UTC. In production payloads, envelope `created_at` is emitted as an offset timestamp (for example `+00:00`), while `data.object` datetime fields are emitted with a `Z` suffix. Fractional seconds may appear when present.

```json
{
  "id": "evt_9UYR3Q8xP4aX4Z4uL2YfN",
  "type": "scheme_notice.created",
  "created_at": "2026-02-13T12:17:42+00:00",
  "data": {
    "object": {
      "id": "schntc_8W2tVb13qfHkK3gQ9n7Yp",
      "organisation_id": "org_WVJ7aJzpT32FED9BqKPpM",
      "merchant_id": "mrch_bQXg83B18qgACnp5Y4qU8",
      "scheme_notice_type": "SAFE",
      "notice_type": "FRAUD_NOTICE",
      "scheme": "MASTERCARD",
      "is_revoked": false,
      "notice_revoked_at": null,
      "fraud_reported_at": "2026-02-13T12:15:00Z",
      "transaction_amount_in_cents": 14760,
      "transaction_currency_code": "USD",
      "transaction_card_bin": "411798",
      "transaction_card_last4": "3508",
      "transaction_merchant_name": "ECOM-STUFF OUTLET",
      "transaction_merchant_id": "274953873887879",
      "transaction_acquirer_reference_number": "77198913101798678449413",
      "transaction_authorisation_code": "96JNEP",
      "transaction_original_date": "2026-02-01T08:13:50Z",
      "transaction_purchase_date": null,
      "fraud_type": "CARD_NOT_PRESENT",
      "fraud_dispute_eligible": true,
      "created_at": "2026-02-13T12:16:10Z",
      "updated_at": "2026-02-13T12:16:10Z"
    }
  },
  "api_version": "v1"
}
```

## Consumer migration checklist

{% stepper %}
{% step %}

#### Prepare your consumer code to support `scheme_notice.created`

1. Prepare your consumer code to support `scheme_notice.created` before changing subscriptions.
   {% endstep %}

{% step %}

#### Update parser mappings

* replace `fraud_notification_type` with `scheme_notice_type`
* expect object `id` to use `schntc_*` prefix
  {% endstep %}

{% step %}

#### Keep existing logic for common fields

Keep existing logic for common fields (`fraud_reported_at`, `transaction_*`, `fraud_type`).
{% endstep %}

{% step %}

#### Add null-safe handling for new optional fields

Add null-safe handling for new optional fields (`merchant_id`, `transaction_purchase_date`, `fraud_dispute_eligible`, `notice_revoked_at`).
{% endstep %}

{% step %}

#### Execute strict cutover in webhook settings

* disable/unsubscribe `fraud_notification.created`
* enable/subscribe `scheme_notice.created`
* do not run both subscriptions concurrently in production
  {% endstep %}

{% step %}

#### Keep handler focused on create semantics

Keep your handler focused on create semantics for `scheme_notice.created`.
{% endstep %}
{% endstepper %}

## Recommended strict cutover strategy

Use a single-event-family cutover to avoid duplicates:

{% stepper %}
{% step %}

#### Deploy consumer changes that can parse `scheme_notice.created`

Deploy consumer changes that can parse `scheme_notice.created`.
{% endstep %}

{% step %}

#### Switch subscription in one cutover window

In production webhook configuration, switch subscription from `fraud_notification.created` to `scheme_notice.created` in one cutover window.
{% endstep %}

{% step %}

#### Verify delivery and downstream processing

Verify successful delivery and downstream processing.
{% endstep %}

{% step %}

#### Keep legacy handler for rollback only

Keep legacy handler code temporarily for rollback, but keep legacy subscription disabled unless rolling back.
{% endstep %}

{% step %}

#### Remove legacy handler after cutover is stable

Remove legacy handler after cutover is stable.
{% endstep %}
{% endstepper %}


# Orders API Integration Guide

This guide is for partners building a white-label integration where your platform creates customer organisations, creates merchants, uploads complete order data through the Orders API, receives charge

{% hint style="info" %}
Use this guide for `COMPLETE` Orders API integrations only. `COMPLETE` orders must use a `CUSTOM_ORDERS` integration. If you are enriching an existing Stripe, Adyen, Shopify, or other processor integration, use `PARTIAL` orders instead. See the [Orders API reference](https://docs.chargebackstop.com/developer/api-documentation/orders).
{% endhint %}

## Reference documentation

* [Orders API](https://docs.chargebackstop.com/developer/api-documentation/orders)
* [Organisations API](https://docs.chargebackstop.com/developer/api-documentation/organisations)
* [Merchants API](https://docs.chargebackstop.com/developer/api-documentation/merchants)
* [Integrations API](https://docs.chargebackstop.com/developer/api-documentation/integrations)
* [Enrolments API](https://docs.chargebackstop.com/developer/api-documentation/enrolments)
* [Alerts API](https://docs.chargebackstop.com/developer/api-documentation/alerts)
* [Rulesets API](https://docs.chargebackstop.com/developer/api-documentation/rulesets-developer-preview)
* [Simulations API](https://docs.chargebackstop.com/developer/api-documentation/simulations)
* [Webhooks](https://docs.chargebackstop.com/developer/partner-integration-guide/webhooks)
* [Chargeback alert enrolments](https://docs.chargebackstop.com/chargeback-alerts/enrolments)
* [Verifi RDR](https://docs.chargebackstop.com/chargeback-alerts/verifi-rdr)
* [Transaction matching](https://docs.chargebackstop.com/chargeback-alerts/transaction-matching)
* [Resolution rules](https://docs.chargebackstop.com/chargeback-alerts/resolution-rules)

## What you will build

Your integration should support this lifecycle:

1. Create one ChargebackStop organisation per customer.
2. Create one or more merchants inside that organisation.
3. Create a `CUSTOM_ORDERS` integration linked to the merchant or merchants.
4. Upload `COMPLETE` orders with card transaction identifiers that support chargeback alert matching.
5. Create chargeback alert enrolments for Ethoca Alerts and/or Verifi RDR.
6. Receive `enrolment.created`, `enrolment.updated`, `alert.created`, and `alert.updated` webhooks.
7. Resolve actionable Ethoca alerts with `PATCH /v1/alerts/{alert_id}`.
8. Promote the same flow from a TEST partner account to a LIVE production partner account.

## Environments and accounts

ChargebackStop uses the same API host for test and production:

```
https://api.chargebackstop.com
```

The account mode controls whether records are TEST or LIVE.

| Environment | Account              | Purpose                                                                               | Notes                                                                                                                         |
| ----------- | -------------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| Development | TEST partner account | Build API client, webhook receiver, order upload, and alert action workflows          | Organisations created under a TEST partner become TEST organisations. Simulation endpoints work only with TEST organisations. |
| Production  | LIVE partner account | Create real customer organisations, submit production orders, and receive real alerts | LIVE organisations cannot use simulation endpoints. Real enrolments must be approved/enabled before alerts arrive.            |

{% hint style="warning" %}
TEST IDs and LIVE IDs are separate. Do not carry `org_`, `mrch_`, `int_`, `enrl_`, `ord_`, or `netalrt_` IDs from TEST into production. Store environment-specific mappings in your system.
{% endhint %}

## Before you write code

Ask ChargebackStop for a TEST partner account. In that TEST account:

1. Open the partner dashboard.
2. Go to the partner settings or administrator group developer settings.
3. Create a TEST partner API key.
4. Add a TEST webhook endpoint for `enrolment.created`, `enrolment.updated`, `alert.created`, and `alert.updated`.
5. Reveal and store the webhook signing secret.

For local development, expose your webhook receiver through a temporary HTTPS URL, such as a tunnel, because webhook endpoint URLs must be HTTPS.

## Authentication and access model

All API requests use a bearer token:

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

For partner-owned onboarding, create a partner-group API key in your partner dashboard. Admin partner-group keys can access all organisations under the partner account. Non-admin partner-group keys can access only organisations assigned to that group.

{% tabs %}
{% tab title="Admin partner key" %}
Use an admin partner-group key for automated customer onboarding because it can create organisations, merchants, and enrolments.

Required abilities for the full flow:

* `organisations:read`, `organisations:write`
* `merchants:read`, `merchants:write`
* `integrations:read`, `integrations:write`
* `orders:read`, `orders:write`, `orders:update`
* `enrolments_v2:read`, `enrolments_v2:write`
* `alerts:read`, `alerts:write`
* `rulesets:read`, `rulesets:write` if you will create custom RDR resolution rules
* `simulations:alerts`, `simulations:enrollments` for TEST only
  {% endtab %}

{% tab title="Restricted partner key" %}
Use a non-admin partner-group key for steady-state operations against assigned organisations, such as uploading orders, reading alerts, and actioning alerts.

Restricted keys are useful once customer organisations and merchants already exist. They should not be used for fully automated onboarding unless your operating model deliberately separates setup from daily processing.
{% endtab %}
{% endtabs %}

{% hint style="danger" %}
API keys are shown only once when created. Store them in your secrets manager immediately, never in source control, logs, or customer-visible configuration.
{% endhint %}

## Data you should store

At minimum, store these mappings in your platform:

| Your system                        | ChargebackStop ID             | Why it matters                                                                                                  |
| ---------------------------------- | ----------------------------- | --------------------------------------------------------------------------------------------------------------- |
| Customer account                   | `organisation_id`             | Required for Orders, Alerts filters, Simulations, and Integrations.                                             |
| Merchant/MID/CAID/provider account | `merchant_id`                 | Required for Integrations and Enrolments.                                                                       |
| Orders feed connection             | `integration_id`              | Required for every `COMPLETE` order.                                                                            |
| Alert provider enrolment           | `enrolment_id`                | Required for simulation, enrolment status tracking, and RDR ruleset assignment.                                 |
| Order                              | `order_id` and `reference_id` | `reference_id` is your stable idempotency key for create. `order_id` is used for `PATCH /v1/orders/{order_id}`. |
| Alert                              | `alert_id`                    | Required for `GET /v1/alerts/{alert_id}` and `PATCH /v1/alerts/{alert_id}`.                                     |

## Orders API requirements for alert matching

Send `COMPLETE` orders as soon as they are available, and backfill recent history before enabling real alert traffic. Agree the backfill window with ChargebackStop during onboarding.

Required order fields for `COMPLETE` mode:

| Field                            | Description                                                                               |
| -------------------------------- | ----------------------------------------------------------------------------------------- |
| `type`                           | Must be `COMPLETE`.                                                                       |
| `organisation_id`                | Customer organisation ID.                                                                 |
| `integration_id`                 | `CUSTOM_ORDERS` integration ID.                                                           |
| `reference_id`                   | Stable unique order identifier from your system.                                          |
| `order_datetime`                 | ISO 8601 order timestamp.                                                                 |
| `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`. |

For alert matching, include at least one transaction per order.

Required transaction fields:

| Field                         | Description                                                         |
| ----------------------------- | ------------------------------------------------------------------- |
| `reference_id`                | Stable unique transaction identifier within the integration.        |
| `amount_in_cents`             | Transaction amount.                                                 |
| `currency`                    | ISO 4217 currency code.                                             |
| `payment_method_type`         | Usually `CARD`.                                                     |
| `authorisation_status`        | Usually `SETTLED` for completed payments.                           |
| `payment_method_reference_id` | Your payment method or payment transaction reference.               |
| `authorised_at`               | ISO 8601 authorisation timestamp.                                   |
| `payment_method_card_brand`   | Required for card transactions, for example `VISA` or `MASTERCARD`. |
| `payment_method_card_last_4`  | Required for card transactions.                                     |

Send as many matching identifiers as possible:

| Identifier                  | Recommendation                                                                     |
| --------------------------- | ---------------------------------------------------------------------------------- |
| `acquirer_reference_number` | Send whenever available. This is the strongest matching identifier.                |
| `authorisation_code`        | Send whenever available, especially with card last 4.                              |
| `payment_method_card_bin`   | Send whenever available, especially with card brand, last 4, amount, and currency. |

{% hint style="info" %}
Also send related `refunds` and `disputes` when you have them. These records help ChargebackStop identify invalid or already-refunded alerts.
{% endhint %}

## Enrolment requirements

Create a separate enrolment for each chargeback alert product the customer will use. Save the returned `enrolment_id`; it is the identifier you will use for status tracking, TEST simulations, and RDR ruleset assignment.

{% tabs %}
{% tab title="Ethoca Alerts" %}
Use `type: ETHOCA_ALERT`.

Required enrolment data:

| Field                                   | Requirement                                          |
| --------------------------------------- | ---------------------------------------------------- |
| `merchant_ids`                          | One or more merchant IDs from the same organisation. |
| `ethoca_alert.descriptors`              | One or more billing descriptors.                     |
| `ethoca_alert.descriptors[].descriptor` | The descriptor text Ethoca should enrol.             |
| `ethoca_alert.descriptors[].match_type` | `STARTS_WITH` or `EXACT_MATCH`.                      |

Ethoca enrolment is descriptor based. Do not use ARN or BIN + CAID as the Ethoca enrolment identifier. ARN, authorisation code, BIN, last 4, amount, currency, and transaction dates belong on Orders API transaction records so alerts can be matched to uploaded orders.
{% endtab %}

{% tab title="Verifi RDR" %}
Use `type: VERIFI_RDR`.

Required enrolment data:

| Field                                  | Requirement                                                                                          |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `merchant_ids`                         | One or more merchant IDs from the same organisation.                                                 |
| `verifi_rdr.bin` and `verifi_rdr.caid` | Preferred for BIN + CAID enrolment. Some partners call this BIN + CID; the API field name is `caid`. |
| `verifi_rdr.arns`                      | Use this instead of BIN + CAID when the RDR enrolment is ARN based.                                  |

Provide either `bin` and `caid` together, or provide at least one ARN in `arns`.

By default, a Verifi RDR BIN + CAID enrolment is published without a custom ruleset. That means eligible RDR cases for that BIN + CAID follow the default RDR flow and are accepted/refunded at the network level. If the customer wants exceptions, create a platform-run resolution ruleset and attach it to the RDR `enrolment_id` before go-live.
{% endtab %}
{% endtabs %}

{% hint style="warning" %}
Enrolments are created in `PENDING`. Treat the enrolment as live only after you receive `enrolment.updated` with `status: "ENABLED"` or a direct `GET /v2/enrolments/{enrolment_id}` returns `ENABLED`.
{% endhint %}

## End-to-end TEST flow

Run this flow in a TEST partner account first.

{% stepper %}
{% step %}

### Set your environment variables

```bash
export CBS_API_BASE="https://api.chargebackstop.com"
export CBS_API_KEY="cbs_REPLACE_WITH_TEST_PARTNER_API_KEY"
```

Confirm your key works by listing organisations:

```bash
curl -X GET "$CBS_API_BASE/v1/organisations/?limit=10&offset=0" \
  -H "Authorization: Bearer $CBS_API_KEY"
```

{% endstep %}

{% step %}

### Create a customer organisation

Use one organisation per customer.

```bash
curl -X POST "$CBS_API_BASE/v1/organisations/" \
  -H "Authorization: Bearer $CBS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Fitness"
  }'
```

Example TEST response:

```json
{
  "id": "org_abc123",
  "name": "[TEST] Acme Fitness",
  "mode": "TEST",
  "created_at": "2026-02-17T10:00:00Z",
  "updated_at": "2026-02-17T10:00:00Z",
  "links": [{"rel": "self", "uri": "/v1/organisations/org_abc123"}]
}
```

Save `organisation_id`.
{% endstep %}

{% step %}

### Create a merchant

Use `external_id` for the merchant identifier your team recognises, such as MID, CAID, or provider account ID.

```bash
curl -X POST "$CBS_API_BASE/v1/merchants/" \
  -H "Authorization: Bearer $CBS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "organisation_id": "org_abc123",
    "name": "Acme Fitness Main Store",
    "type": "GENERIC",
    "external_id": "mid_123456789"
  }'
```

Example response:

```json
{
  "id": "mrch_abc123",
  "organisation_id": "org_abc123",
  "name": "Acme Fitness Main Store",
  "type": "GENERIC",
  "external_id": "mid_123456789",
  "default_representment_service_type": null,
  "created_at": "2026-02-17T10:02:00Z",
  "updated_at": "2026-02-17T10:02:00Z",
  "links": [{"rel": "self", "uri": "/v1/merchants/mrch_abc123"}]
}
```

Save `merchant_id`.
{% endstep %}

{% step %}

### Create a custom orders integration

Create one `CUSTOM_ORDERS` integration for the orders feed you will use for this customer.

```bash
curl -X POST "$CBS_API_BASE/v1/integrations/" \
  -H "Authorization: Bearer $CBS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "organisation_id": "org_abc123",
    "name": "Acme Fitness Orders API",
    "type": "CUSTOM_ORDERS",
    "status": "ENABLED",
    "merchant_ids": ["mrch_abc123"]
  }'
```

Example response:

```json
{
  "id": "int_abc123",
  "organisation_id": "org_abc123",
  "name": "Acme Fitness Orders API",
  "type": "CUSTOM_ORDERS",
  "status": "ENABLED",
  "merchant_ids": ["mrch_abc123"],
  "links": [{"rel": "self", "uri": "/v1/integrations/int_abc123"}]
}
```

Save `integration_id`.
{% endstep %}

{% step %}

### Create a chargeback alert enrolment

Create one enrolment per product. The examples below use the same merchant, but production customers may need separate enrolments for different descriptors, BIN + CAID pairs, or ARN sets.

{% tabs %}
{% tab title="Ethoca Alert" %}
Ethoca enrolments use descriptors.

```bash
curl -X POST "$CBS_API_BASE/v2/enrolments/" \
  -H "Authorization: Bearer $CBS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "merchant_ids": ["mrch_abc123"],
    "type": "ETHOCA_ALERT",
    "ethoca_alert": {
      "descriptors": [
        {
          "descriptor": "ACMEFIT",
          "match_type": "STARTS_WITH"
        }
      ]
    }
  }'
```

Example response:

```json
{
  "id": "enrl_ethoca123",
  "organisation_id": "org_abc123",
  "merchant_ids": ["mrch_abc123"],
  "type": "ETHOCA_ALERT",
  "status": "PENDING",
  "ethoca_alert": {
    "descriptors": [
      {
        "descriptor": "ACMEFIT",
        "match_type": "STARTS_WITH",
        "status": "PENDING"
      }
    ]
  },
  "created_at": "2026-02-17T10:03:00Z",
  "updated_at": "2026-02-17T10:03:00Z",
  "note": null,
  "links": [{"rel": "self", "uri": "/v2/enrolments/enrl_ethoca123"}]
}
```

Save the Ethoca `enrolment_id`.
{% endtab %}

{% tab title="Verifi RDR" %}
Verifi RDR enrolments use either BIN + CAID or ARNs.

BIN + CAID example:

```bash
curl -X POST "$CBS_API_BASE/v2/enrolments/" \
  -H "Authorization: Bearer $CBS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "merchant_ids": ["mrch_abc123"],
    "type": "VERIFI_RDR",
    "verifi_rdr": {
      "bin": "424242",
      "caid": "ACMEFITCAID01"
    }
  }'
```

Example response:

```json
{
  "id": "enrl_rdr123",
  "organisation_id": "org_abc123",
  "merchant_ids": ["mrch_abc123"],
  "type": "VERIFI_RDR",
  "status": "PENDING",
  "verifi_rdr": {
    "bin": "424242",
    "caid": "ACMEFITCAID01",
    "arns": null
  },
  "created_at": "2026-02-17T10:04:00Z",
  "updated_at": "2026-02-17T10:04:00Z",
  "note": null,
  "links": [{"rel": "self", "uri": "/v2/enrolments/enrl_rdr123"}]
}
```

ARN-based example:

```json
{
  "merchant_ids": ["mrch_abc123"],
  "type": "VERIFI_RDR",
  "verifi_rdr": {
    "arns": ["74027012345678901234567"]
  }
}
```

Save the RDR `enrolment_id`. You will need it if the customer wants custom RDR decisioning through a ruleset.
{% endtab %}
{% endtabs %}

{% hint style="info" %}
RDR alerts resolve at the network level. By default, eligible RDR cases for an enrolled BIN + CAID are accepted/refunded. Configure an RDR ruleset before go-live only when the customer wants exceptions to that default flow.
{% endhint %}
{% endstep %}

{% step %}

### Enable the TEST enrolment for simulations

Simulation endpoints work only in TEST mode. Mark each TEST enrolment you want to simulate as `ENABLED`.

```bash
curl -X PATCH "$CBS_API_BASE/v1/simulate/enrolments/enrl_ethoca123" \
  -H "Authorization: Bearer $CBS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "ENABLED"
  }'
```

For a Verifi RDR TEST enrolment, use the RDR `enrolment_id` in the same endpoint.
{% endstep %}

{% step %}

### Submit a COMPLETE order

The Orders API accepts a JSON array, even when you submit one order.

```bash
curl -X POST "$CBS_API_BASE/v1/orders/" \
  -H "Authorization: Bearer $CBS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '[
    {
      "type": "COMPLETE",
      "organisation_id": "org_abc123",
      "integration_id": "int_abc123",
      "reference_id": "order-10001",
      "order_datetime": "2026-02-17T09:59:00Z",
      "order_number": "AF-10001",
      "order_subtotal_amount_in_cents": 4900,
      "order_currency": "USD",
      "order_total_amount_in_cents": 4900,
      "order_status": "CLOSED_COMPLETE",
      "customer_email": "casey.jordan@example.com",
      "transactions": [
        {
          "reference_id": "txn-10001",
          "amount_in_cents": 4900,
          "currency": "USD",
          "payment_method_type": "CARD",
          "authorisation_status": "SETTLED",
          "payment_method_reference_id": "pay_10001",
          "authorised_at": "2026-02-17T09:59:05Z",
          "descriptor": "ACMEFIT MONTHLY",
          "payment_method_card_brand": "VISA",
          "payment_method_card_last_4": "4242",
          "payment_method_card_bin": "424242",
          "authorisation_code": "123456",
          "acquirer_reference_number": "74027012345678901234567"
        }
      ]
    }
  ]'
```

Example response:

```json
{
  "created": 1,
  "failed": 0,
  "results": [
    {
      "id": "ord_abc123",
      "reference_id": "order-10001",
      "type": "COMPLETE",
      "organisation_id": "org_abc123",
      "integration_id": "int_abc123",
      "created_at": "2026-02-17T10:05:00Z",
      "transactions": [
        {
          "reference_id": "txn-10001",
          "amount_in_cents": 4900,
          "currency": "USD",
          "payment_method_type": "CARD",
          "authorisation_status": "SETTLED",
          "payment_method_reference_id": "pay_10001",
          "authorised_at": "2026-02-17T09:59:05Z",
          "payment_method_card_brand": "VISA",
          "payment_method_card_last_4": "4242",
          "payment_method_card_bin": "424242",
          "authorisation_code": "123456",
          "acquirer_reference_number": "74027012345678901234567"
        }
      ],
      "links": [{"rel": "self", "uri": "/v1/orders/ord_abc123"}]
    }
  ],
  "errors": []
}
```

Save `order_id`.
{% endstep %}

{% step %}

### Simulate an actionable chargeback alert

Use this to test alert webhooks and alert resolution. The simulator creates a test alert and triggers the same `alert.created` webhook flow as a non-simulated alert.

```bash
curl -X POST "$CBS_API_BASE/v1/simulate/alerts" \
  -H "Authorization: Bearer $CBS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "organisation_id": "org_abc123",
    "enrolment_id": "enrl_ethoca123",
    "status": "ACTION_REQUIRED",
    "card_scheme": "VISA",
    "amount_in_cents": 4900,
    "currency_code": "USD",
    "transaction_acquirer_reference_number": "74027012345678901234567",
    "transaction_authorisation_code": "123456"
  }'
```

Example response:

```json
{
  "id": "netalrt_abc123",
  "alert_network_id": "SIM123456",
  "organisation_id": "org_abc123",
  "merchant_id": "mrch_abc123",
  "enrolment_id": "enrl_ethoca123",
  "enrolment_type": "ETHOCA_ALERT",
  "status": "ACTION_REQUIRED",
  "transaction_amount_in_cents": 4900,
  "transaction_currency_code": "USD",
  "transaction_authorised_at": "2026-02-10T10:30:00Z",
  "action_required_deadline": "2026-02-19T10:30:00Z",
  "transaction_authorisation_code": "123456",
  "transaction_acquirer_reference_number": "74027012345678901234567",
  "transaction_statement_descriptor": "ACMEFIT",
  "transaction_card_bin": "400000",
  "transaction_card_last4": "1234",
  "transaction_card_scheme": "VISA",
  "transaction_card_issuer": "Bank of America",
  "transaction_refund_outcome": null,
  "subscription_cancel_outcome": null,
  "note": null,
  "integration_id": null,
  "integration_transaction_id": null
}
```

{% hint style="warning" %}
The alert simulator validates alert API behavior, webhook delivery, and resolution flows. It does not perform Orders API transaction matching inline, so `integration_id` and `integration_transaction_id` can be `null` in simulator responses. For production matching readiness, also verify that real or ChargebackStop-coordinated matched test alerts include these fields when they match your uploaded orders.
{% endhint %}
{% endstep %}

{% step %}

### Resolve the simulated alert

For a Custom Orders-only integration, ChargebackStop cannot refund through your processor. Your system should perform the refund, cancellation, or customer handling in your own platform first, then update the alert with the outcome.

If you refunded the customer:

```bash
curl -X PATCH "$CBS_API_BASE/v1/alerts/netalrt_abc123" \
  -H "Authorization: Bearer $CBS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "REFUND",
    "note": "Refund completed in partner platform before actioning alert."
  }'
```

If you want to accept/fight the dispute and did not refund:

```bash
curl -X PATCH "$CBS_API_BASE/v1/alerts/netalrt_abc123" \
  -H "Authorization: Bearer $CBS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "ACCEPT_DISPUTE",
    "note": "Merchant chose not to refund this alert."
  }'
```

Example response:

```json
{
  "id": "netalrt_abc123",
  "organisation_id": "org_abc123",
  "merchant_id": "mrch_abc123",
  "enrolment_id": "enrl_ethoca123",
  "enrolment_type": "ETHOCA_ALERT",
  "status": "RESOLVED",
  "transaction_refund_outcome": "REFUNDED",
  "note": "Refund completed in partner platform before actioning alert.",
  "links": [{"rel": "self", "uri": "/v1/alerts/netalrt_abc123"}]
}
```

{% endstep %}
{% endstepper %}

## Webhook implementation

Create a partner webhook endpoint for:

* `enrolment.created`
* `enrolment.updated`
* `alert.created`
* `alert.updated`

Webhook endpoints must be HTTPS and should return a 2xx response within 20 seconds. Use the `X-Idempotency-Key` header and the event `id` to prevent duplicate processing. Retries use the same `X-Idempotency-Key`.

{% hint style="info" %}
Webhook payloads put the domain object in `data.object`. For enrolment events, `data.object` matches the Enrolments API response shape and `api_version` is `v2`. For alert events, `data.object` matches the Alerts API response shape.
{% endhint %}

### Verify signatures

[To verify please refer to the Webhooks documentation.](/developer/partner-integration-guide/webhooks#signature)

### Process enrolment webhooks

Use enrolment webhooks to manage whether a customer enrolment is live in your product.

Example `enrolment.updated` payload:

```json
{
  "id": "evt_enrolmentupdated123",
  "type": "enrolment.updated",
  "created_at": "2026-02-17T10:30:00Z",
  "data": {
    "object": {
      "id": "enrl_rdr123",
      "organisation_id": "org_abc123",
      "merchant_ids": ["mrch_abc123"],
      "type": "VERIFI_RDR",
      "status": "ENABLED",
      "verifi_rdr": {
        "bin": "424242",
        "caid": "ACMEFITCAID01",
        "arns": null
      },
      "created_at": "2026-02-17T10:04:00Z",
      "updated_at": "2026-02-17T10:30:00Z",
      "note": null
    },
    "previous_attributes": {
      "status": "PENDING"
    }
  },
  "api_version": "v2"
}
```

Recommended handling:

| Enrolment status                    | Partner system behavior                                                                                                |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `PENDING`                           | Store the enrolment and show it as submitted, not live.                                                                |
| `IN_PROGRESS`                       | Show it as being configured by ChargebackStop or the alert provider.                                                   |
| `ACTION_REQUIRED`                   | Block go-live for that enrolment and show the customer-facing `note` to your operations team or customer success team. |
| `ENABLED`                           | Mark the enrolment live. Start expecting alerts for that product and identifier.                                       |
| `FAILED`                            | Mark the enrolment failed and escalate with the `note` and enrolment ID.                                               |
| `CANCELLED`, `PAUSED`, `UNENROLLED` | Mark the enrolment inactive. Stop treating the identifier as live for new alert traffic.                               |

{% hint style="warning" %}
Webhook delivery can be retried and events may be processed out of order. Before showing an enrolment as live or blocked in a customer-facing workflow, fetch `GET /v2/enrolments/{enrolment_id}` and use that response as the current source of truth.
{% endhint %}

### Process alert webhooks

Use this webhook processing pattern:

1. Verify `X-Signature`.
2. Store event `id`, `type`, `created_at`, `api_version`, and `data.object`.
3. Deduplicate by event `id`. You may also store `X-Idempotency-Key` for delivery tracing.
4. Return 2xx quickly.
5. Process the alert asynchronously.
6. Fetch the latest alert with `GET /v1/alerts/{alert_id}` before taking irreversible action.

Example `alert.created` decision logic:

```pseudo
if alert.status == "ACTION_REQUIRED" and alert.enrolment_type == "ETHOCA_ALERT":
    show alert to merchant or run partner decisioning
    if merchant_refunded_in_partner_platform:
        PATCH /v1/alerts/{id} {"action": "REFUND"}
    else if merchant_accepts_or_fights_dispute:
        PATCH /v1/alerts/{id} {"action": "ACCEPT_DISPUTE"}
else:
    store alert as informational
```

## Alert resolution rules

Ethoca and Verifi RDR resolve differently.

| Provider                       | Alert behavior                                                                  | What your integration must do                                                                                |
| ------------------------------ | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| Ethoca Alerts (`ETHOCA_ALERT`) | Alerts can arrive in `ACTION_REQUIRED` with `action_required_deadline`.         | Decide quickly, perform any off-platform refund/cancellation first, then call `PATCH /v1/alerts/{alert_id}`. |
| Verifi RDR (`VERIFI_RDR`)      | Alerts are resolved at the network level and arrive as `RESOLVED` or `INVALID`. | Configure RDR rules before go-live if needed. Do not expect an action-required workflow.                     |

### Ethoca alert resolution

For Custom Orders-only integrations, the usual manual actions are:

| API action       | Use when                                                                            | Result                                              |
| ---------------- | ----------------------------------------------------------------------------------- | --------------------------------------------------- |
| `REFUND`         | You refunded the transaction in your own platform.                                  | Alert is marked resolved with refunded outcome.     |
| `ACCEPT_DISPUTE` | You did not refund and will accept or fight the dispute outside the alert workflow. | Alert is marked resolved with not-refunded outcome. |

{% hint style="danger" %}
For Ethoca, refunding the customer in your own system is not enough. You must also action the alert through ChargebackStop before the deadline so the alert provider receives the outcome.
{% endhint %}

Use `CANCEL` and `REFUND_AND_CANCEL` only when your ChargebackStop setup has the processor/subscription context needed to support those actions, or when ChargebackStop confirms they fit your workflow.

### Verifi RDR default decisioning

For Verifi RDR, the default path is intentionally simple:

1. Create a `VERIFI_RDR` enrolment for the customer's BIN + CAID.
2. Wait until the enrolment becomes `ENABLED`.
3. If no platform-run ruleset is attached to that RDR enrolment, eligible RDR cases for that BIN + CAID are accepted/refunded by default at the network level.

RDR alerts generally arrive in ChargebackStop after the network decision has already happened. They should be stored and reconciled, not routed into an `ACTION_REQUIRED` queue.

### Customise Verifi RDR with a ruleset

Create a platform-run ruleset only when the customer wants exceptions to the default RDR refund flow. Attach the ruleset to the RDR `enrolment_id`.

RDR ruleset constraints:

| Constraint             | Detail                                                                                                                                                                        |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Required API abilities | `rulesets:read` and `rulesets:write`.                                                                                                                                         |
| Endpoint               | `POST /v1/rulesets/`.                                                                                                                                                         |
| Enrolments             | Use one or more `VERIFI_RDR` BIN + CAID enrolment IDs from the same organisation. Confirm with ChargebackStop before relying on custom rulesets for ARN-based RDR enrolments. |
| Ruleset count          | Each enrolment can have one platform-run ruleset.                                                                                                                             |
| Outcomes for RDR       | Use `REFUND` or `ACCEPT_DISPUTE`. `REFUND_AND_CANCEL` is published to Verifi as `REFUND`; handle cancellation in your own system. Do not use `CANCEL` for RDR decisioning.    |
| Rule types             | `AMOUNT` and `DESCRIPTOR`. Only one rule of each type is allowed per ruleset.                                                                                                 |
| Amount currency        | Must be `USD`.                                                                                                                                                                |

Example: keep the default refund behavior for most RDR cases, but accept the dispute instead of refunding when the amount is greater than $100 or the descriptor starts with `ACMEFIT HIGHVALUE`.

```bash
curl -X POST "$CBS_API_BASE/v1/rulesets/" \
  -H "Authorization: Bearer $CBS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "organisation_id": "org_abc123",
    "enrolment_ids": ["enrl_rdr123"],
    "outcome": "ACCEPT_DISPUTE",
    "join_operator": "OR",
    "rules": [
      {
        "type": "AMOUNT",
        "parameters": {
          "operator": "GREATER_THAN",
          "currency_code": "USD",
          "amount_in_cents": 10000
        }
      },
      {
        "type": "DESCRIPTOR",
        "parameters": {
          "descriptors": [
            {
              "value": "ACMEFIT HIGHVALUE",
              "match_type": "STARTS_WITH"
            }
          ]
        }
      }
    ]
  }'
```

Example response:

```json
{
  "id": "rset_abc123",
  "organisation_id": "org_abc123",
  "enrolment_ids": ["enrl_rdr123"],
  "outcome": "ACCEPT_DISPUTE",
  "join_operator": "OR",
  "rules": [
    {
      "id": "resrule_amount123",
      "type": "AMOUNT",
      "parameters": {
        "operator": "GREATER_THAN",
        "currency_code": "USD",
        "amount_in_cents": 10000
      },
      "created_at": "2026-02-17T10:10:00Z",
      "updated_at": "2026-02-17T10:10:00Z"
    },
    {
      "id": "resrule_descriptor123",
      "type": "DESCRIPTOR",
      "parameters": {
        "descriptors": [
          {
            "value": "ACMEFIT HIGHVALUE",
            "match_type": "STARTS_WITH"
          }
        ]
      },
      "created_at": "2026-02-17T10:10:00Z",
      "updated_at": "2026-02-17T10:10:00Z"
    }
  ],
  "created_at": "2026-02-17T10:10:00Z",
  "updated_at": "2026-02-17T10:10:00Z"
}
```

To update an existing ruleset:

* Use `PATCH /v1/rulesets/{ruleset_id}` for `enrolment_ids`, `outcome`, or `join_operator`.
* Use `PATCH /v1/rulesets/{ruleset_id}/rules/{rule_id}` for rule criteria.
* Do not send `rules` to `PATCH /v1/rulesets/{ruleset_id}`; rule updates are handled by the nested rule endpoints.

{% hint style="warning" %}
Create and verify RDR rulesets before production go-live. Enrolment and ruleset changes are published to the RDR decisioning path asynchronously, so do not make last-minute rule changes during launch without coordinating with ChargebackStop.
{% endhint %}

## Order update patterns

Create orders once with `POST /v1/orders/`. Use `PATCH /v1/orders/{order_id}` to add or update later lifecycle data such as refunds, disputes, delivery status, subscription status, or order communications.

Example refund update:

```bash
curl -X PATCH "$CBS_API_BASE/v1/orders/ord_abc123" \
  -H "Authorization: Bearer $CBS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "refunds": [
      {
        "reference_id": "refund-10001",
        "amount_in_cents": 4900,
        "currency": "USD",
        "status": "SUCCEEDED",
        "original_transaction_reference_id": "txn-10001",
        "refund_datetime": "2026-02-17T11:20:00Z"
      }
    ]
  }'
```

{% hint style="warning" %}
Do not generate a new `reference_id` when retrying the same order, transaction, refund, dispute, delivery, item, or subscription. Stable references let you reconcile duplicates and avoid creating conflicting records.
{% endhint %}

## Error handling and retries

The Orders API uses partial success after schema validation. One valid order in a batch can be created even if another order in the same batch fails.

Handle these cases explicitly:

| Case                    | Recommended handling                                                                                               |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `429 RATE_LIMITED`      | Retry with exponential backoff. Limits are 100 requests per minute per endpoint per organisation or partner group. |
| `DUPLICATE_ORDER`       | Treat as a reconciliation case. Fetch the order with `GET /v1/orders/?reference_id=<reference_id>`.                |
| `INVALID_ORDER_TYPE`    | Ensure `COMPLETE` orders use a `CUSTOM_ORDERS` integration.                                                        |
| `INVALID_INTEGRATION`   | Check `integration_id`, organisation access, and integration status.                                               |
| `MISSING_FIELD`         | Validate payloads before enqueueing. Required fields can depend on order mode and enabled product features.        |
| `422` schema validation | No orders were processed. Fix the top-level request shape or field types and retry.                                |
| `5xx`                   | Retry safely with the same `reference_id` values.                                                                  |

## Sandbox acceptance checklist

Complete this checklist before asking ChargebackStop to move you to production.

* [ ] TEST partner API key is stored in a secrets manager.
* [ ] TEST webhook endpoint receives and verifies `enrolment.created`, `enrolment.updated`, `alert.created`, and `alert.updated`.
* [ ] Webhook processing is idempotent by event `id`.
* [ ] Your API client handles `401`, `403`, `404`, `422`, `429`, and `5xx` responses.
* [ ] You can create a TEST organisation through `POST /v1/organisations/`.
* [ ] You can create a merchant through `POST /v1/merchants/`.
* [ ] You can create a `CUSTOM_ORDERS` integration through `POST /v1/integrations/`.
* [ ] You can create an Ethoca Alert enrolment through `POST /v2/enrolments/`.
* [ ] If using RDR, you can create a Verifi RDR enrolment with BIN + CAID or ARNs through `POST /v2/enrolments/`.
* [ ] You can enable a TEST enrolment with `PATCH /v1/simulate/enrolments/{enrolment_id}`.
* [ ] Your system marks an enrolment live only after `status: "ENABLED"` from `enrolment.updated` or `GET /v2/enrolments/{enrolment_id}`.
* [ ] You can submit a `COMPLETE` order through `POST /v1/orders/`.
* [ ] You can add refund or dispute lifecycle updates through `PATCH /v1/orders/{order_id}`.
* [ ] You can create a simulated `ACTION_REQUIRED` alert.
* [ ] You can resolve an Ethoca alert with `REFUND`.
* [ ] You can resolve an Ethoca alert with `ACCEPT_DISPUTE`.
* [ ] If using custom RDR decisioning, you can create a ruleset through `POST /v1/rulesets/` and attach it to the RDR `enrolment_id`.
* [ ] Your UI or operations queue shows `action_required_deadline`.
* [ ] Your system never relies on TEST IDs in production configuration.

## Production launch checklist

When sandbox testing is complete, ChargebackStop will provision or enable your LIVE production partner account.

{% stepper %}
{% step %}

### Create production credentials

In the LIVE partner account:

1. Create a new production partner API key.
2. Store it in your production secrets manager.
3. Register a production HTTPS webhook endpoint.
4. Select `enrolment.created`, `enrolment.updated`, `alert.created`, and `alert.updated`.
5. Reveal and store the webhook signing secret.

Do not reuse TEST API keys or TEST webhook secrets.
{% endstep %}

{% step %}

### Create production customer records

For each production customer:

1. Create the organisation.
2. Create the merchant or merchants.
3. Create the `CUSTOM_ORDERS` integration.
4. Save all returned IDs in your production database.

Production organisation mode will be `LIVE`.
{% endstep %}

{% step %}

### Submit production order history

Before alert traffic begins, backfill recent complete order history for each customer. Include transaction matching identifiers, refunds, and disputes wherever available.

Monitor:

* Orders accepted vs failed
* Duplicate references
* Missing ARN/auth code/BIN coverage
* Rate limiting
* Payload validation errors
  {% endstep %}

{% step %}

### Create real chargeback alert enrolments

Create real Ethoca Alert and/or Verifi RDR enrolments using production descriptors, BIN/CAID, or ARNs.

For RDR, create any custom rulesets before go-live if the customer does not want the default accept/refund flow for every eligible BIN + CAID case.

Enrolments are created in `PENDING`. Treat an enrolment as live only after `enrolment.updated` or `GET /v2/enrolments/{enrolment_id}` returns `status: "ENABLED"`.
{% endstep %}

{% step %}

### Verify first production alerts

For the first real alerts:

1. Confirm your webhook receives the enrolment events and marks each live enrolment `ENABLED`.
2. Fetch the latest alert via `GET /v1/alerts/{alert_id}`.
3. Check whether `integration_id` and `integration_transaction_id` are present for matched alerts.
4. Confirm your refund/accept-dispute workflow resolves Ethoca alerts before `action_required_deadline`.
5. Confirm `alert.updated` arrives after resolution.

Escalate unmatched alerts to ChargebackStop with the alert ID, order reference, transaction reference, ARN, auth code, card brand, BIN, last 4, amount, currency, and timestamps.
{% endstep %}
{% endstepper %}

## Operational recommendations

* Send orders continuously, not only after an alert is received.
* Keep webhook handlers fast. Verify, persist, enqueue, and return 2xx.
* Use `enrolment.updated` and `GET /v2/enrolments/{enrolment_id}` to drive customer-facing enrolment status.
* Poll `GET /v1/alerts?status=ACTION_REQUIRED` as a fallback if webhook delivery is interrupted.
* Alert your operations team well before `action_required_deadline`.
* Treat `REFUND` as a declaration that your system has already refunded when using Custom Orders without a processor integration.
* Create RDR rulesets before go-live when the customer wants exceptions to the default RDR accept/refund behavior.
* Keep TEST and LIVE credentials, webhook secrets, IDs, and queues separate.
* Rotate API keys immediately after suspected exposure.


# Merchant Integration Guides


# Orders API Partial Enrichment Merchant Guide

Use this guide when you already have a payment processor integration, such as Stripe, connected to ChargebackStop and you want to enrich those processor transactions with order, customer, device, product, delivery, and evidence data.

The examples throughout this guide use Stripe, but the same `PARTIAL` order pattern applies to other existing processor integrations where ChargebackStop already ingests the payment data.

{% hint style="info" %}
Use `PARTIAL` orders only with existing non-`CUSTOM_ORDERS` integrations, for example `STRIPE`. If you do not have a payment processor integration and need to upload complete order and transaction data yourself, use a `CUSTOM_ORDERS` integration with `COMPLETE` orders instead.
{% endhint %}

## Reference documentation

* [Orders API](https://docs.chargebackstop.com/developer/api-documentation/orders)
* [Integrations API](https://docs.chargebackstop.com/developer/api-documentation/integrations)
* [Lookups API](https://docs.chargebackstop.com/developer/api-documentation/lookups)
* [Simulations API](https://docs.chargebackstop.com/developer/api-documentation/simulations)
* [Webhooks](https://docs.chargebackstop.com/developer/partner-integration-guide/webhooks)

## What partial enrichment does

With Stripe connected, ChargebackStop already receives the payment record: amount, currency, card details, authorisation data, descriptor, refunds, and disputes where available.

`PARTIAL` orders add the commerce context that is often missing from the processor record:

| Data area                  | Why it matters                                                                                                              |
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| Customer identity          | Helps link repeat legitimate purchases for systematic deflection and digital receipt products.                              |
| Device identity            | Required for `PARTIAL` orders, First-Party Trust, and CE3.0. Stable device identifiers improve historical evidence quality. |
| Order details              | Makes the transaction recognisable and helps answer issuer/cardholder lookup requests.                                      |
| Products and delivery      | Strengthens the receipt and evidence package returned for the matched transaction.                                          |
| Consent and communications | Adds dispute context, such as terms accepted, checkout notices, and support communications.                                 |

ChargebackStop combines the synced processor transaction with your enriched order record when responding to digital receipt or systematic deflection lookup requests.

## Before you start

You need:

| Requirement                       | Notes                                                                                                                                                                                  |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A connected Stripe integration    | The integration must already be syncing successful Stripe payments.                                                                                                                    |
| Orders enrichment enabled         | ChargebackStop must enable orders enrichment on the Stripe integration. Otherwise `PARTIAL` order creation returns `ENRICHMENT_NOT_ENABLED`. Ask support to make sure this is enabled. |
| API key abilities                 | Use `orders:write` to create enrichment records, `orders:read` to verify them, and `orders:update` if you will PATCH delivery/refund/dispute updates.                                  |
| Relevant lookup enrolment enabled | ChargebackStop must enable the relevant digital receipt or systematic deflection enrolment for the merchant, such as Consumer Clarity, Order Insight, First-Party Trust, or CE3.0.     |

{% hint style="warning" %}
`PARTIAL` orders cannot be created on a `CUSTOM_ORDERS` integration. If you send `type: "PARTIAL"` with a `CUSTOM_ORDERS` integration, the API returns `INVALID_ORDER_TYPE`.
{% endhint %}

## ID mapping for Stripe

The most important implementation decision is which ID you use as `reference_id`.

For partial enrichment, `reference_id` should identify the processor transaction that ChargebackStop will match during a lookup. In a typical Stripe setup, use the Stripe PaymentIntent ID, for example `pi_3QJ2...`. If your Stripe integration syncs legacy charge-only transactions, use the Stripe charge ID, for example `ch_3QJ2...`.

Continue sending your storefront or commerce order number separately as `order_number`.

| Your system value                                       | Orders API field                           | Example                     |
| ------------------------------------------------------- | ------------------------------------------ | --------------------------- |
| ChargebackStop organisation ID                          | `organisation_id`                          | `org_abc123`                |
| ChargebackStop Stripe integration ID                    | `integration_id`                           | `int_stripe456`             |
| Stripe transaction identifier matched by ChargebackStop | `reference_id`                             | `pi_3QJ2KRFSLReU4y0Example` |
| Customer-facing order number                            | `order_number`                             | `SHOP-100045`               |
| Stable merchant customer ID                             | `customer_account_id`                      | `cus_internal_789`          |
| Checkout email                                          | `order_email` and usually `customer_email` | `customer@example.com`      |

{% hint style="info" %}
If you are not sure whether your Stripe integration is matching on PaymentIntent IDs or charge IDs, confirm with ChargebackStop before backfilling. The `reference_id` must match the processor transaction entity ID ChargebackStop has ingested.
{% endhint %}

## Minimum valid `PARTIAL` order

Every create request is a JSON array, even when you submit one order.

{% tabs %}
{% tab title="Request" %}

```bash
curl -X POST "https://api.chargebackstop.com/v1/orders/" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '[
    {
      "type": "PARTIAL",
      "organisation_id": "org_abc123",
      "integration_id": "int_stripe456",
      "reference_id": "pi_3QJ2KRFSLReU4y0Example",
      "order_email": "customer@example.com",
      "customer_account_id": "cus_internal_789",
      "device_ip_address": "216.24.60.17"
    }
  ]'
```

{% endtab %}

{% tab title="Required fields" %}

| Field                 | Required value                                                                                           |
| --------------------- | -------------------------------------------------------------------------------------------------------- |
| `type`                | Must be `PARTIAL`.                                                                                       |
| `organisation_id`     | The ChargebackStop organisation that owns the Stripe integration.                                        |
| `integration_id`      | The existing Stripe integration ID.                                                                      |
| `reference_id`        | The processor transaction identifier ChargebackStop can match.                                           |
| `order_email`         | Email associated with the order.                                                                         |
| `customer_account_id` | Stable customer identifier. Use your customer ID where possible, or email if there is no account system. |
| Device identifier     | At least one of `device_ip_address`, `device_id`, or `device_fingerprint`.                               |
| {% endtab %}          |                                                                                                          |
| {% endtabs %}         |                                                                                                          |

{% hint style="warning" %}
Create the enrichment record as early as possible, ideally at successful checkout or fulfilment. The current PATCH endpoint updates delivery, subscriptions, refunds, disputes, order status, and `order_communications`; it does not update customer email, account ID, device identifiers, items, transactions, or core order totals after creation.
{% endhint %}

## What to send by product

The [Orders API reference](https://docs.chargebackstop.com/developer/api-documentation/orders) defines the exact `PARTIAL` field sets for each product. This guide shows how to apply those field sets when your payment data is already coming from Stripe.

| **Product group**     | **Lookup type**                | **Purpose**                                    | **Add these fields on top of the `PARTIAL` minimum**                                                                                      |
| --------------------- | ------------------------------ | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| Digital receipts      | `ETHOCA_CONSUMER_CLARITY`      | Mastercard Consumer Clarity receipt enrichment | `order_datetime`, `order_number`, `order_subtotal_amount_in_cents`, `order_currency`, `order_total_amount_in_cents`.                      |
| Digital receipts      | `VERIFI_ORDER_INSIGHT`         | Visa Order Insight receipt enrichment          | At least one `items[]` entry with `name`; include product, merchant, delivery, and order details when available.                          |
| Systematic deflection | `ETHOCA_FIRST_PARTY_TRUST`     | Mastercard First-Party Trust                   | `customer_email`, `order_email`, and at least one device identifier.                                                                      |
| Systematic deflection | `VERIFI_COMPELLING_EVIDENCE_3` | Visa CE3.0                                     | `customer_email` and at least one device identifier; include historical order, item, delivery, merchant, consent, and communication data. |

{% hint style="info" %}
You can send one enriched `PARTIAL` order that supports more than one product. For example, a single payload can include Consumer Clarity order totals, Order Insight item data, and CE3.0 customer/device data.
{% endhint %}

## Digital receipt examples

Digital receipts help cardholders recognise legitimate purchases in issuer experiences. These examples add receipt context to a Stripe PaymentIntent while Stripe remains the source of the payment transaction.

{% tabs %}
{% tab title="Consumer Clarity" %}
Use this when the merchant is enrolled for Mastercard Consumer Clarity.

```json
[
  {
    "type": "PARTIAL",
    "organisation_id": "org_abc123",
    "integration_id": "int_stripe456",
    "reference_id": "pi_3QJ2KRFSLReU4y0Example",

    "order_email": "ava.chen@example.com",
    "customer_account_id": "cus_internal_789",
    "device_ip_address": "216.24.60.229",

    "order_datetime": "2026-06-15T10:30:00Z",
    "order_number": "SHOP-100045",
    "order_subtotal_amount_in_cents": 12900,
    "order_tax_amount_in_cents": 1032,
    "order_total_amount_in_cents": 13932,
    "order_currency": "USD",
    "order_status": "CLOSED_COMPLETE",

    "merchant_name": "Example Store Inc.",
    "merchant_store_name": "Example Store",
    "merchant_customer_service_email": "support@example.com",
    "merchant_contact_phone": "+14155551234",
    "merchant_url": "https://store.example.com"
  }
]
```

{% endtab %}

{% tab title="Order Insight" %}
Use this when the merchant is enrolled for Visa Order Insight.

```json
[
  {
    "type": "PARTIAL",
    "organisation_id": "org_abc123",
    "integration_id": "int_stripe456",
    "reference_id": "pi_3QJ2KRFSLReU4y0Example",

    "order_email": "ava.chen@example.com",
    "customer_account_id": "cus_internal_789",
    "device_ip_address": "216.24.60.229",

    "order_datetime": "2026-06-15T10:30:00Z",
    "order_number": "SHOP-100045",
    "order_total_amount_in_cents": 13932,
    "order_currency": "USD",
    "order_view_url": "https://store.example.com/account/orders/SHOP-100045",

    "merchant_name": "Example Store Inc.",
    "merchant_store_name": "Example Store",
    "merchant_contact_phone": "+14155551234",
    "merchant_url": "https://store.example.com",

    "deliveries": [
      {
        "reference_id": "dlv_SHOP-100045_1",
        "type": "PHYSICAL",
        "physical_shipping_carrier": "UPS",
        "physical_shipping_tracking_number": "1Z999AA10123456784",
        "physical_shipping_status": "SHIPPED",
        "physical_shipping_datetime_shipped": "2026-06-16T08:00:00Z"
      }
    ],

    "items": [
      {
        "reference_id": "item_SHOP-100045_1",
        "name": "Premium Subscription - Annual",
        "price_in_cents": 12900,
        "quantity": 1,
        "sku": "PREMIUM-ANNUAL",
        "product_reference_id": "prod_premium_annual",
        "product_url": "https://store.example.com/products/premium",
        "delivery_reference_id": "dlv_SHOP-100045_1"
      }
    ]
  }
]
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
Visa Order Insight responses cap the returned order number to 25 characters. Keep `order_number` recognisable within that limit where possible.
{% endhint %}

## Systematic deflection examples

Systematic deflection products rely on a history of legitimate purchases. Send enrichment continuously for every eligible order, not only after a dispute or lookup happens.

{% tabs %}
{% tab title="First-Party Trust" %}
Use this when the merchant is enrolled for Mastercard First-Party Trust.

```json
[
  {
    "type": "PARTIAL",
    "organisation_id": "org_abc123",
    "integration_id": "int_stripe456",
    "reference_id": "pi_3QJ2KRFSLReU4y0Example",

    "customer_email": "ava.chen@example.com",
    "customer_account_id": "cus_internal_789",
    "order_email": "ava.chen@example.com",

    "device_ip_address": "216.24.60.229",
    "device_id": "ios-device-9f4b2a",
    "device_fingerprint": "fp_4e1f6d8c2b9a",

    "order_datetime": "2026-06-15T10:30:00Z",
    "order_number": "SHOP-100045",
    "order_total_amount_in_cents": 13932,
    "order_currency": "USD"
  }
]
```

{% endtab %}

{% tab title="CE3.0" %}
Use this when the merchant is enrolled for Visa CE3.0.

```json
[
  {
    "type": "PARTIAL",
    "organisation_id": "org_abc123",
    "integration_id": "int_stripe456",
    "reference_id": "pi_3QJ2KRFSLReU4y0Example",

    "customer_email": "ava.chen@example.com",
    "customer_first_name": "Ava",
    "customer_last_name": "Chen",
    "customer_account_id": "cus_internal_789",
    "order_email": "ava.chen@example.com",

    "device_ip_address": "216.24.60.229",
    "device_id": "ios-device-9f4b2a",
    "device_fingerprint": "fp_4e1f6d8c2b9a",

    "order_datetime": "2026-06-15T10:30:00Z",
    "order_number": "SHOP-100045",
    "order_subtotal_amount_in_cents": 12900,
    "order_tax_amount_in_cents": 1032,
    "order_total_amount_in_cents": 13932,
    "order_currency": "USD",
    "order_status": "CLOSED_COMPLETE",
    "order_view_url": "https://store.example.com/account/orders/SHOP-100045",
    "order_proof_of_consent": "Customer accepted Terms of Service and refund policy during checkout.",
    "order_communications": "Order confirmation email sent 2026-06-15. Shipment confirmation sent 2026-06-16.",

    "merchant_reference_id": "store_us",
    "merchant_name": "Example Store Inc.",
    "merchant_store_name": "Example Store",
    "merchant_customer_service_email": "support@example.com",
    "merchant_contact_phone": "+14155551234",
    "merchant_url": "https://store.example.com",
    "merchant_refund_policy_url": "https://store.example.com/refunds",

    "deliveries": [
      {
        "reference_id": "dlv_SHOP-100045_1",
        "type": "PHYSICAL",
        "physical_shipping_carrier": "UPS",
        "physical_shipping_tracking_number": "1Z999AA10123456784",
        "physical_shipping_status": "SHIPPED",
        "physical_shipping_datetime_shipped": "2026-06-16T08: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_SHOP-100045_1",
        "name": "Premium Subscription - Annual",
        "price_in_cents": 12900,
        "quantity": 1,
        "sku": "PREMIUM-ANNUAL",
        "product_reference_id": "prod_premium_annual",
        "product_url": "https://store.example.com/products/premium",
        "delivery_reference_id": "dlv_SHOP-100045_1"
      }
    ]
  }
]
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
You usually do not need to submit a `transactions` array for Stripe partial enrichment because the Stripe integration already supplies the transaction data. If ChargebackStop asks you to supplement payment details, follow the transaction schema in the [Orders API reference](https://docs.chargebackstop.com/developer/api-documentation/orders#transactions).
{% endhint %}

## Implementation flow

{% stepper %}
{% step %}

### Confirm the integration

List integrations and identify the Stripe integration ID.

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

Use an integration with `type: "STRIPE"` and `status: "ENABLED"`.
{% endstep %}

{% step %}

### Enable enrichment and product enrolments

Ask ChargebackStop to enable orders enrichment for the Stripe integration. Also confirm the merchant's required digital receipt or systematic deflection enrolments are enabled.
{% endstep %}

{% step %}

### Choose the `reference_id`

Use the Stripe PaymentIntent ID for PaymentIntent-based transactions, or the Stripe charge ID for legacy charge-only transactions. Send the customer-facing commerce order ID as `order_number`.
{% endstep %}

{% step %}

### Create enrichment at checkout or fulfilment

POST one or more `PARTIAL` orders to `/v1/orders/`. Send up to 100 orders per request.
{% endstep %}

{% step %}

### Store the ChargebackStop order ID

The create response returns the ChargebackStop `ord_...` ID. Store it so you can PATCH delivery, refund, dispute, or communication updates later.
{% endstep %}

{% step %}

### Verify the record

Use `GET /v1/orders?reference_id=<stripe_transaction_id>` to confirm the enrichment record exists and contains the expected customer, device, product, and delivery data.
{% endstep %}
{% endstepper %}

## Handling creates, retries, and duplicates

The create endpoint supports partial success within a batch. Some orders can be created while other orders in the same request fail validation.

| Response field | Meaning                                                                                             |
| -------------- | --------------------------------------------------------------------------------------------------- |
| `created`      | Number of order records created.                                                                    |
| `results`      | Created order objects. Store each returned `id`.                                                    |
| `failed`       | Number of order records that failed.                                                                |
| `errors`       | Per-order error details, including the array index, `reference_id`, error code, message, and field. |

Use an idempotent retry pattern:

1. Generate the same `reference_id` for the same Stripe transaction every time.
2. If the first request times out, query `GET /v1/orders?reference_id=<reference_id>` before retrying.
3. If the order exists, treat the create as successful.
4. If the order does not exist, retry the POST.
5. If a retry returns `DUPLICATE_ORDER`, query the order and continue.

Common errors:

| Error code               | Cause                                                                                                   | Fix                                                                                                              |
| ------------------------ | ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `INVALID_INTEGRATION`    | The `integration_id` does not belong to the organisation or is not accessible to the key.               | Check `organisation_id`, `integration_id`, and API key scope.                                                    |
| `INVALID_ORDER_TYPE`     | `PARTIAL` was sent to a `CUSTOM_ORDERS` integration, or `COMPLETE` was sent to a processor integration. | Use `PARTIAL` with Stripe or another existing processor integration.                                             |
| `ENRICHMENT_NOT_ENABLED` | Orders enrichment is not enabled for the integration.                                                   | Ask ChargebackStop to enable it.                                                                                 |
| `DUPLICATE_ORDER`        | The same `reference_id` already exists for the integration.                                             | Treat as already created after verifying with the GET endpoint.                                                  |
| `MISSING_FIELD`          | A required field is missing, or an enabled product validation rule requires an additional field.        | Add the field shown in the error.                                                                                |
| `429 Too Many Requests`  | Rate limit exceeded.                                                                                    | Back off and retry. The create endpoint is limited to 100 requests per minute per organisation or partner group. |

## Updating enriched orders

Use PATCH only for fields the update endpoint supports.

| Update                                         | Supported by PATCH? | Notes                                                              |
| ---------------------------------------------- | ------------------- | ------------------------------------------------------------------ |
| `order_status`                                 | Yes                 | Include `order_status_other_description` when status is `OTHER`.   |
| `order_communications`                         | Yes                 | Useful for late support contact or dispute notes.                  |
| Delivery status and tracking                   | Yes                 | Existing deliveries are updated by `reference_id`.                 |
| Refunds                                        | Yes                 | New refunds can be created, existing refund status can be updated. |
| Disputes                                       | Yes                 | New disputes can be created, existing stage/status can be updated. |
| Customer email, account ID, device identifiers | No                  | Send these in the original POST.                                   |
| Items and transactions                         | No                  | Send these in the original POST if needed.                         |

Example delivery completion update:

```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_SHOP-100045_1",
        "physical_shipping_status": "DELIVERED",
        "physical_shipping_datetime_delivered": "2026-06-18T14:30:00Z"
      }
    ],
    "order_communications": "Order confirmation email sent 2026-06-15. Shipment confirmation sent 2026-06-16. Delivery confirmation sent 2026-06-18."
  }'
```

## Verifying digital receipt and deflection outcomes

Digital receipt and systematic deflection activity is exposed through lookup records and lookup webhooks, not chargeback alert webhooks.

Common lookup types:

| Product                      | Lookup `type`                  |
| ---------------------------- | ------------------------------ |
| Mastercard Consumer Clarity  | `ETHOCA_CONSUMER_CLARITY`      |
| Mastercard First-Party Trust | `ETHOCA_FIRST_PARTY_TRUST`     |
| Visa Order Insight           | `VERIFI_ORDER_INSIGHT`         |
| Visa CE3.0                   | `VERIFI_COMPELLING_EVIDENCE_3` |

For Visa:

| Scheme request source | Lookup `type`                  | Deflection behaviour                                                                                                                                  |
| --------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OI` or `OID`         | `VERIFI_ORDER_INSIGHT`         | If CE3.0 is enabled and the transaction is matched, `deflection_status` starts as `PENDING`. A later CE notice updates it to `SUCCEEDED` or `FAILED`. |
| `CE`                  | `VERIFI_COMPELLING_EVIDENCE_3` | Historical evidence lookup. It is not the deflection target, so `deflection_status` remains `NOT_ATTEMPTED`.                                          |

Verifi CE notice notifications update the linked lookup by `linkedInsightId`. A `caseEvent` of `NEW` marks the linked lookup `deflection_status: "SUCCEEDED"`. `FAILED`, `TIMEOUT`, and `DELETE` mark it `FAILED`; `DELETE` can reverse a previous successful deflection.

Subscribe to:

* `lookup.created`
* `lookup.updated`

Use:

* [Lookups API](https://docs.chargebackstop.com/developer/api-documentation/lookups) for polling or reconciliation.
* [Webhooks](https://docs.chargebackstop.com/developer/partner-integration-guide/webhooks) for real-time events.
* [Simulations API](https://docs.chargebackstop.com/developer/api-documentation/simulations) in TEST organisations.

Lookup fields to monitor:

| Field                        | Meaning                                                                      |
| ---------------------------- | ---------------------------------------------------------------------------- |
| `type`                       | One of the lookup types listed above.                                        |
| `lookup_status`              | `PENDING`, `SUCCEEDED`, `FAILED`, or `TIMEOUT`.                              |
| `deflection_status`          | `NOT_ATTEMPTED`, `PENDING`, `SUCCEEDED`, or `FAILED`.                        |
| `integration_id`             | The matched processor integration.                                           |
| `integration_transaction_id` | The matched processor transaction ID, for example a Stripe PaymentIntent ID. |

{% hint style="success" %}
For CE3.0 billing or success metrics, use `deflection_status: "SUCCEEDED"` on the linked lookup as the positive deflection signal. `lookup_status: "SUCCEEDED"` means ChargebackStop matched and answered the lookup; it is not the same as a successful deflection.
{% endhint %}

## Production readiness checklist

* Stripe integration is connected, enabled, and syncing production payments.
* Orders enrichment is enabled on the production Stripe integration.
* Required Consumer Clarity, Order Insight, First-Party Trust, or CE3.0 enrolments are enabled.
* Production API key has `orders:write`, `orders:read`, and optionally `orders:update`.
* Webhook endpoint verifies ChargebackStop signatures and handles retries idempotently.
* `reference_id` maps to the Stripe transaction entity ChargebackStop ingests.
* Customer, device, item, and delivery fields are sent at creation time.
* Backfill strategy is agreed with ChargebackStop before importing historical orders.
* Monitoring alerts on failed creates, `ENRICHMENT_NOT_ENABLED`, `INVALID_ORDER_TYPE`, and webhook delivery failures.
* Reconciliation job checks recent Stripe transactions against ChargebackStop `PARTIAL` orders by `reference_id`.


# 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)


# Stripe

### Do I need to have a Stripe account?

No, we work with all processors and a Stripe integration is not required. Do note, some of our functionality is only available with Stripe currently (billing descriptors etc.)

{% hint style="success" %}
You can skip manual API key setup by using this magic link that pre-fills all required permissions.

[Create ChargebackStop key in Stripe](https://dashboard.stripe.com/apikeys/create?name=ChargebackStop\&permissions%5B%5D=rak_charge_read\&permissions%5B%5D=rak_charge_write\&permissions%5B%5D=rak_customer_read\&permissions%5B%5D=rak_event_read\&permissions%5B%5D=rak_payment_intent_read\&permissions%5B%5D=rak_payment_intent_write\&permissions%5B%5D=rak_payment_method_read\&permissions%5B%5D=rak_product_read\&permissions%5B%5D=rak_plan_read\&permissions%5B%5D=rak_subscription_read\&permissions%5B%5D=rak_subscription_write\&permissions%5B%5D=rak_credit_note_read\&permissions%5B%5D=rak_credit_note_write\&permissions%5B%5D=rak_invoice_write\&permissions%5B%5D=rak_webhook_write\&permissions%5B%5D=rak_dispute_read\&permissions%5B%5D=rak_dispute_write)
{% endhint %}

### Stripe Integration Instructions

1. Create a new restricted API key in Stripe (<https://dashboard.stripe.com/apikeys>) ensuring that all the required permissions (see the full list below) are selected.
   1. Follow this link to open the API keys page in Stripe with all of our required permissions pre-filled: [Create ChargebackStop key in Stripe](https://dashboard.stripe.com/apikeys/create?name=ChargebackStop\&permissions%5B%5D=rak_charge_read\&permissions%5B%5D=rak_charge_write\&permissions%5B%5D=rak_customer_read\&permissions%5B%5D=rak_event_read\&permissions%5B%5D=rak_payment_intent_read\&permissions%5B%5D=rak_payment_intent_write\&permissions%5B%5D=rak_payment_method_read\&permissions%5B%5D=rak_product_read\&permissions%5B%5D=rak_plan_read\&permissions%5B%5D=rak_subscription_read\&permissions%5B%5D=rak_subscription_write\&permissions%5B%5D=rak_credit_note_read\&permissions%5B%5D=rak_credit_note_write\&permissions%5B%5D=rak_invoice_write\&permissions%5B%5D=rak_webhook_write\&permissions%5B%5D=rak_dispute_read\&permissions%5B%5D=rak_dispute_write)
2. Grab your Stripe account ID from your profile page (<https://dashboard.stripe.com/settings/user>).
3. Login to ChargebackStop and under [**Settings**](https://dashboard.chargebackstop.com/dashboard/settings/integrations) **> Integrations** add your Stripe account ID and restricted key.

#### Stripe Integration Permissions

| **Permission**                  | **Description**                                                                                                                                                                         |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Charges (Read and Write)        | Required for matching network alerts to charges and processing refunds.                                                                                                                 |
| Customers (Read)                | Essential for linking alerts to customers and accessing payment methods.                                                                                                                |
| Events (Read and Write)         | Ensures real-time updates on account changes without the need for frequent resource refetching.                                                                                         |
| PaymentIntents (Read and Write) | Used for correlating network alerts with charges and initiating refund processes.                                                                                                       |
| ⁠PaymentMethods (Read)          | Helps match network alerts by analysing the BIN on card objects.                                                                                                                        |
| Products (Read):                | Provides necessary information related to payment intents and subscriptions.                                                                                                            |
| Prices (Read)                   | Displays information pertinent to subscriptions.                                                                                                                                        |
| Subscriptions (Read and Write)  | Facilitates the linking of payment intents to invoices, supporting refund and cancellation workflows.                                                                                   |
| Credit notes (Read and Write)   | Enables viewing and managing invoice refunds when using Stripe Billing.                                                                                                                 |
| ⁠Invoices (Write)               | Allows for linking payment intents to their respective subscriptions and updating statement descriptors for Link payments.                                                              |
| Webhooks (Write)                | Allows for ChargebackStop to setup webhooks for the platform to automate certain tasks in real time.                                                                                    |
| Disputes (Read and Write)       | Allows the platform to sync the disputes to detect if the incoming alert is not disputed yet. Write permission allows us to submit representment for the disputes you decide to accept. |

### What is synced?

In order to match network alerts to your Stripe transactions and automate refunds, we need to sync the following data into our system:

→ Payment intents / charges

→ Customers

→ Refunds

→ Subscriptions

→ Payment methods (without card numbers)

→ Last 4 digits of card number

→ BIN

→ Disputes

→ Early fraud notification

### Matching Stripe Link payments

{% hint style="warning" %}
If you offer Link to your customers you may not be able to prevent as many chargebacks unless you implement some changes to the default Link settings with Stripe. Without these changes, you may be charged for unactionable alerts.

Please read this section carefully to fully understand the required changes
{% endhint %}

Stripe made the decision to not disclose card brand, card last 4, authorisation code, BIN, or ARN for transactions paid for with Link in their API. As a result, If you use Stripe Link as a payment method in your Stripe integration, we won't be able to automatically match alerts to Stripe transactions.

If we cannot match an alert due to multiple matching Link transactions existing, you might still be charged for the alert. This is not a valid creditable scenario recognised by Verifi nor Ethoca.

There are a few suggested options to try and prevent unmatched alerts and potentially being charged for them:

1. Use Link in wallet mode. [Read more details in Stripe documentation](https://docs.stripe.com/payments/link/link-payment-integrations?link-integrations=link-card-integrations)
2. If Stripe Checkout is not used, alternative approaches are available, although these may require some additional configuration. Please reach out to discuss.
3. Don’t offer Link

If these options don’t work for your specific case, then we can provide you with the opportunity to manually match these. Instead of us automatically resolving as “Not Refunded” we can add these to your “Action Required” queue. This gives you the opportunity to manually action this alert. Reach out to us, should you wish to activate this.


# Adyen

{% hint style="info" %}
You don't need an Adyen account in order to use ChargebackStop. We work with all processors and an Adyen integration is not required.
{% endhint %}

### Before you begin…

We are aware of the fact that this integration guide is quite long. We are happy to run this integration for you in your Adyen account.

{% hint style="success" %}
The ChargebackStop team can perform this integration process for you. See details below.
{% endhint %}

To have ChargebackStop team perform the integration process for you, please add our Client Services account (<kbd><clientservices@chargebackstop.com></kbd>) as a user to your Adyen account.

Make sure to include the following permissions.

**Accounts**

* Company account and all associated merchant accounts

**Roles**

Make sure the following roles are selected:

* General
  * Merchant report
  * Merchant view PII
  * Download reports
  * Generate reports
  * Merchant report
* Developer
  * Manage API credentials
  * Merchant technical integrator
* Transactions
* Merchant manage payments
* View payments

Once that's done please let us know, and we'll pick it up from there! That's all you need to do.

### Integrating Adyen Manually

In order to integrate ChargebackStop and Adyen, you'll need to perform a few actions in your Adyen account.

If you want to keep track of your progress as you work through all these steps, you can use the checklist at the bottom of this page.

If you encounter any issues at any point of the process, please contact our support at <support@chargebackstop.com>.

If you prefer to perform the integration steps manually, please follow this guide.

{% hint style="success" %}
ChargebackStop team can perform this integration for you. Please see the section above.
{% endhint %}

### Automatic reports generation

ChargebackStop integration with Adyen relies on two types of reports being automatically generated by Adyen every day:

* Payment accounting
* Received payment details

Before you begin, please open your Adyen dashboard.

1. Click "Reports" in left-hand menu.
2. Select "All" tab underneath the search input.
3. Type "Payment accounting" in the search input.
4. Click "Payment accounting" report title in the list.

Here's where you need to click in each of the steps above:

<figure><img src="/files/Df542UCc9cfMo63MmbAa" alt=""><figcaption></figcaption></figure>

Once you are inside of the "Payment accounting" report, please do the following:

1. Click "Manage report".
2. In the dropdown menu, click "Automatic (generate on a schedule)".

<figure><img src="/files/9EYcsRdSQHAcPiszOPT3" alt=""><figcaption></figcaption></figure>

In the pop-up window that opens, switch the "Automatic generation" toggle to "On".

<figure><img src="/files/IWe2F7GDLZ9OIfUsMjNN" alt=""><figcaption></figcaption></figure>

Once that's done, repeat the same steps for the "Received payment details" report.

### Additional fields in "Payment accounting" report

Before you begin, please open your Adyen dashboard.

1. Click "Reports" in left-hand menu.
2. Select "All" tab underneath the search input.
3. Type "Payment accounting" in the search input.
4. Click "Payment accounting" report title in the list.

Here's where you need to click in each of the steps above:

<figure><img src="/files/Df542UCc9cfMo63MmbAa" alt=""><figcaption></figcaption></figure>

Once the report page opens, click on the "Column configuration" tab underneath the main headline.

<figure><img src="/files/T3qO0vLzny88GD2hnRWY" alt=""><figcaption></figcaption></figure>

In the list, please make sure that the following columns are enabled:

* Company Account
* Merchant Account
* Psp Reference
* Record Type
* Main Amount
* Main Currency
* ARN
* Creation Date (AMS)

In most cases, only the last two columns will need to be enabled. You can add more columns to your reports if you need them for other purposes, ChargebackStop will ignore any data that is not required by us. Ordering of the column does not matter for ChargebackStop.

Once you made sure all the columns from the list above are enabled, please save the changes by clicking the "Save configuration" button at the bottom of the page.

<figure><img src="/files/6rNfkiLcJpipCYH6TS1I" alt=""><figcaption></figcaption></figure>

Make sure to familiarise yourself with the contents of the popup window displayed, and confirm it with "OK" button once you've made sure the change won't break any workflows in your organisation.

<figure><img src="/files/sY2W879DGC9JGXfo4D0u" alt=""><figcaption></figcaption></figure>

### Create web service user API credential

Next step is to create an API credential web service user that will be used by ChargebackStop to perform operations in your account.

Firstly, make sure you are in your Adyen company account context, not merchant account context.

<figure><img src="/files/52frPyJFKDoFO3QoOG2G" alt=""><figcaption></figcaption></figure>

Once you're confident you're within company account context, you can proceed to create web service user API credential by following these steps:

1. Click "Developers" in the left-hand menu.
2. In the list that opens, select "API credentials".
3. Click the blue button in the top right that says "Create new credential".

<figure><img src="/files/uKq5ATvmvh9q5KGf01Cz" alt=""><figcaption></figcaption></figure>

In the pop-up window that opens, select the following:

1. Make sure that "Web service user" credential type is selected.
2. You can optionally add a description to make it easier to identify the API credential later on, e.g. "ChargebackStop integration".
3. Click the blue "Create credential" button in the bottom right corner of the window.

<figure><img src="/files/sIwzlnIj9qLXMT0bBjNu" alt=""><figcaption></figcaption></figure>

In the next screen, you will see your new key details. Firstly, please copy the API key and store it in a secure spot. You will use it later on when creating your ChargebackStop integration.

{% hint style="warning" %}
You won't be able to see this key again. Make sure you store it somewhere safe temporarily as you'll need it in the next steps!
{% endhint %}

<figure><img src="/files/31qeMDAhCmNO3ScQ6OkH" alt=""><figcaption></figcaption></figure>

Next, please scroll down to the "Permissions" section below. In the "Roles" block, please make sure that the following checkboxes are selected:

* Account
  * Management API - Accounts read and write
  * Management API - API credentials read and write
  * Management API - Webhooks read and write
* Uncategorized
  * Checkout webservice role

Once that's verified, you can finalise API credential creation using the blue "Save changes" button in the bottom right corner of the page.

<figure><img src="/files/tQSIxSbBxy7IXnIIWaA3" alt=""><figcaption></figcaption></figure>

### Create reports service user API credential

The process for creating reports service user is similar.

Firstly, make sure you are in your Adyen company account context, not merchant account context.

<figure><img src="/files/52frPyJFKDoFO3QoOG2G" alt=""><figcaption></figcaption></figure>

Once you're confident you're within company account context, you can proceed to create web service user API credential by following these steps:

1. Click "Developers" in the left-hand menu
2. In the list that opens, select "API credentials"
3. Click the blue button in the top right that says "Create new credential"

<figure><img src="/files/uKq5ATvmvh9q5KGf01Cz" alt=""><figcaption></figcaption></figure>

In the pop-up window that opens, select the following:

1. Make sure that "Report service user" credential type is selected.
   1. This is the first step that's different from the web service user API credential creation.
2. You can optionally add a description to make it easier to identify the API credential later on, e.g. "ChargebackStop integration".
3. Click the blue "Create credential" button in the bottom right corner of the window.

<figure><img src="/files/wDxH9SwdmOrKWaCbybw6" alt=""><figcaption></figcaption></figure>

In the next screen, you will see your new key details.

1. Please copy the API key and store it in a secure spot. You will use it later on when creating your ChargebackStop integration.
2. Finalise API credential creation using the blue "Save changes" button in the bottom right corner of the page.

{% hint style="info" %}
You won't be able to see this key again. Make sure you store it somewhere safe temporarily as you'll need it in the next steps!
{% endhint %}

<figure><img src="/files/OqxNBfgndE8Sp442Zhl1" alt=""><figcaption></figcaption></figure>

You are now ready to create the integration in ChargebackStop dashboard!

### Connecting ChargebackStop to your Adyen account

The first step is to open [integration settings page in ChargebackStop](https://dashboard.chargebackstop.com/dashboard/settings/integrations).

Next, click on the black "Add provider" button. In the dropdown list, select "Adyen".

<figure><img src="/files/60UMQHqvX5Gvbhc7Etkc" alt=""><figcaption></figcaption></figure>

If this is the first integration in your ChargebackStop organisation, you'll need to click "Add your first integration" button in the middle of the page instead.

<figure><img src="/files/lDNeEJWVdqEAVXBW4Y50" alt=""><figcaption></figcaption></figure>

Once the pop-up window opens, enter the required data into respective fields.

<figure><img src="/files/tsoT0SQTpqy0hABfqzOC" alt=""><figcaption></figcaption></figure>

Company and merchant account names can be found in dropdown menu in the top left corner of Adyen dashboard. Web service user API key and report service user API key are the keys we generated in previous part of this tutorial.

<figure><img src="/files/ojWCfCMkDEgUzc9Z5P7a" alt=""><figcaption></figcaption></figure>

In order to save and enable the integration, press the black "Add integration" button at the bottom of the pop-up window.

ChargebackStop platform will now do the following:

1. Verify your web service user API key.
2. Verify your reports service user API key.
3. Set up required webhooks in your Adyen account.

If all of the above succeed, you'll see a "Successfully connected to Adyen" message above the integrations list and a green "Enabled" badge next to the integration.

<figure><img src="/files/kHcGV1cOxg2SVWEE7Jvw" alt=""><figcaption></figcaption></figure>

### Final checklist

In order to keep track of your ChargebackStop - Adyen integration progress, you can use this handy checklist.

* [ ] [My "Payment accounting" and "Received payment details" reports are being generated automatically on a daily basis](#automatic-reports-generation)
* [ ] ["Payment accounting" report enabled columns include "ARN" and "Creation Date (AMS)"](#additional-fields-in-payment-accounting-report)
* [ ] [I created web service user API credential for ChargebackStop with correct permissions](#create-web-service-user-api-credential)
* [ ] [I created report service user API credential for ChargebackStop](#create-reports-service-user-api-credential)
* [ ] [I successfully connected ChargebackStop platform to my Adyen account](#connecting-chargebackstop-to-your-adyen-account)


# Authorize.net

{% hint style="info" %}
You don't need Authorize.net account in order to use ChargebackStop. We work with all processors and an Authorize.net integration is not required.
{% endhint %}

### Integrating Authorize.net

ChargebackStop ↔ Authorize.net integration is straightforward.

1. Log in to your [Authorize.net](https://www.authorize.net/) account.
2. From the top navigation bar, select "Account".
3. In "Security Settings" section, select "API Credentials & Keys"

<figure><img src="/files/5GW4wtG5476JxrbOmZvl" alt=""><figcaption></figcaption></figure>

4. In the page that opens, take note of the "API Login ID" value.

<figure><img src="/files/QDenfEoxRIBhz9788Z8o" alt=""><figcaption></figcaption></figure>

{% hint style="danger" %}
Authorize.net only allows one API key to exist at the time.

If you already use Authorize.net API, creating a new key will invalidate it and cause your other integrations to fail.

Proceed with caution!
{% endhint %}

5. If you already use Authorize.net API in other integrations, you'll need to find your Transaction Key in order to integrate with ChargebackStop.\
   If you are confident that you don't use Authorize.net API anywhere you can select "New Transaction Key" at the bottom page, and click "Submit".
6. Open [ChargebackStop Integrations settings](https://dashboard.chargebackstop.com/dashboard/settings/integrations), and select "Authorize.net" from the "Add provider" dropdown.

<figure><img src="/files/U1TQn5AQKDExhPo6GUXL" alt=""><figcaption></figcaption></figure>

7. In the pop up window that opens, enter:
   1. Integration name - this is for your reference purposes only
   2. Login ID - API Login ID from step 4.
   3. Transaction Key - API key from step 5.
   4. Confirm with "Add integration"

<figure><img src="/files/O09Bi9obbCh1ARLoOLAg" alt=""><figcaption></figcaption></figure>

8. ChargebackStop will validate your API details, and activate the integration if they are correct.


# NMI

{% hint style="info" %}
You don't need NMI account in order to use ChargebackStop. We work with all processors and an NMI integration is not required.
{% endhint %}

### Integrating NMI

ChargebackStop ↔ NMI integration is straightforward.

1. Log in to your NMI account
2. Click "My Settings" in the top right corner
3. Scroll down to "Private Security Keys"
4. Create a new key with API permissions
5. Open [ChargebackStop Integrations settings](https://dashboard.chargebackstop.com/dashboard/settings/integrations), and select "NMI" from the "Add provider" dropdown.\
   ![](/files/I3xm3mDn5tDTgYofKkIY)
6. Give your new integration a display name (used only internally by ChargebackStop) and paste the security key you just created.
7. Save the new integration with "Add integration".


