# Penny Validation API

## Overview

The Penny Validation API enables account ownership verification by sending a minimal payment (penny) to a beneficiary account. This process generates a CEP (Comprobante Electrónico de Pago) that provides verified account holder information and creates an official payment receipt through BANXICO.

### Key Benefits

- **Account Verification:** Confirm the actual account holder matches your intended recipient
- **Official Documentation** Get a easy to use URL to obtain a XML or PDF receipts for compliance and record-keeping directly from Banxico's website.
- **Fraud Prevention:** Automate payments, reduce errors and fraudulent transactions


### Important Considerations

- **Processing Time:** CEP generation depends on the beneficiary's bank and may vary significantly
- **Supported Accounts:** Currently supports CLABE accounts only (debit card support planned)
- **Data delivery:** CEP results are delivered via a dedicated CEP webhook for Penny Validation.
- **Transient initial state** Immediately after creation, metadata.dataCep.status may briefly be INITIALIZED. Treat it as equivalent to PENDING for client logic/UI.
- **RFC is optional in the Instrument:** You may register instruments with rfc = "ND"


### CEP status values

Client-facing values for `metadata.dataCep.status` are:

- **`INITIALIZED`** — **transient** initial state right after creation; typically flips to `PENDING` within seconds. **Do not** build distinct flows based on this state.
- **`PENDING`** — waiting for CEP retrieval/confirmation.
- **`DELAYED`** — CEP is taking longer than expected; background retries continue.
- **`COMPLETED`** — CEP available and validated.
- **`FAILED`** — CEP unavailable or permanently failed.


> **Note:** Treat **`INITIALIZED`** the same as **`PENDING`** in client logic and UI.


## Penny Validation endpoint

