Direct Debit · Guides
Penny validation
Confirm a CLABE or debit card belongs to your customer with a MXN $0.01 deposit before you debit it.
Penny validation is an ownership check. It confirms that a Mexican bank account (CLABE) or debit card belongs to the customer you say it belongs to, before you start pulling real money from it.
- Monato sends a MXN $0.01 deposit to the account.
- Banxico returns a CEP (Comprobante Electrónico de Pago) for that deposit. The CEP includes the name and tax ID of the account holder.
- Monato compares the CEP’s beneficiary data with the customer record you provided.
- If they match, the account is trusted and direct debits can proceed. If they don’t, the account is rejected.
Penny validation is turned on per organization. Once enabled, it applies automatically to every instrument and charge that qualifies. You don’t toggle it per request.
When it runs
With penny validation enabled for your organization, it runs:
- when you create an instrument (CLABE or debit card) with a
customer_id; - when you create a charge that includes both
inline_instrumentandinline_customer, including each row of a batch file.
It does not run for a charge on a stored instrument_id, which must already be active, or for an inline_instrument sent with a customer_id instead of inline_customer. If the same account was validated before, the earlier result is reused.
Why it matters
Penny validation protects you from:
- Customers entering the wrong CLABE by mistake.
- Fraudulent attempts to debit an account the customer doesn’t own.
- Failed charges, and their fees, on accounts that were never going to work.
Because the check runs before the real charge, a rejected account is never debited.
Before you start: create a customer
Penny validation compares the CEP with a customer record, so create the customer first with POST /customers.
{
"name": "Jane Doe",
"document_type": "mx_rfc",
"document_number": "XXXX000000XXX",
"email": "jane@example.com",
"phone_number": "+520000000000"
}| Field | Required | Description |
|---|---|---|
name |
Yes | Customer’s full legal name |
document_type |
Yes | Document type, for example mx_rfc or passport |
document_number |
Yes | Document number |
email |
No | Customer email |
phone_number |
No | Customer phone number |
{
"id": "33333333-3333-4333-8333-333333333333",
"org_id": "00000000-0000-4000-8000-000000000000",
"name": "Jane Doe",
"document_type": "mx_rfc",
"document_number": "XXXX000000XXX",
"email": "jane@example.com",
"phone_number": "+520000000000",
"customer_metadata": null,
"created_at": "2026-01-15T12:00:00Z",
"updated_at": null
}Keep the id. You use it as customer_id on an instrument. For inline charges, send the customer data as inline_customer.
Two ways to trigger it
- Create an instrument. Validate once, then reuse the instrument for future charges.
- Create a charge with
inline_instrument. Validate and charge in a single call.
Both flows use the same match logic and the same billing rule: you only pay for the first successful validation of an account (see Billing).
Option 1: Validate when creating an instrument
Use this flow to save a customer’s bank account for repeat billing. Call POST /instruments.
{
"customer_id": "33333333-3333-4333-8333-333333333333",
"type": "mx_clabe",
"mx_clabe": {
"clabe": "000000000000000001"
}
}The instrument is created right away with status: "verification_in_progress", and the check starts in the background.
{
"id": "66666666-6666-4666-8666-666666666666",
"org_id": "00000000-0000-4000-8000-000000000000",
"customer_id": "33333333-3333-4333-8333-333333333333",
"type": "mx_clabe",
"status": "verification_in_progress",
"ownership_verification_result": null,
"ownership_verification_result_at": null,
"mx_clabe": {
"clabe": "000000000000000001",
"can_credit": true,
"can_debit": true
},
"created_at": "2026-01-15T12:00:00Z",
"updated_at": null
}How the instrument status changes
| CEP result | status |
ownership_verification_result |
|---|---|---|
| Account holder matches the customer | active |
matched |
| Account holder does not match | errored |
no_match |
| CEP couldn’t be retrieved | errored |
no_match |
| Validation error occurred | errored |
errored |
| The bank reports the account is canceled | errored |
account_canceled |
| The bank reports the account does not exist | errored |
account_does_not_exist |
| Still waiting on CEP | verification_in_progress |
null |
active: safe to use. You can create charges that reference this instrument’sid.errored: the account failed validation. Charges that reference this instrument are rejected (see below).verification_in_progress: the CEP hasn’t arrived yet. Wait for the webhook, or poll the instrument.
Option 2: Validate when creating a charge
Use this flow to validate and charge in one call, for example for a one-off direct debit where you don’t need to store the account. Call POST /charges.
When a charge includes both inline_instrument and inline_customer, penny validation always runs. You can’t skip it in this flow.
How long it takes depends on whether the account was validated before.
- First validation of the account: the charge stays in
verification_in_progresswhile Monato waits for the CEP. This is typically under 90 seconds, but can take up to 3 hours in the worst case (see CEP processing timeline). - Account already validated: the charge moves to
pendingorcanceledalmost instantly.
{
"amount": 150.00,
"currency": "mxn",
"reference": "INV-001",
"inline_instrument": {
"type": "mx_clabe",
"identifier": "000000000000000001"
},
"inline_customer": {
"name": "Jane Doe",
"document_type": "mx_rfc",
"document_number": "XXXX000000XXX"
}
}{
"id": "77777777-7777-4777-8777-777777777777",
"org_id": "00000000-0000-4000-8000-000000000000",
"instrument_id": null,
"customer_id": null,
"amount": 150.00,
"currency": "mxn",
"reference": "INV-001",
"status": "verification_in_progress",
"inline_instrument": { "type": "mx_clabe", "identifier": "000000000000000001" },
"inline_customer": { "name": "Jane Doe", "document_type": "mx_rfc", "document_number": "XXXX000000XXX" },
"risk_status": null,
"risk_evaluated_at": null,
"created_at": "2026-01-15T12:00:00Z",
"updated_at": null
}How the charge status changes
Penny validation runs before the risk engine. Only charges whose ownership matched go on to the risk engine.
| CEP result | status |
ownership_verification_result |
|---|---|---|
| Account holder matches the customer | pending (ready to process) |
matched |
| Account holder does not match | canceled |
no_match |
| CEP couldn’t be retrieved | canceled |
no_match |
| Validation error occurred | canceled |
errored |
| The bank reports the account is canceled | canceled |
account_canceled |
| The bank reports the account does not exist | canceled |
account_does_not_exist |
| Still waiting on CEP | verification_in_progress |
null |
Trying to charge an un-validated instrument
If you create a charge with an instrument_id that is not active (for example errored after a failed validation, or still verification_in_progress), the API rejects it with 400. Wait for the instrument to become active before you charge it.
{
"instrument_id": "66666666-6666-4666-8666-666666666666",
"amount": 150.00,
"currency": "mxn",
"reference": "INV-002"
}{
"error_code": "invalid_input",
"error_message": "instrument_in_invalid_state"
}Once validation fails, the instrument is a dead end. Ask the customer for a different account and create a new instrument.
Billing
Penny validation is billed per account, not per request.
- The first time an account is validated successfully, meaning the validation completed with
matchedorno_match, you are charged for it. - Later validations of the same account are free, whether they come from a new instrument or a charge with
inline_instrument. - A validation that did not complete with
matchedorno_match(for example, the CEP was never retrieved and it ended aserrored) doesn’t count as the billable first validation. The next attempt on that account is billed as the first.
The comparison with the customer still runs every time. An account that matched one customer can come back as no_match against a different customer. Only the billing changes.
Batch charges
Every charge created from a batch file (SFTP or Portal) behaves exactly like a single charge with inline_instrument. The same rules apply to each row:
- Penny validation always runs for each charge.
- Penny validation runs first. Only charges whose ownership matched go on to the risk engine.
- Each charge changes status independently, based on its own CEP result, using the same table as single charges.
- Billing follows the same per-account rule: you only pay for the first successful validation of each unique account, however many charges in the batch (or across batches) use it.
CEP processing timeline
When an account has never been validated, Monato has to pull the CEP from Banxico. Most complete within 90 seconds, but banks vary. If the CEP isn’t available on the first try, Monato retries automatically over roughly 3 hours.
As soon as an attempt succeeds, the result is locked in as matched or no_match, and the instrument or charge moves on.
If every attempt comes back empty, the validation fails:
- The instrument is set to
erroredwithno_match. - Any associated charges are
canceledwithno_match.
Once an account has a matched or no_match result, future validations of that account resolve almost instantly.
Webhooks
Monato reports penny validation outcomes by webhook, so you don’t need to poll.
| Trigger | Event type | When it fires |
|---|---|---|
Charge with inline_instrument |
charge_result |
Only when the charge is canceled due to no_match or errored |
| Instrument creation | instrument_ownership_verification_result |
Whenever the instrument transitions to active or errored |
For inline charges, successful validations don’t send a dedicated webhook. The charge moves on to normal processing and you get the usual events for it.
{
"event": "charge_result",
"timestamp": "2026-01-15T12:01:30.000000+00:00",
"data": {
"charge_id": "77777777-7777-4777-8777-777777777777",
"charge_result": "canceled",
"charge_reference": "INV-001",
"amount": 150.0,
"declined_reason": null,
"risk_status": "ok",
"risk_reasons": null,
"client_debt_id": null,
"instrument_identifier": null,
"ownership_verification_result": "no_match",
"ownership_verification_result_at": "2026-01-15T12:01:29.450000+00:00",
"ownership_information": {
"name": "JOHN ROE",
"document_id": "XXXX000000YYY"
}
}
}{
"event": "instrument_ownership_verification_result",
"timestamp": "2026-01-15T12:05:00.000000+00:00",
"data": {
"instrument_id": "66666666-6666-4666-8666-666666666666",
"ownership_verification_result": "matched",
"ownership_verification_result_at": "2026-01-15T12:04:58.200000+00:00",
"ownership_information": {
"name": "JANE DOE",
"document_id": "XXXX000000XXX"
},
"is_test": false
}
}ownership_information carries the account holder data from the CEP whatever the result. It is null when no CEP is available. See Webhook events for every field.
Status flows
Created
→ verification_in_progress
→ active (matched)
→ errored (no_match / errored / account_canceled / account_does_not_exist)Created
→ verification_in_progress
→ Penny validation
→ canceled (no_match / errored / account_canceled / account_does_not_exist)
→ pending (matched)
→ Risk engine
→ processing → confirmed / declined
→ canceled (cancelled_by_risk)Created
→ pending
→ Risk engine
→ processing → confirmed / declinedFAQ
Can I opt a single instrument or charge out of penny validation?
No. Once your organization has it enabled, it runs automatically. If you don’t want validation on a specific charge, reference an already-validated instrument_id instead of using inline_instrument.
What if the customer data doesn’t exactly match what the bank has?
The comparison tolerates common formatting differences (casing, whitespace, punctuation in names). Material mismatches, such as a different name or a different RFC, come back as no_match and the charge is canceled.
How do I know when validation is done? Listen for the webhooks above, or poll the instrument or charge.