# Automate a chargeback dispute (https://docs.chargeflow.io/docs/merchants/automation/automate-a-chargeback-dispute)



This recipe is a full, copy-paste-runnable flow for responding to a chargeback with Chargeflow, whatever processor it came from. You receive the `dispute.created` webhook, fetch the dispute, attach supporting evidence and notes, and Chargeflow builds and submits the representment on your behalf, ahead of the deadline.

The flow is identical for every processor. Each dispute carries a `source` field (`stripe`, `paypal`, `shopify_payments`, `braintree`, and so on) telling you where it originated, but you call the same Chargeflow endpoints regardless. See [Integrations](https://docs.chargeflow.io/docs/reference/integrations) for how each processor connection ingests disputes.

<Callout title="Chargeflow submits, you don't">
  You never call a submit endpoint or talk to the processor's dispute API. Once a dispute is in
  `needs_response` and your evidence is attached, Chargeflow assembles the evidence package and
  files the representment for you. This recipe is about feeding Chargeflow the context, not about
  submitting.
</Callout>

## Before you start [#before-you-start]

* Connect at least one payment processor to Chargeflow. After that, new disputes from that processor are ingested automatically. See [Integrations](https://docs.chargeflow.io/docs/reference/integrations).
* Get your `API Access Key`. Every request sends it in the `x-api-key` header. See [Authentication](https://docs.chargeflow.io/docs/reference/api-fundamentals/authentication).
* Register a webhook endpoint for `dispute.created`. See [Subscribe to Webhook Events](https://docs.chargeflow.io/docs/merchants/automation/subscribe-to-events).

All requests go to `https://api.chargeflow.io` under the `/public/2025-04-01/` path.

## Steps [#steps]

<Steps>
  <Step>
    ### Register for the dispute.created event [#register-for-the-disputecreated-event]

    Register a webhook endpoint so Chargeflow notifies you when a new dispute is ingested, from any connected processor.

    <Tabs items="['curl', 'Node.js', 'Python']">
      <Tab value="curl">
        ```bash title="Terminal"
        curl -X POST https://api.chargeflow.io/public/2025-04-01/webhooks \
          -H "x-api-key: YOUR_API_KEY" \
          -H "Content-Type: application/json" \
          -d '{
            "url": "https://your-server.com/webhook",
            "event": "dispute.created"
          }'
        ```
      </Tab>

      <Tab value="Node.js">
        ```javascript title="Node.js"
        const res = await fetch('https://api.chargeflow.io/public/2025-04-01/webhooks', {
          method: 'POST',
          headers: {
            'x-api-key': process.env.CHARGEFLOW_API_KEY,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            url: 'https://your-server.com/webhook',
            event: 'dispute.created',
          }),
        });

        console.log(await res.json());
        ```
      </Tab>

      <Tab value="Python">
        ```python title="Python"
        import os, requests

        res = requests.post(
            "https://api.chargeflow.io/public/2025-04-01/webhooks",
            headers={"x-api-key": os.environ["CHARGEFLOW_API_KEY"]},
            json={
                "url": "https://your-server.com/webhook",
                "event": "dispute.created",
            },
        )
        print(res.json())
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step>
    ### Receive the dispute.created webhook [#receive-the-disputecreated-webhook]

    When a cardholder disputes a payment, Chargeflow ingests the dispute, runs its first enrichment pass, and posts a `dispute.created` event to your endpoint.

    <Callout title="Note">
      The `dispute.created` event arrives wrapped in the delivery envelope: the dispute fields are
      inside `data`, and `data.id` is the Chargeflow dispute ID you use in the rest of this recipe.
    </Callout>

    Example payload (the `source` here is `stripe`, but it could be any connected processor):

    ```json title="Webhook payload"
    {
      "type": "dispute.created",
      "webhookId": "123e4567-e89b-12d3-a456-426614174000",
      "creationDate": "2025-01-27T10:00:00Z",
      "data": {
        "id": "dis_12345",
        "source_id": "du_123456789",
        "account_id": "act_112233",
        "created_at": "2025-01-27T09:00:00Z",
        "reason": "fraud",
        "due_by": "2025-02-10T10:00:00Z",
        "source": "stripe",
        "amount": 150,
        "currency": "USD",
        "status": "needs_response",
        "stage": "Chargeback",
        "closed_at": null,
        "transaction": "tx_1234567890",
        "subscription": null,
        "order": "ord_1234567890",
        "customerCommunication": null
      }
    }
    ```

    Your endpoint must return a `2XX` status promptly, then process the event asynchronously. Acknowledge first, work after.

    ```javascript title="Node.js"
    const express = require('express');
    const app = express();

    app.post('/webhook', express.json(), (req, res) => {
      const event = req.body;

      // Acknowledge receipt immediately so Chargeflow does not retry
      res.json({ received: true });

      if (event.type === 'dispute.created') {
        // Same flow for every processor - branch on event.data.source only if you
        // need processor-specific handling on your side.
        handleDispute(event.data.id);
      }
    });

    app.listen(8000, () => console.log('Webhook handler running on port 8000'));
    ```

    <Callout type="idea" title="Tip">
      Verify the `X-Chargeflow-Hmac-Sha256` signature on every webhook in production. See
      [Webhooks](https://docs.chargeflow.io/docs/reference/concepts/webhooks#verifying-a-webhook-signature) for the verification
      code.
    </Callout>
  </Step>

  <Step>
    ### Fetch the dispute [#fetch-the-dispute]

    Use the Chargeflow dispute ID from `data.id` to read the full, current dispute record. Check the `status`, the `due_by` deadline, and which linked objects (`transaction`, `order`, `subscription`) Chargeflow has already matched.

    <Tabs items="['curl', 'Node.js', 'Python']">
      <Tab value="curl">
        ```bash title="Terminal"
        curl -X GET https://api.chargeflow.io/public/2025-04-01/disputes/dis_12345 \
          -H "x-api-key: YOUR_API_KEY"
        ```
      </Tab>

      <Tab value="Node.js">
        ```javascript title="Node.js"
        async function handleDispute(disputeId) {
          const res = await fetch(`https://api.chargeflow.io/public/2025-04-01/disputes/${disputeId}`, {
            headers: { 'x-api-key': process.env.CHARGEFLOW_API_KEY },
          });
          const dispute = await res.json();
          console.log(dispute.status, dispute.reason, dispute.due_by);
          return dispute;
        }
        ```
      </Tab>

      <Tab value="Python">
        ```python title="Python"
        import os, requests

        res = requests.get(
            "https://api.chargeflow.io/public/2025-04-01/disputes/dis_12345",
            headers={"x-api-key": os.environ["CHARGEFLOW_API_KEY"]},
        )
        dispute = res.json()
        ```
      </Tab>
    </Tabs>

    Example response:

    ```json title="Response"
    {
      "id": "dis_12345",
      "source": "stripe",
      "source_id": "du_123456789",
      "account_id": "act_112233",
      "created_at": "2025-01-27T09:00:00Z",
      "reason": "fraud",
      "due_by": "2025-02-10T10:00:00Z",
      "amount": 150,
      "currency": "USD",
      "status": "needs_response",
      "stage": "Chargeback",
      "closed_at": null,
      "transaction": "tx_1234567890",
      "subscription": null,
      "order": "ord_1234567890"
    }
    ```

    Respond only while `status` is `needs_response`. Once the response is filed with the issuer, the status moves to `under_review`.
  </Step>

  <Step>
    ### Add evidence [#add-evidence]

    Attach the files that refute the claim. This is a `multipart/form-data` request. The dispute ID is the path parameter, and each call uploads one file with an `evidenceUploadCategory`.

    For a fraud chargeback, proof of delivery and customer communication are the strongest artifacts.

    <Tabs items="['curl', 'Node.js', 'Python']">
      <Tab value="curl">
        ```bash title="Terminal"
        curl -X POST https://api.chargeflow.io/public/2025-04-01/disputes/dis_12345/evidence \
          -H "x-api-key: YOUR_API_KEY" \
          -F "file=@/path/to/delivery-confirmation.pdf" \
          -F "evidenceUploadCategory=tracking_information"
        ```
      </Tab>

      <Tab value="Node.js">
        ```javascript title="Node.js"
        const fs = require('fs');
        const { blob } = require('stream/consumers');

        async function addEvidence(disputeId) {
          const form = new FormData();
          form.append('evidenceUploadCategory', 'tracking_information');
          form.append(
            'file',
            await blob(fs.createReadStream('./delivery-confirmation.pdf')),
            'delivery-confirmation.pdf',
          );

          const res = await fetch(
            `https://api.chargeflow.io/public/2025-04-01/disputes/${disputeId}/evidence`,
            {
              method: 'POST',
              headers: { 'x-api-key': process.env.CHARGEFLOW_API_KEY },
              body: form,
            },
          );

          return res.json(); // { requestId, evidenceId }
        }
        ```
      </Tab>

      <Tab value="Python">
        ```python title="Python"
        import os, requests

        with open("/path/to/delivery-confirmation.pdf", "rb") as f:
            res = requests.post(
                "https://api.chargeflow.io/public/2025-04-01/disputes/dis_12345/evidence",
                headers={"x-api-key": os.environ["CHARGEFLOW_API_KEY"]},
                files={"file": f},
                data={"evidenceUploadCategory": "tracking_information"},
            )
        data = res.json()  # { requestId, evidenceId }
        ```
      </Tab>
    </Tabs>

    A successful upload returns the evidence ID:

    ```json title="Response"
    {
      "requestId": "req_abc123def456",
      "evidenceId": "evd_789xyz"
    }
    ```

    Supported categories are `tracking_information`, `customer_communication`, `invoice`, `additional_evidence`, and `additional_notes`. Files can be PNG, JPG, or PDF, up to 5 MB. See [Upload Evidence](https://docs.chargeflow.io/docs/merchants/automation/upload-evidence) for the full reference.
  </Step>

  <Step>
    ### Add notes [#add-notes]

    Notes are uploaded through the same endpoint using the `additional_notes` category. This category requires a `content` field with your free-form text, alongside the file.

    <Tabs items="['curl', 'Node.js', 'Python']">
      <Tab value="curl">
        ```bash title="Terminal"
        curl -X POST https://api.chargeflow.io/public/2025-04-01/disputes/dis_12345/evidence \
          -H "x-api-key: YOUR_API_KEY" \
          -F "file=@/path/to/account-history.pdf" \
          -F "evidenceUploadCategory=additional_notes" \
          -F "content=Customer has 4 prior undisputed orders to the same address. IP and billing ZIP match the order on file."
        ```
      </Tab>

      <Tab value="Node.js">
        ```javascript title="Node.js"
        async function addNotes(disputeId) {
          const form = new FormData();
          form.append('evidenceUploadCategory', 'additional_notes');
          form.append(
            'content',
            'Customer has 4 prior undisputed orders to the same address. IP and billing ZIP match the order on file.',
          );
          form.append(
            'file',
            await blob(fs.createReadStream('./account-history.pdf')),
            'account-history.pdf',
          );

          const res = await fetch(
            `https://api.chargeflow.io/public/2025-04-01/disputes/${disputeId}/evidence`,
            {
              method: 'POST',
              headers: { 'x-api-key': process.env.CHARGEFLOW_API_KEY },
              body: form,
            },
          );

          return res.json();
        }
        ```
      </Tab>

      <Tab value="Python">
        ```python title="Python"
        import os, requests

        with open("/path/to/account-history.pdf", "rb") as f:
            res = requests.post(
                "https://api.chargeflow.io/public/2025-04-01/disputes/dis_12345/evidence",
                headers={"x-api-key": os.environ["CHARGEFLOW_API_KEY"]},
                files={"file": f},
                data={
                    "evidenceUploadCategory": "additional_notes",
                    "content": "Customer has 4 prior undisputed orders to the same address. IP and billing ZIP match the order on file.",
                },
            )
        data = res.json()
        ```
      </Tab>
    </Tabs>

    <Callout type="idea" title="Tip">
      Add evidence as early as possible after `dispute.created`. Chargeflow folds your uploads into the
      evidence package before submission, so the sooner they arrive, the more the AI can build around
      them.
    </Callout>
  </Step>

  <Step>
    ### Let Chargeflow submit the response [#let-chargeflow-submit-the-response]

    You do not call a separate submit endpoint, and you never touch the processor's own dispute API. Once a dispute is in `needs_response` and your evidence is attached, Chargeflow assembles the evidence package and submits the representment to the issuer ahead of the `due_by` deadline.

    Track progress by reading the dispute again. The `status` moves from `needs_response` to `under_review` after submission, and to `won` or `lost` once the issuer decides.

    <Tabs items="['curl', 'Node.js', 'Python']">
      <Tab value="curl">
        ```bash title="Terminal"
        curl -X GET https://api.chargeflow.io/public/2025-04-01/disputes/dis_12345 \
          -H "x-api-key: YOUR_API_KEY"
        ```
      </Tab>

      <Tab value="Node.js">
        ```javascript title="Node.js"
        const res = await fetch('https://api.chargeflow.io/public/2025-04-01/disputes/dis_12345', {
          method: 'GET',
          headers: {
            'x-api-key': process.env.CHARGEFLOW_API_KEY,
          },
        });
        const data = await res.json();
        ```
      </Tab>

      <Tab value="Python">
        ```python title="Python"
        import os, requests

        res = requests.get(
            "https://api.chargeflow.io/public/2025-04-01/disputes/dis_12345",
            headers={"x-api-key": os.environ["CHARGEFLOW_API_KEY"]},
        )
        data = res.json()
        ```
      </Tab>
    </Tabs>
  </Step>
</Steps>

## Next steps [#next-steps]

<Cards>
  <Card title="Upload Evidence" href="/docs/merchants/automation/upload-evidence">
    Every evidence category and file rule.
  </Card>

  <Card title="Backfill the last 90 days" href="/docs/merchants/automation/backfill-90-days">
    Pull in disputes that predate your webhook setup.
  </Card>

  <Card title="dispute.created" href="/docs/merchants/webhook-events#dispute-created">
    The full event payload reference.
  </Card>

  <Card title="Chargeback lifecycle" href="/docs/reference/concepts/chargeback-lifecycle">
    What each dispute status means.
  </Card>
</Cards>
