Skip to main content

Bank Account Verification REST API

Stitch’s Bank Account Verification (BAV) service is available over REST and supports full integration via the REST API.

info

Please note the following about the Bank Account Verification product:

  • BAV is currently only available for South African bank accounts. Identifying documents and business registrations default to country ZA when country is omitted, explicit non-ZA values are passed through to the upstream verifier, though only ZA documents are routinely supported today.
  • A client token with both the client_bankaccountverification and accountholders scopes is required.

Please contact support@stitch.money if you have any questions about access.

Use the Stitch REST API URL https://api.stitch.money/v2 for all requests on both test and live clients. Standard HTTP status conventions are used for all API responses.

Authentication

A client token is required in order to verify a bank account. Follow the steps described in the client token guide to obtain a client token with the following scopes:

  • client_bankaccountverification — grants access to the bank-account-verification endpoint.
  • accountholders — required to read the verified identifyingDocument fields (ID number / passport number) on the response. Without it, the API returns 403 Forbidden with missingScopes: ["accountholders"].

Business verifications technically only need the accountholders scope when individual identifying documents are involved, but Stitch requires both scopes uniformly for predictable error handling.

Verifying a Bank Account

Initiate a POST request to /v2/bank-account-verifications to verify that a bank account exists, is open, and that the account holder details match the identity (or business registration) information you have on record.

curl -X POST 'https://api.stitch.money/v2/bank-account-verifications' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-d '{
"bankAccount": {
"bank": "absa",
"number": "1234567890",
"type": "current"
},
"accountHolder": {
"type": "individual",
"familyName": "Doe",
"initials": "JD",
"identifyingDocument": {
"type": "identity_document",
"number": "9001015009087"
}
}
}'

Request body

The request body has two top-level fields: bankAccount and accountHolder. Both are required.

bankAccount

FieldTypeDescription
bankstringThe bank where the account is held.
One of:
absa, african_bank, capitec, discovery_bank, fnb, grindrod_bank, investec, nedbank, sasfin_bank, standard_bank, tymebank, za_access_bank, za_albaraka_bank, za_bank_zero, za_bidvest, za_standard_chartered_bank.
branchCodestring (optional)The 6-digit branch code. When omitted, the universal branch code for the supplied bank is used.
numberstringThe bank account number. Numeric digits only, 6–13 characters.
typestring (optional)The type of bank account. One of:
current, savings, credit, loan, investment, other, unknown. Will be verified if provided.
tip

If you have a branch code on file and wish to verify with it specifically, supply branchCode alongside bank. Otherwise, supplying bank alone is sufficient — the universal branch code will be used automatically.

accountHolder

accountHolder is a discriminated object identified by the type field (individual or business).

accountHolder for an individual

When type is individual:

FieldTypeDescription
typestringMust be individual.
familyNamestring (optional)Surname or family name of the individual (max 60 chars). Will be verified if provided.
initialsstring (optional)Initials of the account holder, e.g. "JD" (max 5 chars). Will be verified if provided.
identifyingDocumentobjectThe identifying document for the individual. See below.

The identifyingDocument object accepts:

FieldTypeDescription
typestringThe document type. One of identity_document (for South African ID numbers) or passport.
countrystring (optional)ISO 3166-1 alpha-2 country code where the document was issued. Defaults to ZA when omitted.
numberstringThe document number.
info

BAVS verification is only available for South African bank accounts. The country field is exposed for forward compatibility and to align with the upstream contract — today, only ZA documents are routinely verified by the upstream provider.

accountHolder for a business

When type is business:

FieldTypeDescription
typestringMust be business.
registrationNumberstringThe business registration number.
countrystring (optional)ISO 3166-1 alpha-2 country code where the business is registered. Defaults to ZA when omitted.
namestring (optional)The registered business name (max 60 chars). Will be verified if provided.

Verification Examples

Individual (Local) Account

For South African users, verify the bank account against South African identity document information. Within accountHolder, set type to individual and supply identifyingDocument with type: identity_document:

curl -X POST 'https://api.stitch.money/v2/bank-account-verifications' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-d '{
"bankAccount": {
"bank": "absa",
"number": "1234567890",
"type": "current"
},
"accountHolder": {
"type": "individual",
"familyName": "Doe",
"initials": "JD",
"identifyingDocument": {
"type": "identity_document",
"number": "9001015009087"
}
}
}'

Example 200 OK response:

