Skip to main content
POST
Confirms a South African bank account belongs to the named holder via the inter-bank Account Holder Verification service. Didit exposes this service through POST /v3/database-validation/ so you can verify the submitted data against the authoritative source and receive normalized match results.

Coverage

  • Coverage: —
  • Country: South Africa
  • Service ID: zaf_bank_account_holder
  • Data domain: Financial
  • Category: Banking

Inputs

  • Required inputs: national_id, bank_account_number, bank_name
  • Optional inputs: account_type, first_name, last_name, initials, email, phone_number, vendor_data
  • Consent: Required
  • Workflow availability: Available in workflow with field mapping
  • Coverage: —
  • Price: $0.40 per successful query
In a workflow, bank_account_number and bank_name are not printed on an identity document, so no step produces them on their own. Collect them from a Questionnaire answer, a Document AI extraction field, or a value you send when you create the session, and map each one on the Database Validation step. Until they are mapped the workflow editor shows this service disabled and names the missing input; at run time a missing input means the check is skipped before any query, so you are never billed for a database that could not be asked.

Body parameters

string
default:"ZAF"
required
ISO 3166-1 alpha-3 country code for this database service.Example: ZAF
string
default:"zaf_bank_account_holder"
required
Array containing this service ID. Pinning the service keeps the request scoped to this exact database.Example: zaf_bank_account_holder
Explicit end-user consent for this service.Example: true
string
default:"8708150847085"
required
National identity number for this service.Example: 8708150847085
string
default:"1234567890"
required
bank_account_number value required by this database service.Example: 1234567890
string
default:"ABSA"
required
bank_name value required by this database service.Example: ABSA
string
default:"current"
account_type value required by this database service.Example: current
string
default:"John"
Given name to validate.Example: John
string
default:"Doe"
Family name to validate.Example: Doe
string
default:"JD"
initials value required by this database service.Example: JD
string
default:"john.doe@example.com"
Email address.Example: john.doe@example.com
string
default:"+15550101000"
phone_number value required by this database service.Example: +15550101000
string
default:"user-1234"
Your stable user reference for this person, such as your internal user ID. Didit uses it to link standalone checks to the same end user and reduce duplicate-detection noise.Example: user-1234

Input rules & validation notes

  • account_type must be one of the South African AVS account types: current, savings, transmission, subscriptionShare, notKnown. Matching is case-insensitive. Omit the field when you do not know the account type - Didit then sends notKnown and the account-type cross-check is reported as not confirmed instead of failing the request.

How to call it

Returned data

The exact fields surfaced in source_data depend on what the registry returns. The generated example for zaf_bank_account_holder currently documents this normalized shape:
  • accepts_credits
  • accepts_debits
  • account_found
  • account_number_length_valid
  • account_open
  • account_type_match
  • id_match
  • identification_number
  • initials_match
  • surname_match

Pricing & SLAs

South Africa - Bank Account Holder Verification queries are billed only when Didit receives a conclusive result from the validation source.
  • Per-call price: $0.40 USD.
  • Billing: per successful query. You are not charged when the registry is unreachable, when required fields are missing, or when the request is rejected before reaching the source.
  • Latency: typical p95 < 2 s.
  • Availability: 99.9% per quarter on Didit’s side; downstream source availability varies by country and dataset.

What a MATCH confirms

Account Holder Verification (AVS) is a set of yes/no cross-checks, not a record lookup. The source never returns a name, an ID number or an account balance - it takes the details you send, compares them against what the bank holds for that account, and answers each comparison with a flag. source_data carries those flags:
  • account_found - the bank holds an account with this number. Everything else is meaningless when this is false.
  • account_open - the account is open. An account can be found but closed.
  • id_match - the account is held by the national id you submitted. This is the check the service exists for.
  • initials_match, surname_match - the submitted initials and last_name match the account holder on file.
  • account_type_match - the submitted account_type matches the account’s real type.
  • email_match, phone_match - the submitted email and phone_number match the contact details the bank holds.
  • accepts_debits, accepts_credits - the account can receive debit orders and credits.
  • account_number_length_valid - the account number is the length the bank expects.
The outcome code summarises them:
  • MATCH - the account exists and every field you submitted matched, id_match included.
  • PARTIAL_MATCH - the account is held by the submitted national id, but at least one other field you sent did not match (a surname spelled differently on the bank’s records, for example). outcome_detail names the fields that failed.
  • NO_MATCH - either the bank holds no such account, or the account exists but belongs to someone else. Both are conclusive answers about the person.
A cross-check you did not ask for is neither counted nor reported. AVS answers N for every field that was not supplied, so surname_match, initials_match, email_match, phone_match and account_type_match appear in source_data only when you sent the matching input. Sending only national_id, bank_account_number and bank_name therefore still produces a full MATCH.

Accepted bank_name and account_type values

bank_name and account_type are enumerations. A value outside either list is refused before the query runs, so a typo costs you a round-trip rather than a charge. account_type is optional and accepts, case-insensitively: current · savings · transmission · subscriptionShare · notKnown Omit it when you do not know the account type. Didit then sends notKnown, the request succeeds, and account_type_match comes back false because there was nothing to compare. Any other value - checking, cheque, bond - is rejected with a 400 that lists the accepted set. bank_name is required, and these values are confirmed accepted (case-insensitive): ABSA · Capitec · FNB · Nedbank · Standard Bank · Investec · African Bank · Discovery Bank · Old Mutual · Bank Zero · Tyme · Grindrod Consumer accounts at all of these are in scope, Capitec included. Send the short form exactly as listed - Capitec, not Capitec Bank; Tyme, not TymeBank. An unrecognised bank comes back as an outcome of REGISTRY_ERROR whose outcome_detail names the field the source refused.

Telling a missing account apart from an unavailable source

These are different HTTP outcomes, so no parsing of error strings is needed:
  • The account does not exist - 200 OK, match_type: "no_match", outcome_code: "NO_MATCH", source_data.account_found: false. A real answer from the bank. Billed, and your no-match action applies.
  • The source did not answer - 502 Bad Gateway with validation_errors[].code = empty_provider_response and retryable: true. Nothing was established about the person, nothing is billed, and the request is safe to retry.
  • The source refused to run the query - 400 Bad Request with validation_errors[].code = provider_rejected_input and retryable: false. The source never searched, because it would not accept what it was sent. Correct the data; retrying the same values cannot succeed. Nothing is billed.
  • The source answered and rejected your input - 502 Bad Gateway with validation_errors[].code = provider_invalid_input (for example an account number that fails the bank’s own format check). Retrying is pointless until the input is corrected; nothing is billed.
  • Your request never left Didit - 400 Bad Request, listing the field to fix. Nothing is billed.
Only the first of these is an answer about the person. Treat a 502 as transient - AVS relays to the account holder’s bank rather than reading a registry, so it is the slowest of the South African services - and retry with backoff.

Continue reading