curl -X POST "https://verification.didit.me/v3/database-validation/" \
-H "x-api-key: YOUR_API_KEY" \
-F "issuing_state=ZAF" \
-F "services=zaf_bank_account_holder" \
-F "vendor_data=user-1234" \
-F "consent=true" \
-F "national_id=8708150847085" \
-F "bank_account_number=1234567890" \
-F "bank_name=ABSA" \
-F "account_type=current" \
-F "last_name=Doe" \
-F "initials=JD"
{
"request_id": "req_01H…",
"status": "Approved",
"issuing_state": "ZAF",
"match_type": "full_match",
"validations": [
{
"outcome_code": "MATCH",
"service_id": "zaf_bank_account_holder",
"service_name": "South Africa - Bank Account Holder Verification",
"source_data": {
"accepts_credits": true,
"accepts_debits": true,
"account_found": true,
"account_number_length_valid": true,
"account_open": true,
"account_type_match": true,
"id_match": true,
"identification_number": "SAMPLE-ID-12345",
"initials_match": true,
"surname_match": true
},
"validation": {
"full_name": "full_match",
"identification_number": "full_match"
}
}
]
}
{
"request_id": "req_01H…",
"status": "In Review",
"issuing_state": "ZAF",
"match_type": "partial_match",
"validations": [
{
"outcome_code": "PARTIAL_MATCH",
"service_id": "zaf_bank_account_holder",
"service_name": "South Africa - Bank Account Holder Verification",
"source_data": {
"accepts_credits": true,
"accepts_debits": true,
"account_found": true,
"account_number_length_valid": false,
"account_open": true,
"account_type_match": true,
"id_match": true,
"identification_number": "SAMPLE-ID-12345",
"initials_match": true,
"surname_match": false
},
"validation": {
"full_name": "partial_match",
"identification_number": "full_match"
}
}
]
}
{
"request_id": "req_01H…",
"status": "Declined",
"issuing_state": "ZAF",
"match_type": "no_match",
"validations": [
{
"outcome_code": "NO_MATCH",
"service_id": "zaf_bank_account_holder",
"service_name": "South Africa - Bank Account Holder Verification",
"source_data": {
"accepts_credits": false,
"accepts_debits": false,
"account_found": false,
"account_number_length_valid": false,
"account_open": false,
"account_type_match": false,
"id_match": false,
"identification_number": "NO_MATCH",
"initials_match": false,
"surname_match": false
},
"validation": {
"identification_number": "no_match"
}
}
]
}
🇿🇦 South Africa
South Africa - Bank Account Holder Verification
Confirms a South African bank account belongs to the named holder via the inter-bank Account Holder Verification service. Authoritative real-time identity lookup for South Africa. Real-time lookup, pay-per-call.
POST
/
v3
/
database-validation
/
curl -X POST "https://verification.didit.me/v3/database-validation/" \
-H "x-api-key: YOUR_API_KEY" \
-F "issuing_state=ZAF" \
-F "services=zaf_bank_account_holder" \
-F "vendor_data=user-1234" \
-F "consent=true" \
-F "national_id=8708150847085" \
-F "bank_account_number=1234567890" \
-F "bank_name=ABSA" \
-F "account_type=current" \
-F "last_name=Doe" \
-F "initials=JD"
{
"request_id": "req_01H…",
"status": "Approved",
"issuing_state": "ZAF",
"match_type": "full_match",
"validations": [
{
"outcome_code": "MATCH",
"service_id": "zaf_bank_account_holder",
"service_name": "South Africa - Bank Account Holder Verification",
"source_data": {
"accepts_credits": true,
"accepts_debits": true,
"account_found": true,
"account_number_length_valid": true,
"account_open": true,
"account_type_match": true,
"id_match": true,
"identification_number": "SAMPLE-ID-12345",
"initials_match": true,
"surname_match": true
},
"validation": {
"full_name": "full_match",
"identification_number": "full_match"
}
}
]
}
{
"request_id": "req_01H…",
"status": "In Review",
"issuing_state": "ZAF",
"match_type": "partial_match",
"validations": [
{
"outcome_code": "PARTIAL_MATCH",
"service_id": "zaf_bank_account_holder",
"service_name": "South Africa - Bank Account Holder Verification",
"source_data": {
"accepts_credits": true,
"accepts_debits": true,
"account_found": true,
"account_number_length_valid": false,
"account_open": true,
"account_type_match": true,
"id_match": true,
"identification_number": "SAMPLE-ID-12345",
"initials_match": true,
"surname_match": false
},
"validation": {
"full_name": "partial_match",
"identification_number": "full_match"
}
}
]
}
{
"request_id": "req_01H…",
"status": "Declined",
"issuing_state": "ZAF",
"match_type": "no_match",
"validations": [
{
"outcome_code": "NO_MATCH",
"service_id": "zaf_bank_account_holder",
"service_name": "South Africa - Bank Account Holder Verification",
"source_data": {
"accepts_credits": false,
"accepts_debits": false,
"account_found": false,
"account_number_length_valid": false,
"account_open": false,
"account_type_match": false,
"id_match": false,
"identification_number": "NO_MATCH",
"initials_match": false,
"surname_match": false
},
"validation": {
"identification_number": "no_match"
}
}
]
}
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
| Field | Required | Example |
|---|---|---|
national_id | Yes | 8708150847085 |
bank_account_number | Yes | 1234567890 |
bank_name | Yes | ABSA |
account_type | No | current |
first_name | No | John |
last_name | No | Doe |
initials | No | JD |
email | No | john.doe@example.com |
phone_number | No | +15550101000 |
vendor_data | No | user-1234 |
- 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
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:
ZAFstring
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_holderboolean
default:"true"
required
Explicit end-user consent for this service.Example:
truestring
default:"8708150847085"
required
National identity number for this service.Example:
8708150847085string
default:"1234567890"
required
bank_account_number value required by this database service.Example: 1234567890string
default:"ABSA"
required
bank_name value required by this database service.Example: ABSAstring
default:"current"
account_type value required by this database service.Example: currentstring
default:"John"
Given name to validate.Example:
Johnstring
default:"Doe"
Family name to validate.Example:
Doestring
default:"JD"
initials value required by this database service.Example: JDstring
default:"john.doe@example.com"
Email address.Example:
john.doe@example.comstring
default:"+15550101000"
phone_number value required by this database service.Example: +15550101000string
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-1234Input rules & validation notes
account_typemust 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 sendsnotKnownand the account-type cross-check is reported as not confirmed instead of failing the request.
How to call it
curl -X POST "https://verification.didit.me/v3/database-validation/" \
-H "x-api-key: YOUR_API_KEY" \
-F "issuing_state=ZAF" \
-F "services=zaf_bank_account_holder" \
-F "vendor_data=user-1234" \
-F "consent=true" \
-F "national_id=8708150847085" \
-F "bank_account_number=1234567890" \
-F "bank_name=ABSA" \
-F "account_type=current" \
-F "last_name=Doe" \
-F "initials=JD"
Every successful call returns HTTP 200. The outcome_code field tells you what actually happened — distinguishing, for example, a real biometric mismatch (BIOMETRIC_NO_MATCH) from a selfie that could not be read (BIOMETRIC_IMAGE_UNUSABLE). The status shown is the default feature status; your configured Partial Match / No Match actions can override it.MATCH — The registry confirmed the identity and every checked field matched.{
"request_id": "req_01H…",
"status": "Approved",
"issuing_state": "ZAF",
"match_type": "full_match",
"validations": [
{
"outcome_code": "MATCH",
"service_id": "zaf_bank_account_holder",
"service_name": "South Africa - Bank Account Holder Verification",
"source_data": {
"accepts_credits": true,
"accepts_debits": true,
"account_found": true,
"account_number_length_valid": true,
"account_open": true,
"account_type_match": true,
"id_match": true,
"identification_number": "SAMPLE-ID-12345",
"initials_match": true,
"surname_match": true
},
"validation": {
"full_name": "full_match",
"identification_number": "full_match"
}
}
]
}
PARTIAL_MATCH — The identification number was found but one or more personal fields (usually name or date of birth) did not fully match.{
"request_id": "req_01H…",
"status": "In Review",
"issuing_state": "ZAF",
"match_type": "partial_match",
"validations": [
{
"outcome_code": "PARTIAL_MATCH",
"service_id": "zaf_bank_account_holder",
"service_name": "South Africa - Bank Account Holder Verification",
"source_data": {
"accepts_credits": true,
"accepts_debits": true,
"account_found": true,
"account_number_length_valid": false,
"account_open": true,
"account_type_match": true,
"id_match": true,
"identification_number": "SAMPLE-ID-12345",
"initials_match": true,
"surname_match": false
},
"validation": {
"full_name": "partial_match",
"identification_number": "full_match"
}
}
]
}
NO_MATCH — The registry returned no match for the submitted data.{
"request_id": "req_01H…",
"status": "Declined",
"issuing_state": "ZAF",
"match_type": "no_match",
"validations": [
{
"outcome_code": "NO_MATCH",
"service_id": "zaf_bank_account_holder",
"service_name": "South Africa - Bank Account Holder Verification",
"source_data": {
"accepts_credits": false,
"accepts_debits": false,
"account_found": false,
"account_number_length_valid": false,
"account_open": false,
"account_type_match": false,
"id_match": false,
"identification_number": "NO_MATCH",
"initials_match": false,
"surname_match": false
},
"validation": {
"identification_number": "no_match"
}
}
]
}
Returned data
The exact fields surfaced insource_data depend on what the registry returns. The generated example for zaf_bank_account_holder currently documents this normalized shape:
accepts_creditsaccepts_debitsaccount_foundaccount_number_length_validaccount_openaccount_type_matchid_matchidentification_numberinitials_matchsurname_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 isfalse.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 submittedinitialsandlast_namematch the account holder on file.account_type_match- the submittedaccount_typematches the account’s real type.email_match,phone_match- the submittedemailandphone_numbermatch 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.
MATCH- the account exists and every field you submitted matched,id_matchincluded.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_detailnames 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.
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 Gatewaywithvalidation_errors[].code=empty_provider_responseandretryable: 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 Requestwithvalidation_errors[].code=provider_rejected_inputandretryable: 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 Gatewaywithvalidation_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.
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.