CheckDigit

API docs

A JSON API for checking the format and check digits of IBANs, BICs, EU VAT numbers, GTIN/EAN/UPC codes and ISBNs. Base URL: https://checkdigit

This is a format check, not a compliance check. A valid format or checksum does not prove that a bank account, VAT registration, bank (BIC) or product exists, is active, or belongs to anyone in particular. Don't use CheckDigit as your only KYC, AML, VAT-compliance or payment-verification control. VAT registration status comes from the EU's VIES service, which CheckDigit does not query.

Endpoints

Method and pathWhat it doesKey
GET /v1/iban/{iban}IBAN: country, length, BBAN layout, mod-97 check digitsoptional
GET /v1/bic/{bic}BIC / SWIFT code: structure and country (no bank directory)optional
GET /v1/vat/{number}EU VAT number: format and check digit, offline (no VIES)optional
GET /v1/gtin/{code}EAN-8, UPC-A, EAN-13, GTIN-14 and ISBN-10: check digit and conversionsoptional
GET /v1/isbn/{code}ISBN-10 or ISBN-13: check digit and ISBN-10/13 conversionoptional
GET /v1/detect/{code}Detects the type, then validates itoptional
POST /v1/{type}Same as the GET endpoints above (iban, bic, vat, gtin, isbn, detect) with the code in a JSON body {"code": "..."} instead of the URL. Recommended for IBANs and VAT numbers.optional
POST /v1/batchUp to 500 mixed codes in one requestPro key
GET /v1/metaVersion, supported countries and formats, limitsoptional

Send the code in the URL path (GET) or in a JSON body (POST, see below). Spaces, hyphens and dots are ignored and letters are upper-cased, so de89 3704 0044 0532 0130 00 works. A leading label is ignored too: IBAN:, VAT:, BIC:, ISBN / ISBN-13: (on /v1/detect and on the matching endpoint). Percent-encode codes in URLs (spaces as %20). Codes can be at most 64 characters.

Every validation returns HTTP 200 with "valid": true or "valid": false. When it is false, reason explains why, for example "checksum fails, expected 7". Other HTTP status codes mean the request itself could not be processed (see Errors).

POST/v1/{type}  (iban, bic, vat, gtin, isbn, detect)

Every single-code endpoint also accepts POST with the code in a JSON body. The response is the same as for GET. Use POST for IBANs, VAT numbers and anything else you'd rather not have in URLs: a URL path can end up in request logs, proxies and browser history, a request body normally doesn't. No API key is needed; the limits, quotas and 32 KB body cap are the same as for GET. The free checker on the home page uses POST /v1/detect.

curl -X POST https://checkdigit/v1/iban \
  -H "Content-Type: application/json" \
  -d '{"code": "DE89 3704 0044 0532 0130 00"}'

Errors: no or empty code gives 400 missing_code, a body that isn't JSON gives 400 invalid_json, and a code in the URL as well (POST /v1/iban/DE89...) gives 400 code_in_url.

GET/v1/iban/{iban}

Checks the country code against the SWIFT IBAN Registry (89 countries), the length and BBAN layout for that country, and the ISO 13616 mod-97 check digits. Returns valid, country, bban, check_digits, formatted plus the bank code and SEPA flag. National check digits inside the BBAN are not verified in this version.

curl https://checkdigit/v1/iban/DE89370400440532013000
{
  "type": "iban",
  "input": "DE89370400440532013000",
  "normalized": "DE89370400440532013000",
  "valid": true,
  "country": "DE",
  "country_name": "Germany",
  "sepa": true,
  "check_digits": "89",
  "bban": "370400440532013000",
  "bank_code": "37040044",
  "formatted": "DE89 3704 0044 0532 0130 00",
  "electronic": "DE89370400440532013000"
}

An invalid IBAN:

{
  "type": "iban",
  "input": "DE89370400440532013001",
  "normalized": "DE89370400440532013001",
  "valid": false,
  "reason": "checksum fails, expected check digits 62 (mod 97)",
  "country": "DE",
  "country_name": "Germany",
  "sepa": true,
  "check_digits": "89",
  "bban": "370400440532013001",
  "bank_code": "37040044",
  "formatted": "DE89 3704 0044 0532 0130 01",
  "expected_check_digits": "62"
}

GET/v1/bic/{bic}