{
"id": "bav_1Q0PsIJvEtkwdCNYMSaVuR",
"bankAccount": {
"bank": "absa",
"branchCode": "632005",
"number": "1234567890",
"type": "current"
},
"accountHolder": {
"type": "individual",
"familyName": "Doe",
"initials": "JD",
"identifyingDocument": {
"type": "identity_document",
"country": "ZA",
"number": "9001015009087"
}
},
"verificationResult": {
"outcome": "verified",
"bankAccount": {
"exists": "verified",
"typeMatch": "verified",
"isOpen": true,
"isOpenForMoreThanThreeMonths": true,
"acceptsDebits": true,
"acceptsCredits": true
},
"accountHolder": {
"identifyingDocument": "verified",
"familyName": "verified",
"initials": "verified"
}
},
"createdAt": "2024-01-15T09:30:00Z",
"updatedAt": "2024-01-15T09:30:00Z"
}

Individual (Foreign) Account

For foreign users holding South African bank accounts, verify against passport document information. Supply identifyingDocument with type: passport and the issuing country (ISO 3166-1 alpha-2):

curl -X POST 'https://api.stitch.money/v2/bank-account-verifications' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-d '{
"bankAccount": {
"bank": "fnb",
"number": "1234567890",
"type": "savings"
},
"accountHolder": {
"type": "individual",
"familyName": "Smith",
"initials": "JS",
"identifyingDocument": {
"type": "passport",
"country": "GB",
"number": "A12345678"
}
}
}'

Example 200 OK response:

{
"id": "bav_2eyfk4nQpHs7iWQ4QwjJK5",
"bankAccount": {
"bank": "fnb",
"number": "1234567890",
"type": "savings"
},
"accountHolder": {
"type": "individual",
"familyName": "Smith",
"initials": "JS",
"identifyingDocument": {
"type": "passport",
"country": "GB",
"number": "A12345678"
}
},
"verificationResult": {
"outcome": "verified",
"bankAccount": {
"exists": "verified",
"typeMatch": "verified",
"isOpen": true,
"isOpenForMoreThanThreeMonths": true,
"acceptsDebits": true,
"acceptsCredits": true
},
"accountHolder": {
"identifyingDocument": "verified",
"familyName": "verified",
"initials": "verified"
}
},
"createdAt": "2024-01-15T09:30:00Z",
"updatedAt": "2024-01-15T09:30:00Z"
}

Business Account

For business accounts, verify against business registration information. Set accountHolder.type to business:

curl -X POST 'https://api.stitch.money/v2/bank-account-verifications' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-d '{
"bankAccount": {
"bank": "nedbank",
"number": "9876543210",
"type": "current"
},
"accountHolder": {
"type": "business",
"registrationNumber": "2020/123456/07",
"country": "ZA",
"name": "Acme Trading (Pty) Ltd"
}
}'

Example 200 OK response. Note that verificationResult.accountHolder carries only the keys that apply to a business (registrationNumber, name) — individual-only keys like familyName and initials are omitted entirely rather than returned as null.

{
"id": "bav_3RxGl8mTuSqXyZ1abc2DEF",
"bankAccount": {
"bank": "nedbank",
"number": "9876543210",
"type": "current"
},
"accountHolder": {
"type": "business",
"registrationNumber": "2020/123456/07",
"country": "ZA",
"name": "Acme Trading (Pty) Ltd"
},
"verificationResult": {
"outcome": "verified",
"bankAccount": {
"exists": "verified",
"typeMatch": "verified",
"isOpen": true,
"isOpenForMoreThanThreeMonths": true,
"acceptsDebits": true,
"acceptsCredits": true
},
"accountHolder": {
"registrationNumber": "verified",
"name": "verified"
}
},
"createdAt": "2024-01-15T09:30:00Z",
"updatedAt": "2024-01-15T09:30:00Z"
}

Refuted Account (Name Mismatch)

If the bank confirms the account exists but the holder's name does not match the supplied name, the response surfaces outcome: refuted. The per-field verdicts under verificationResult.accountHolder identify which fields did not match, while remaining checks still report what could be verified.

curl -X POST 'https://api.stitch.money/v2/bank-account-verifications' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-d '{
"bankAccount": {
"bank": "capitec",
"number": "1234567890",
"type": "savings"
},
"accountHolder": {
"type": "individual",
"familyName": "Doe",
"initials": "J",
"identifyingDocument": {
"type": "identity_document",
"number": "9001015009087"
}
}
}'