- **Prerequisites** You need to have a [CENTRALIZING_ACCOUNT](/products/fincore/guides/quickstart#centralizing-account) and an [INSTRUMENT](/products/fincore/guides/quickstart#create-an-instrument) previously created. And you need to create a new webhook for the penny validation message.


**Endpoint**
`POST /v1/transactions/penny_validation`

**Request**
**Path parameters**: none

**Query Parameters**: none

**Request Body**:


```json
{
  "client_id": "{{client_id}}",
  "source_instrument_id": "",          // use your own centralizing account where funds are located
  "destination_instrument_id": "",     // add the id of the previously generated instrument
  "description": "Validacion de cuenta",  // optional (max 40 chars)
  "external_reference": "1234567"             // optional (max 7 digits)
}
```

### Field behavior & defaults

#### `description` (optional)

- **Type:** string (**letters numbers and spaces only**; **special characters not allowed**, except **`ñ`/`Ñ`**)
- **Max length:** **40 characters**
- If provided, it is stored as the transaction description/concept.
- If not provided, backend sets:
  - **Default:** `"Validacion de cuenta"`


It must be visible in:

- `GET /v1/clients/{clientId}/transactions/{transactionId}` (`description`)
- Penny Validation webhook/notification (concept field, e.g. `payment_concept`)


#### `external_reference` (optional)

- **Type:** string (numeric)
- **Max length:** **7 digits**
- If provided, it is stored **as-is** (no formatting changes).
- If not provided, backend generates a default based on **transaction_date**:
  - **Default format:** `ddmmaa` (e.g. 24/11/2025 → `"241125"`)


It must be visible in:

- `GET /v1/clients/{clientId}/transactions/{transactionId}` (`external_reference` / `externalReference` depending on API casing)
- Penny Validation webhook/notification


**Response**
**Status Code**: 200 OK
**Response Body**:


```json
{
    "id": "1eb4b5ac-71f6-4203-aded-4fcb7fd21637",
    "bankId": "14b402f6-5dd1-4cd8-ac54-d12c5647d137",
    "clientId": "09b1c156-73c7-62e1-9a3e-bf705f8f3cbe",
    "externalReference": "1234567",
    "trackingId": "20250820FINCHXXXXQ6RPX4",
    "description": "Validacion de cuenta",
    "amount": "0.01",
    "currency": "MXN",
    "category": "DEBIT_TRANS",
    "subCategory": "SPEI_DEBIT",
    "transactionStatus": "INITIALIZED",
    "audit": {
        "createdAt": "2025-08-19 13:03:36.194761-06:00",
        "updatedAt": "2025-08-19 13:03:36.194761-06:00",
        "deletedAt": "None",
        "blockedAt": "None"
    },
    "metadata": {
        "dataCep": {
            "cepUrl": "https://www.banxico.org.mx/cep/go?i=90734&s=20250401&d=uBq%2BmmaxaJx72v%2FafY3P2XgtF6PkNIxovKrCElr14xnUBOcXkfe9Vllu8xJ%2FHNPE6YqJ9U%2F2BEcsd3jbcB8OdTqrcqUZYkYQYxzjg4WIVSg%3D",
            "validationId": "f4ebe9af-6c93-4218-8dcb-74cb7456f3cd",
            "status": "PENDING",
            "createdAt": "2025-08-19 13:03:36.732586-06:00"
        }
    }
}
```

The response is mostly the same as a money out transaction. But, after around 90 seconds (average time for most banks) the CEP Webhook will return the information parsed in the response.
You could also manually fetch the transaction data, together with the updated metadata through the transactions endpoint.

> **Note:** Immediately after creation, the very first read may briefly return **`INITIALIZED`** before transitioning to **`PENDING`**. Clients should treat **`INITIALIZED`** as equivalent to **`PENDING`** for logic and UI.



```
GET /v1/clients/:client_id/transactions/:id
```

## CEP Webhook Response


```json
{
  "client_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "payload": {
    "id_msg": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "msg_name": "CEP",
    "msg_date": "2025-08-15",
    "body": {
      "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "tracking_key": "20250815XXXXXX123456789",
      "external_reference": "1234567",
      "payment_concept": "Validacion de cuenta para Colegiatura",
      "beneficiary_account": "000000000000000000",
      "beneficiary_name": "Daniela Paola Santelices Chavez",
      "beneficiary_rfc": "XXXX000000XXX",
      "status": "COMPLETED",
      "processed_at": "2025-08-15 16:42:39.327313-06:00"
    }
  },
  "webhook_type": "CEP"
}
```

> **Defaults in webhook:** If `description` is omitted, `payment_concept` defaults to `"Validacion de cuenta"`.
If `external_reference` is omitted, it defaults to the operation date formatted as `ddmmaa` (e.g. 24/11/2025 → `"241125"`).


## Transaction response


```json
{
    "id": "UUID_1",
    "bankId": "UUID_2",
    "clientId": "UUID_3",
    "externalReference": "1234567",
    "trackingId": "TRACKING_ID_001",
    "description": "Validacion de cuenta",
    "amount": "0.01",
    "currency": "MXN",
    "category": "DEBIT_TRANS",
    "subCategory": "SPEI_DEBIT",
    "transactionStatus": "LIQUIDATED",
    "audit": {
        "createdAt": "2025-01-01 00:00:00.000000-00:00",
        "updatedAt": "2025-01-01 00:00:00.000000-00:00",
        "deletedAt": "None",
        "blockedAt": "None"
    },
    "jsonReference": "",
    "sourceInstrument": {
        "id": "UUID_4",
        "bankId": "UUID_2",
        "clientId": "UUID_3",
        "ownerId": "UUID_5",
        "instrumentAlias": "InternalAccount",
        "instrumentStatus": "ACTIVE",
        "instrumentType": "SENDER_RECEIVER",
        "instrumentDetail": {
            "accountNumber": "ACCOUNT_001",
            "clabeNumber": "CLABE_001",
            "holderName": "ANCV"
        },
        "rfc": "ND"
    },
    "destinationInstrument": {
        "id": "UUID_6",
        "bankId": "UUID_7",
        "clientId": "UUID_3",
        "ownerId": "UUID_3",
        "instrumentAlias": "CLABE",
        "instrumentStatus": "ACTIVE",
        "instrumentType": "SENDER_RECEIVER",
        "instrumentDetail": {
            "accountNumber": "ACCOUNT_002",
            "clabeNumber": "CLABE_002",
            "holderName": "BENEFICIARY_NAME"
        },
        "rfc": "RFC_001"
    },
    "metadata": {
        "dataCep": {
            "cepUrl": "https://example.com/cep/SPECIAL_URL_FOR_CEP",
            "validationId": "UUID_8",
            "beneficiaryName": "BENEFICIARY_NAME",
            "beneficiaryRfc": "RFC_001",
            "status": "COMPLETED", // possible: INITIALIZED (transient) | PENDING | DELAYED | COMPLETED | FAILED
            "createdAt": "2025-01-01 00:00:00.000000-00:00",
            "processedAt": "2025-01-01 00:00:00.000000-00:00"
        }
    }
}
```

## Delivery & Retry Policy (CEP Webhook)

**Total attempts:** 17 (≈ **3:03 hrs** total)
**Phases:**

- **Attempts 1–3:** every **90s** → `t0`, `+1:30`, `+3:00`
- **Attempts 4–6:** every **5 min** → `+8:00`, `+13:00`, `+18:00`
- **Attempts 7–17:** every **15 min** → from `+33:00` … up to `+3:03:00` (final attempt)


### Detailed schedule (with webhook status)

| Attempt | Delay from previous | Elapsed time | Webhook status (typical) |
|  --- | --- | --- | --- |
| 1 | 0:00:00 | 0:00:00 | `PENDING` |
| 2 | 0:01:30 | 0:01:30 | `PENDING` |
| 3 | 0:01:30 | 0:03:00 | `PENDING` |
| 4 | 0:05:00 | 0:08:00 | `DELAYED` |
| 5 | 0:05:00 | 0:13:00 | `DELAYED` |
| 6 | 0:05:00 | 0:18:00 | `DELAYED` |
| 7 | 0:15:00 | 0:33:00 | `DELAYED` |
| 8 | 0:15:00 | 0:48:00 | `DELAYED` |
| 9 | 0:15:00 | 1:03:00 | `DELAYED` |
| 10 | 0:15:00 | 1:18:00 | `DELAYED` |
| 11 | 0:15:00 | 1:33:00 | `DELAYED` |
| 12 | 0:15:00 | 1:48:00 | `DELAYED` |
| 13 | 0:15:00 | 2:03:00 | `DELAYED` |
| 14 | 0:15:00 | 2:18:00 | `DELAYED` |
| 15 | 0:15:00 | 2:33:00 | `DELAYED` |
| 16 | 0:15:00 | 2:48:00 | `DELAYED` |
| 17 | 0:15:00 | **3:03:00** | `FAILED` *(only if no CEP confirmation)* |


### Webhook-reported states

- **`PENDING`** → from `t0` through **attempt 3**
- **`DELAYED`** → from **attempt 4** through **attempt 16**
- **`FAILED`** → **only** if no confirmation by **attempt 17**


> When the CEP is issued, the webhook will send **`COMPLETED`** (see “CEP Webhook Response” example below).


#### Example payload with intermediate states


```json
{
  "client_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "payload": {
    "id_msg": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "msg_name": "CEP",
    "msg_date": "2025-08-15",
    "body": {
      "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "tracking_key": "20250815XXXXXX123456789",
      "beneficiary_account": "000000000000000000",
      "beneficiary_name": "Daniela Chavez Juarez",
      "beneficiary_rfc": "XXXX000000XXX",
      "status": "DELAYED", // possible: INITIALIZED (transient) | PENDING | DELAYED | COMPLETED | FAILED
      "processed_at": null
    }
  },
  "webhook_type": "CEP"
}
```