Checks the ISO 9362 structure (4-character party prefix, ISO 3166 country code, 2-character location code, optional 3-character branch code). It flags test BICs (location code ending in 0). It does not look up banks or confirm that a BIC is registered.

curl https://checkdigit/v1/bic/DEUTDEFF500
{
  "type": "bic",
  "input": "DEUTDEFF500",
  "normalized": "DEUTDEFF500",
  "valid": true,
  "bank_code": "DEUT",
  "country": "DE",
  "country_name": "Germany",
  "location_code": "FF",
  "branch_code": "500",
  "primary_office": false,
  "test_bic": false,
  "bic8": "DEUTDEFF",
  "bic11": "DEUTDEFF500"
}

GET/v1/vat/{number}

Include the country prefix: AT, BE, BG, CY, CZ, DE, DK, EE, EL (Greece; GR is accepted and mapped to EL), ES, FI, FR, HR, HU, IE, IT, LT, LU, LV, MT, NL, PL, PT, RO, SE, SI, SK, plus XI (Northern Ireland). The check runs offline using each country's published check-digit rule. VIES is not queried, so the response says nothing about whether the number is registered or active ("vies_checked": false).

curl https://checkdigit/v1/vat/ATU13585627
{
  "type": "vat",
  "input": "ATU13585627",
  "normalized": "ATU13585627",
  "valid": true,
  "country": "AT",
  "country_name": "Austria",
  "number": "U13585627",
  "checksum": "passed",
  "vies_checked": false
}
{
  "type": "vat",
  "input": "DE136695977",
  "normalized": "DE136695977",
  "valid": false,
  "reason": "checksum fails, expected 6",
  "country": "DE",
  "country_name": "Germany",
  "number": "136695977",
  "expected_check": "6"
}

GET/v1/gtin/{code}

EAN-8, UPC-A (12 digits), EAN-13 and GTIN-14 with the GS1 mod-10 check digit. A 10-character ISBN-10 is accepted too. The response includes the GTIN-14 form, UPC-A to EAN-13 conversion, the ISBN-10/ISBN-13 forms for 978/979 codes, and a note for special GS1 prefixes (for example restricted-circulation or coupon ranges). UPC-E (the 8-digit zero-suppressed UPC) is not supported in this version: an 8-digit code is always checked as EAN-8.

curl https://checkdigit/v1/gtin/036000291452
{
  "type": "gtin",
  "input": "036000291452",
  "normalized": "036000291452",
  "valid": true,
  "format": "UPC-A",
  "check_digit": "2",
  "gtin14": "00036000291452",
  "ean13": "0036000291452",
  "prefix_note": "GS1 US (UPC-compatible, prefix 001-019)"
}
{
  "type": "gtin",
  "input": "4006381333932",
  "normalized": "4006381333932",
  "valid": false,
  "reason": "checksum fails, expected 1",
  "format": "EAN-13",
  "check_digit": "2",
  "expected_check_digit": "1"
}

GET/v1/isbn/{code}

ISBN-10 (mod 11, check character 0-9 or X) or ISBN-13 (978/979 prefix, mod 10). Converts ISBN-10 to ISBN-13 and back (979 ISBNs have no ISBN-10 form). Hyphens and an ISBN prefix are ignored. Hyphenation into group/publisher parts is not provided.

curl https://checkdigit/v1/isbn/0-306-40615-2
{
  "type": "isbn",
  "input": "0-306-40615-2",
  "normalized": "0306406152",
  "valid": true,
  "format": "ISBN-10",
  "check_digit": "2",
  "isbn10": "0306406152",
  "isbn13": "9780306406157",
  "gtin14": "09780306406157"
}

GET/v1/detect/{code}

Works out which kind of code it is from its shape, runs the matching validators, and returns the best match. detected_as is the type used and candidates lists every type the shape could fit. If none of them validates, the type where the code had the right shape (only the checksum or a content rule failed) is reported, for example FR40303265046 is reported as a French VAT number whose SIREN fails its Luhn check, not as a too-short French IBAN. The free checker on the home page uses the POST form of this endpoint.

