For PlatformsAlerts

Enroll merchants in alerts

Enroll a merchant's billing descriptor in Ethoca, CDRN, RDR and TC40 alert programs with API v3, track each program until alerts flow, enroll in bulk, and disenroll. Early access for invited platforms.

You are a platform that wants your merchants covered by pre-chargeback alerts without sending them to a dashboard. This guide takes one merchant from no enrollment to alerts flowing, using the alert enrollment endpoints in API v3.

API v3 is in early access

API v3 is in early access. Alert enrollments are the first API v3 capability open for use, and only to invited platforms. API v3 is not generally available: every other part of your integration stays on the 2025-04-01 API. To get access for your platform, contact your Chargeflow account manager through Support.

What you'll build

An alert enrollment covers one billing descriptor on one processor for one merchant. Each enrollment carries one or more alert programs, and every program confirms on its own schedule, so you track them separately.

ProgramNetworkNeeds
ethocaEthoca alerts (Mastercard)psp and descriptor
cdrnVerifi CDRNpsp and descriptor
rdrVerifi Rapid Dispute Resolutionpsp, descriptor, card_acceptor_id, bin, at least one arns
tc40Visa TC40 fraud reportspsp, descriptor, card_acceptor_id, bin, at least one arns

RDR refunds automatically

RDR refunds automatically. It resolves a matching dispute inside the card network by refunding the cardholder before it becomes a chargeback. Neither you nor Chargeflow can decline that refund per alert. Leave rdr out unless the merchant accepts automatic refunds.

How alerts deflect chargebacks is covered in Alerts model.

Prerequisites

  • A platform API key with v3 access, shaped cf_live_<key_id>_<secret>. v3 sends it as Authorization: Bearer, not in the x-api-key header the 2025-04-01 API uses. A merchant key is refused with 403 WRONG_KEY_TYPE.
  • The merchant's chargeflow_id, the 24-character account ID of a merchant in your portfolio.
  • The merchant onboarded with Alerts. v3 has no endpoint for this step. Step 1 tells you whether it is done.
  • The billing descriptor exactly as it appears on the cardholder's statement, and the processor it bills through.

The snippets read the key from CHARGEFLOW_PLATFORM_API_KEY:

Terminal
export CHARGEFLOW_PLATFORM_API_KEY="YOUR_PLATFORM_API_KEY"

Check the merchant's Alerts account

Confirm the key works and the merchant is ready before you enroll anything.

Terminal
curl https://api.chargeflow.io/v3/merchants/00000000d0c5000000000001/alert_account \
  -H "Authorization: Bearer $CHARGEFLOW_PLATFORM_API_KEY"

You should see a 200 with the merchant's Alerts settings and a count of its programs by status:

Response
{
  "object": "alert_account",
  "chargeflow_id": "00000000d0c5000000000001",
  "created_at": "2026-08-12T09:30:00Z",
  "settings": {
    "alerts_only": false,
    "manual_billing": false,
    "transaction_linking_disabled": false,
    "payment_method_authorized": true
  },
  "payment_method_authorized_at": "2026-08-12T09:31:00Z",
  "last_disenrollment_requested_at": null,
  "enrollment_count": 0,
  "provider_status_counts": {
    "in_progress": 0,
    "enrolled": 0,
    "active": 0,
    "in_process_of_disenrollment": 0,
    "disenrolled": 0,
    "dropped": 0
  },
  "livemode": true
}

If this fails

404 ALERT_ACCOUNT_NOT_FOUND means the merchant was never onboarded with Alerts: ask your Chargeflow account manager to onboard it, then retry. 403 MERCHANT_NOT_IN_PORTFOLIO means the chargeflow_id is not one of your merchants. 401 UNAUTHORIZED means the key or the Bearer prefix is wrong.

Create the enrollment

Send the processor, the descriptor and the programs. providers defaults to ["ethoca"] when you leave it out.

Terminal
curl -X POST https://api.chargeflow.io/v3/alert_enrollments \
  -H "Authorization: Bearer $CHARGEFLOW_PLATFORM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "chargeflow_id": "00000000d0c5000000000001",
    "psp": "stripe",
    "descriptor": "EXAMPLE*STORE",
    "providers": ["ethoca", "cdrn"]
  }'

You should see a 202: the request was accepted and each program starts in_progress. Store the id, the next steps use it.

