A beneficiary is a destination account saved against the API key's user_id — a per-user allow-list, never shared across accounts. Persisting beneficiaries gives you three concrete behaviors: 1) auto-fill of bank and type on later validations, 2) a priority source for resolving masked PANs that the OCR cannot complete on its own, and 3) the bilingual alias that exports and the account statement in Finance carry.

The three account types

MexCep detects account_type from digit count (src/Support/CardBins.php::detectAccountType):

  • clabe — exactly 18 digits. Bank derived from the 3-digit prefix via BanxicoBanks::bankFromClabe. bank_code is not required in the body.
  • card — 13 to 19 digits, excluding 18 (reserved for CLABE) and 10 (reserved for phone). Bank inferred from BIN/dictionary. bank_code is not required.
  • phone — exactly 10 digits. This is a SPEI DiMo handle. bank_code is mandatory in the body because no public phone-to-bank mapping exists; the endpoint replies 422 bank_code_required_for_phone when it is missing. The catalog of codes lives at GET /v1/public/banks.

To validate structure without persisting (CLABE checksum or card Luhn) use GET /v1/beneficiaries/validate-account.

Masked PAN and OCR

When POST /v1/validate-ocr extracts an account from a receipt in the shape ••••5678 or 4111 •••• •••• 1111, the pipeline routes through NormalizationService::resolveMaskedAccount. The lookup uses the visible trailing digits in this order:

  1. User allow-listBeneficiaryRepository::findByPartialAccount filters the user's active beneficiaries by suffix + length_hint + visible_first. Single match → resolved.
  2. Global account directory by suffix — only when the allow-list does not resolve. Returns masked_catalog_unique on a single match, masked_probe_pending with candidates for 2..N matches (N = clabe.probe_max_candidates, default 3), masked_ambiguous_in_catalog above that, or masked_unresolvable when no row carries that suffix.

Without registered beneficiaries a masked receipt drops straight to the global suffix lookup and may stay ambiguous. Registering the accounts you validate often turns these cases into deterministic resolutions.

Create, archive, import

  • POST /v1/beneficiaries creates one. Accepts an optional alias used in exports.
  • GET /v1/beneficiaries lists with the tri-state ?with_archived= filter (0/null = active, 1 = archived, omitted = both).
  • DELETE /v1/beneficiaries/{id} archives — a soft-delete (UPDATE user_beneficiaries SET status='inactive'). The public API does not expose hard-delete; archived rows can be restored from the admin panel when the deploy operator needs to.
  • PATCH /v1/beneficiaries/{id} edits the alias or bank_code (mandatory when the type is phone and the field is included in the edit).

For bulk creation the endpoint is POST /v1/beneficiaries/imports — see Bulk imports for the preview → commit flow and the manual+Gemini hybrid parser.