curl https://checkdigit/v1/detect/9780306406157
{
  "type": "isbn",
  "input": "9780306406157",
  "normalized": "9780306406157",
  "valid": true,
  "format": "ISBN-13",
  "check_digit": "7",
  "isbn13": "9780306406157",
  "isbn10": "0306406152",
  "gtin14": "09780306406157",
  "detected_as": "isbn",
  "candidates": [
    "isbn",
    "gtin"
  ]
}
{
  "type": "vat",
  "input": "FR40303265046",
  "normalized": "FR40303265046",
  "valid": false,
  "reason": "SIREN part fails its Luhn checksum",
  "country": "FR",
  "country_name": "France",
  "number": "40303265046",
  "detected_as": "vat",
  "candidates": [
    "iban",
    "vat"
  ]
}

POST/v1/batch

Validate up to 500 codes in one request, for example when cleaning a product feed. Requires a Pro API key. Each code counts as one call against the monthly quota. Items are either strings (auto-detected) or objects with a type of auto, iban, bic, vat, gtin or isbn. Results come back in the same order. The body can be at most 32 KB.

curl -X POST https://checkdigit/v1/batch \
  -H "Authorization: Bearer cd_live_..." \
  -H "Content-Type: application/json" \
  -d '{"codes": ["DE89370400440532013000", {"code": "4006381333931", "type": "gtin"}, "FR40303265046"]}'
{
  "count": 3,
  "valid_count": 2,
  "invalid_count": 1,
  "results": [
    {
      "index": 0,
      "type": "iban",
      "input": "DE89370400440532013000",
      "valid": true,
      "...": "(same fields as the single endpoints)"
    },
    {
      "index": 1,
      "type": "gtin",
      "input": "4006381333931",
      "valid": true,
      "...": "(same fields as the single endpoints)"
    },
    {
      "index": 2,
      "type": "vat",
      "input": "FR40303265046",
      "valid": false,
      "reason": "SIREN part fails its Luhn checksum",
      "...": "(same fields as the single endpoints)"
    }
  ]
}

More than 500 codes gives 400 batch_too_large. A body over 32 KB gives 413 payload_too_large.

GET/v1/meta

Service version and build (git commit), supported IBAN and VAT countries, formats and current limits. It doesn't count against your quota.

curl https://checkdigit/v1/meta

API keys and limits

Send a key as Authorization: Bearer cd_live_... or X-Api-Key: cd_live_.... Requests without a key use the anonymous tier. A key that isn't recognised gives 401 invalid_api_key; it never silently falls back to the anonymous tier.

TierMonthly callsRate limitBatch
Anonymous (no key)no monthly allowance; shared daily capacity20 requests/min per IPno
Free key1,000300 requests/min per keyno
Pro key50,000300 requests/min per keyyes, up to 500 codes per request

Monthly quotas reset at 00:00 UTC on the 1st. Responses to keyed requests carry X-CheckDigit-Plan, X-Quota-Limit and X-Quota-Remaining. A global daily capacity limit protects the service; if it's reached, requests get 429 daily_capacity_reached until 00:00 UTC, and part of the capacity is kept for key holders.

Getting a key: API keys are not self-serve yet and the Pro plan is not on sale. During this preview, keys are issued by the operator on request; the anonymous tier and the POST endpoints need no key.

Errors

Errors use the shape {"error": {"code": "...", "message": "..."}}.

StatuscodeWhen
400missing_code, input_too_long, bad_encoding, code_in_urlNo code in the path or body, code over 64 characters, broken percent-encoding, or a POST with the code in the URL
400invalid_json, invalid_batch, empty_batch, batch_too_largeBody is not JSON, or batch problems (no codes array, empty, more than 500 codes)
401invalid_api_key, api_key_requiredUnknown key, or batch without a key
403paid_plan_requiredBatch with a free key
404 / 405not_found, method_not_allowedUnknown path or wrong method
413payload_too_largePOST body over 32 KB (32,768 bytes)
429rate_limitedPer-minute limit reached (Retry-After: 60), including too many new or unrecognised API keys from one IP
429monthly_quota_exceededKey's monthly calls used up, or a batch is larger than what's left
429daily_capacity_reachedGlobal daily capacity reached (resets 00:00 UTC)
503storage_unavailableKey lookup temporarily unavailable

Privacy

Codes are checked in memory and returned only to you. CheckDigit does not store or log them; it keeps request counts only (per key per month, and a global daily total). CheckDigit makes no outbound requests. GET puts the code in the URL, which HTTP clients, proxies, CDN request logs and browsers may record; for IBANs, VAT numbers or anything else sensitive, use the POST form (POST /v1/iban, /v1/vat, /v1/detect and so on, with {"code": "..."}). It's free and needs no API key.

Sources