Example 200 OK response (verificationResult.accountHolder.familyName is refuted and outcome rolls up to refuted):

{
"id": "bav_3RxGl8mTuSqXyZ1abc2DEF",
"bankAccount": {
"bank": "capitec",
"number": "1234567890",
"type": "savings"
},
"accountHolder": {
"type": "individual",
"familyName": "Doe",
"initials": "J",
"identifyingDocument": {
"type": "identity_document",
"country": "ZA",
"number": "9001015009087"
}
},
"verificationResult": {
"outcome": "refuted",
"bankAccount": {
"exists": "verified",
"typeMatch": "verified",
"isOpen": true,
"isOpenForMoreThanThreeMonths": true,
"acceptsDebits": true,
"acceptsCredits": true
},
"accountHolder": {
"identifyingDocument": "verified",
"familyName": "refuted",
"initials": "verified"
}
},
"createdAt": "2024-01-15T09:30:00Z",
"updatedAt": "2024-01-15T09:30:00Z"
}

Refuted Account (Closed)

A more subtle refuted case: every per-field verdict passes — identity, name, initials, account exists, type matches — but the bank reports the account is closed. The API still returns outcome: refuted because a closed account is not safely usable for any downstream operation (payouts, debits, credits). The bankAccount.isOpen === false signal is what drives the override; the per-field verdicts remain verified so callers can still see exactly what matched.

{
"id": "bav_6KkLm3oPqRsT0bYdef5KLM",
"bankAccount": {
"bank": "fnb",
"branchCode": "250655",
"number": "1234567890",
"type": "current"
},
"accountHolder": {
"type": "individual",
"familyName": "Doe",
"initials": "J",
"identifyingDocument": {
"type": "identity_document",
"country": "ZA",
"number": "9001015009087"
}
},
"verificationResult": {
"outcome": "refuted",
"bankAccount": {
"exists": "verified",
"typeMatch": "verified",
"isOpen": false,
"isOpenForMoreThanThreeMonths": false,
"acceptsDebits": false,
"acceptsCredits": false
},
"accountHolder": {
"identifyingDocument": "verified",
"familyName": "verified",
"initials": "verified"
}
},
"createdAt": "2024-01-15T09:30:00Z",
"updatedAt": "2024-01-15T09:30:00Z"
}

Indeterminate Result

When the bank cannot perform the verification — for example because the bank's verification system is temporarily unavailable, or the bank does not support a particular check — the response surfaces outcome: indeterminate. Per-field verdicts that the bank could not evaluate are indeterminate; bank-returned facts that the bank did not provide a value for are null.

{
"id": "bav_4SyHm9nUvTrYzA2cde3FGH",
"bankAccount": {
"bank": "za_albaraka_bank",
"branchCode": "800000",
"number": "5678901234",
"type": "current"
},
"accountHolder": {
"type": "individual",
"familyName": "Doe",
"initials": "J",
"identifyingDocument": {
"type": "identity_document",
"country": "ZA",
"number": "9001015009087"
}
},
"verificationResult": {
"outcome": "indeterminate",
"bankAccount": {
"exists": "indeterminate",
"typeMatch": "indeterminate",
"isOpen": null,
"isOpenForMoreThanThreeMonths": null,
"acceptsDebits": null,
"acceptsCredits": null
},
"accountHolder": {
"identifyingDocument": "indeterminate",
"familyName": "indeterminate",
"initials": "indeterminate"
}
},
"createdAt": "2024-01-15T09:30:00Z",
"updatedAt": "2024-01-15T09:30:00Z"
}

Response Resource

Every successful response is a Bank Account Verification resource:

  • id — Unique identifier for this verification, in the form bav_<22-character-base62>. Use this when contacting support.
  • bankAccount — The bank, branch code, number, and type as resolved by the verifier. Mirrors the request bankAccount.
  • accountHolder — The verified account holder, either an individual or a business. Mirrors the request accountHolder (the per-field verification verdicts live under verificationResult.accountHolder, not here).
  • verificationResult — The verification outcome (see below).
  • createdAt — UTC timestamp of when the verification was performed.
  • updatedAt — Always equal to createdAt. Bank account verifications are immutable; the field is included to align with the standard resource shape.

Verdict values