Response
{
  "id": "aen_00000000d0c5000000000001-00000000-d0c5-4000-8000-000000000003",
  "object": "alert_enrollment",
  "created_at": "2026-08-12T09:30:00Z",
  "updated_at": "2026-08-12T09:30:00Z",
  "livemode": true,
  "psp": "stripe",
  "chargeflow_id": "00000000d0c5000000000001",
  "descriptor": "EXAMPLE*STORE",
  "providers": [
    {
      "provider": "ethoca",
      "status": "in_progress",
      "status_reason": "sent_to_enrollment",
      "retryable": null,
      "enrolled_at": null,
      "alerts_stopped_at": null,
      "updated_at": "2026-08-12T09:30:00Z"
    },
    {
      "provider": "cdrn",
      "status": "in_progress",
      "status_reason": "sent_to_enrollment",
      "retryable": null,
      "enrolled_at": null,
      "alerts_stopped_at": null,
      "updated_at": "2026-08-12T09:30:00Z"
    }
  ],
  "card_acceptor_id": null,
  "bin": null,
  "arns": []
}

Creating is safe to repeat. The same psp and descriptor (compared ignoring case and extra spaces) always resolve to the same enrollment: a repeat turns on any program that is off, never stops one that is on, and answers 200 when nothing changed. Retry a 502 or 504 without fear of a duplicate.

If this fails

A 422 means the request was well formed but refused, and nothing was created. The usual codes: MISSING_FIELD (rdr or tc40 without card_acceptor_id, bin and arns, so send all three together), UNKNOWN_PSP (the processor is not supported), and DESCRIPTOR_UNAVAILABLE (this descriptor cannot be enrolled on this processor). The full list is on Create an alert enrollment.

Subscribe to status changes

Programs confirm asynchronously. Register a webhook for alerts.enrollment.status_changed so you hear about each change without polling.

Terminal
curl -X POST https://api.chargeflow.io/v3/webhooks/registrations \
  -H "Authorization: Bearer $CHARGEFLOW_PLATFORM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url_target": "https://example.com/hooks/chargeflow",
    "event_types": ["alerts.enrollment.status_changed"]
  }'

You should see a 201. signing_secret appears in this response and never again, so store it now: you need it to verify the X-Chargeflow-Signature header on every delivery.

Response
{
  "id": "whr_d0c5000001",
  "object": "webhook_registration",
  "url_target": "https://example.com/hooks/chargeflow",
  "event_types": ["alerts.enrollment.status_changed"],
  "is_active": true,
  "created_at": "2026-08-12T09:32:00.000Z",
  "updated_at": "2026-08-12T09:32:00.000Z",
  "livemode": true,
  "signing_secret": "YOUR_SIGNING_SECRET"
}

If this fails

409 WEBHOOK_ALREADY_EXISTS means this URL is already registered: one registration per URL. Add the event type to the existing one with Update a registration.

Signature verification and the retry schedule are on Create a registration.

Wait for active

Each time a program changes status, your endpoint receives one event for that program:

Webhook payload
{
  "webhookId": "whd_d0c5000001",
  "eventId": "evt_d0c5000001",
  "creationDate": "2026-08-13T08:00:03.000Z",
  "type": "alerts.enrollment.status_changed",
  "livemode": true,
  "data": {
    "enrollment_id": "aen_00000000d0c5000000000001-00000000-d0c5-4000-8000-000000000003",
    "chargeflow_id": "00000000d0c5000000000001",
    "descriptor": "EXAMPLE*STORE",
    "psp": "stripe",
    "provider": "ethoca",
    "status": "enrolled",
    "status_reason": null,
    "occurred_at": "2026-08-13T08:00:02.381Z"
  }
}

Return a 200 within 10 seconds. Events arrive at least once and not always in order, so deduplicate on eventId and ignore an event whose occurred_at is older than what you already hold for that enrollment_id and provider.

enrolled means the network confirmed, not that alerts are arriving. The merchant is covered by a program only when that program reaches active.

Verify it worked

Retrieve the enrollment and read each program's status. This is also the fallback when a webhook is missed.

Terminal
curl https://api.chargeflow.io/v3/alert_enrollments/aen_00000000d0c5000000000001-00000000-d0c5-4000-8000-000000000003 \
  -H "Authorization: Bearer $CHARGEFLOW_PLATFORM_API_KEY"

You should see the same object the create returned, with each entry in providers at its current status. To see every enrollment for a merchant, use List alert enrollments with chargeflow_id.

Read the status

Every program on an enrollment has its own status:

StatusMeaningWhat to do
in_progressSubmitted and not yet confirmed. Also a refused program: see the next section.Wait, or act on status_reason
enrolledThe network confirmed. No alert has arrived yet.Wait
activeAlerts are flowing. The only value that means this.Nothing, the merchant is covered
in_process_of_disenrollmentRemoval is in progress. Still billable, and alerts can still arrive.Wait
disenrolledRemoved.Create again to re-enroll
droppedNever went live and was closed after 45 days.Read status_reason for the cause

When a program is refused

A refused program does not get its own status: it stays in_progress with an error in status_reason and a retryable flag. Branch on retryable.