verificationResult.outcome and every nested verdict (verificationResult.bankAccount.exists, verificationResult.accountHolder.familyName, …) is one of three values:

  • verified — The data provided was successfully verified against the bank's data.
  • refuted — The data provided is not correct. It failed to match the data the verifier holds for the account, or the field itself is invalid or non-existent.
  • indeterminate — Verification could not be performed conclusively for the field. The bank may not support the check, the bank's verification system may be temporarily unavailable, or the caller may not have supplied the input needed to perform the check.

Verdicts are never null. When the bank could not return a verdict, the field is indeterminate.

Bank-returned facts

The booleans under verificationResult.bankAccount (isOpen, isOpenForMoreThanThreeMonths, acceptsDebits, acceptsCredits) are boolean | null. null means the bank did not return a value for that fact — typical when the verification as a whole is indeterminate.

Distinguish carefully between false and null:

  • isOpen: false — the bank actively reported that the account is closed. This is a definitive negative signal.
  • isOpen: null — the bank did not report a status for openness. Treat as unknown, not as "open".

verificationResult.outcome

outcome is the rolled-up decision for the verification, derived from all of the verdicts and the bank-returned facts. The derivation rule is:

  1. refuted — if any verdict under verificationResult.bankAccount or verificationResult.accountHolder is refuted, OR if the bank reports verificationResult.bankAccount.isOpen === false (the account is closed and therefore not safely usable).
  2. indeterminate — otherwise, if any verdict is indeterminate and no verdict is refuted.
  3. verified — otherwise.

The closed-account override (rule 1, second clause) is why a verification can return outcome: refuted even when every per-field verdict is verified. See the closed-account example above.

verificationResult.bankAccount

Per-check verdicts and bank-returned facts about the bank account itself:

  • exists — Whether the account exists at the bank.
  • typeMatch — Whether the account type matches. indeterminate when the caller did not supply bankAccount.type in the request.
  • isOpen — Whether the account is currently open at the bank.
  • isOpenForMoreThanThreeMonths — Whether the account has been open for longer than three months. A common risk signal.
  • acceptsCredits — Whether the account accepts credit payments (i.e. can be paid to). For frozen or closed accounts this is false.
  • acceptsDebits — Whether the account accepts debit payments (i.e. can make payments). For frozen or closed accounts this is false.

verificationResult.accountHolder

Per-field verdicts about the account holder. Only the keys relevant to the holder type are present.

For accountHolder.type === "individual":

  • identifyingDocument — Whether the supplied identity number or passport number matches the account holder on record.
  • familyName — Whether the supplied family name matches the account holder on record.
  • initials — Whether the supplied initials match the account holder on record.

For accountHolder.type === "business":

  • registrationNumber — Whether the supplied business registration number matches the account holder on record.
  • name — Whether the supplied registered business name matches the account holder on record.

Individual-only keys are omitted from a business response (and vice versa) rather than returned as null. The shape mirrors the request: if a field was not relevant to the holder type, it simply isn't there.

Handling Pending Verifications

In some cases, the upstream verification provider may take up to 120 seconds to produce a result, depending on the bank selected. When this happens, the REST API returns 202 Accepted:

{
"status": "pending",
"message": "Verification is in progress. Resubmit the same request to check for results."
}

Resubmit the same request body to poll for the result. Results are stored for up to 48 hours following the initial request, so you will not be billed for subsequent lookups of the same data within this period.

The 202 response may also be returned from the test environment to simulate the random occurrences that may be expected in production.

Error Reference

Status CodeMeaningDescription
200OKVerification completed successfully. The response body contains the verification result resource.
202AcceptedVerification is still in progress. Resubmit the same request to poll for the result.
400Bad RequestThe request body is malformed or fails schema validation (e.g. bankAccount.number is missing, bankAccount.bank is missing).
401UnauthorizedThe access token is missing, expired, or invalid.
403ForbiddenThe access token is missing one or both of the required scopes (client_bankaccountverification, accountholders). The response includes a missingScopes array.
422Unprocessable EntityThe request was valid, but verification could not be completed due to a known business-logic reason. See Verification Errors below.
503Service UnavailableThe verification service is temporarily unavailable, typically due to upstream maintenance.

Verification Errors

A 422 response indicates that the upstream BAVS provider rejected the verification for a known reason. The body has the shape:

{
"title": "Verification Error",
"code": "VERIFICATION_ERROR",
"detail": "The provided account number is not valid at the selected bank.",
"reason": "invalid_account_number"
}

The reason field is one of the following values from the upstream VerificationErrorReason enum:

  • technical_error — A technical failure prevented verification.
  • invalid_account_type — The supplied bankAccount.type is not valid for this bank or account.
  • invalid_branch_number — The supplied branchCode (or universal code derived from bank) is not valid.
  • invalid_account_number — The supplied bankAccount.number is not valid at the selected bank.
  • invalid_account_number_length — The supplied bankAccount.number is the wrong length for this bank.
  • instituition_not_supported — The bank is not supported by the upstream verifier.
  • bond_account_not_allowed — The supplied account is a bond account, which is not supported by the upstream verifier.

Validation Errors

A 400 response indicates that the request body failed gateway-side schema validation before being forwarded to BAVS. The body has the shape:

{
"title": "Invalid Input",
"code": "BAD_USER_INPUT",
"detail": "Invalid request body: bankAccount.number is required"
}

Common causes:

  • bankAccount.number missing or not 6–13 digits.
  • bankAccount.bank missing or not a recognised bank enum value.
  • accountHolder.type missing or not one of individual / business.
  • bankAccount.branchCode is not exactly 6 numeric digits.
  • country is not a valid ISO 3166-1 alpha-2 code.

400 responses may also surface upstream resolver-level validation failures (for example, an invalid ZA identity number, or a malformed business registration number).

Notes and Limitations

  • South Africa only. BAV is currently only available for South African bank accounts. The country field on identifying documents and business registrations is exposed for forward compatibility but defaults to ZA; verification of non-ZA documents is governed by upstream support.
  • bav_<id> is a gateway-minted identifier, not (yet) a queryable resource. Use it for support correlation. A follow-up release will swap it for the persisted upstream id.
  • Polling required for 202. When the upstream provider returns a pending result, resubmit the same request body to retrieve the final outcome.

Validation Differences from GraphQL

The REST surface is deliberately stricter than the equivalent GraphQL mutation in a few places. These tightenings are designed to catch obviously-malformed requests before they reach the upstream provider, reducing wasted calls and producing predictable 400 errors instead of opaque downstream failures.

  • bankAccount.number — REST enforces a 6-character minimum (in addition to the shared 13-character maximum). GraphQL only enforces the maximum.
  • bankAccount.bank — REST requires bank on every request. GraphQL allows branchCode as an alternative to bankId.
  • bankAccount.branchCode — REST enforces exactly 6 numeric digits via ^\d{6}$ when provided. GraphQL accepts 1–6 numeric digits.

All other validations (character sets on names, regex on business registration numbers, country-specific ID number checksums) run upstream and surface as 400 Bad Request with the upstream's specific message when they fail.

Simulating Verification in Test

How test mode is enabled

There is no test-mode flag on the BAVS request body. The verification path is selected by your client token: tokens issued to a test client (whose client_id is prefixed with test-) are automatically routed to the simulator instead of the live verification provider. The REST endpoint shares this routing with the equivalent GraphQL mutation — the token identifies whether the request is a test request, and the upstream service dispatches accordingly. Live-client tokens always reach the live provider; test-client tokens never do.

A consequence of this design:

  • Authenticate with a test-client OAuth token to hit the simulator. Use the same client_bankaccountverification and accountholders scopes as a live request.
  • The request body shape is identical for test and live requests. The values in the table below only steer the simulator's response (verified vs refuted) — they are not sentinel values that opt you into test mode.
  • Test-mode requests are billed at the simulator's tariff (not the live tariff) and never contact the upstream verification provider.

When integrating with your test client, mock information can be specified over the API to simulate verified or refuted results.

To simulate a successful individual account verification, whether for an identity document holder or a passport holder, include the following values in your test request:

ParameterValueIdentifying Document Type
accountHolder.initialsJID Document / Passport
accountHolder.familyNameCleggID Document / Passport
identifyingDocument.number (ID)5306075800082ID Document
identifyingDocument.number (PP)A12345678Passport
bankAccount.numberAny account number ending in 0ID Document / Passport
note

Use either passport or ID document as the identifying document, not both.

To simulate a successful business account verification, include the following values in your test request:

ParameterValue
accountHolder.nameJohnny Clegg Recordings Ltd
accountHolder.registrationNumber1234/567891/23
bankAccount.numberAny account number ending in 0

Varying the inputs from the values above will result in refuted outcomes being returned for the changed fields.

The 202 Accepted (pending) response may be returned from any requests in the test environment. This simulates the random occurrences that may be expected in the live environment. On receiving this response, resubmit your request to receive the final verification result.