retryablestatus_reasonWhat to do
falsegeneric_descriptor, invalid_arn, invalid_caid_bin, vendor_submission_rejectedIt can't succeed as it is. Enroll a corrected descriptor or identifiers.
trueadditional_descriptors_required, requires_migration, payment_method_issueResolve the cause named. The enrollment can then succeed unchanged.
truevendor_submission_failed, technical_failureNothing. Chargeflow follows up.
nullsent_to_enrollment and other non-error reasonsInformational. Keep waiting.

Every reason and its meaning is listed on Retrieve an alert enrollment.

Enroll many descriptors at once

A merchant with several descriptors can be enrolled in one call: up to 25 items, all for the same merchant. Each item takes the same fields as a single create, without chargeflow_id.

Terminal
curl -X POST https://api.chargeflow.io/v3/alert_enrollments/bulk \
  -H "Authorization: Bearer $CHARGEFLOW_PLATFORM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "chargeflow_id": "00000000d0c5000000000001",
    "enrollments": [
      { "psp": "stripe", "descriptor": "EXAMPLE*STORE" },
      { "psp": "adyen", "descriptor": "EXAMPLE*STORE EU", "providers": ["ethoca", "rdr"] }
    ]
  }'

You should see a 207 with one result per item, in request order. A 207 does not mean every item succeeded: check each item's status. A 200 or 202 carries alert_enrollment, anything else carries error.

Response
{
  "results": [
    {
      "index": 0,
      "status": 200,
      "alert_enrollment": {
        "id": "aen_00000000d0c5000000000001-00000000-d0c5-4000-8000-000000000003",
        "object": "alert_enrollment",
        "created_at": "2026-08-12T09:30:00Z",
        "updated_at": "2026-08-12T09:30:00Z",
        "livemode": true,
        "psp": "stripe",
        "chargeflow_id": "00000000d0c5000000000001",
        "descriptor": "EXAMPLE*STORE",
        "providers": [
          {
            "provider": "ethoca",
            "status": "in_progress",
            "status_reason": "sent_to_enrollment",
            "retryable": null,
            "enrolled_at": null,
            "alerts_stopped_at": null,
            "updated_at": "2026-08-12T09:30:00Z"
          }
        ],
        "card_acceptor_id": null,
        "bin": null,
        "arns": []
      }
    },
    {
      "index": 1,
      "status": 422,
      "error": {
        "type": "validation_error",
        "code": "MISSING_FIELD",
        "message": "card_acceptor_id is required for rdr.",
        "field": "card_acceptor_id",
        "request_id": "00000000-d0c5-4000-8000-000000000004"
      }
    }
  ]
}

Resend only the items that failed with 500, 502 or 504: a retry is safe. A 422 item needs a fix first.

Stop an enrollment

Disenroll some programs by naming them in providers, or all of them by sending no body.

Terminal
curl -X POST https://api.chargeflow.io/v3/alert_enrollments/aen_00000000d0c5000000000001-00000000-d0c5-4000-8000-000000000003/disenrollments \
  -H "Authorization: Bearer $CHARGEFLOW_PLATFORM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "providers": ["cdrn"], "reason": "Merchant request" }'

You should see a 202 with the enrollment, and the named program at in_process_of_disenrollment. It moves to disenrolled once the network confirms, which can take days. Until then the program is still billable and alerts can keep arriving, so keep handling them. The enrollment stays readable at the same id, and creating it again re-enrolls it.

Troubleshooting

SymptomCauseFix
401 UNAUTHORIZEDThe key is missing, malformed, revoked, or sent in x-api-keySend Authorization: Bearer with a cf_live_ key
403 WRONG_KEY_TYPEA merchant key called an Alerts endpointUse your platform key
403 MERCHANT_NOT_IN_PORTFOLIOThe chargeflow_id is not one of your merchantsCheck the ID. The answer is the same when the merchant does not exist
404 ALERT_ACCOUNT_NOT_FOUNDThe merchant was never onboarded with AlertsAsk your account manager to onboard it
422 ENROLLMENT_IDENTIFIERS_DIFFERThe enrollment exists with a different card_acceptor_id, bin or arnsLeave the field out to keep the stored value. resource_id is the existing enrollment
422 PROVIDER_DISENROLLINGThe program is still in_process_of_disenrollmentCreate again once it is disenrolled
422 PROVIDER_NOT_ENROLLEDA disenroll named a program the enrollment never hadName only programs listed in the enrollment's providers
429 RATE_LIMITEDToo many requestsWait the seconds in Retry-After
A program stays in_progressStill waiting on the network, or refusedRead status_reason and retryable: see When a program is refused
A program is enrolled, no alertsConfirmed, not yet flowingWait for active

Error bodies, and which statuses are safe to retry, are on v3 errors.

Next step

Was this page helpful?