API Console - Interactive documentation
POST /api/person

Verify an individual's identity against the FR individual records database. 0-100 score per field + aggregated global score.

database
Database+220M FR individual records (multi-source)
speed
LatencySynchronous real-time response, JSON format
warning
Ambiguous caseInformational signal, does not alter the score

Required headers

HeaderValueRequiredDescription
X-API-Keyvkey-...yesAccount authentication key (otherwise HTTP 401 missing_api_key)
Content-Typeapplication/jsonyesRequest body content type JSON. The body is parsed as JSON regardless of the value.

Body

FieldTypeRequiredDescription
referencestringnoClient identifier returned as-is
person.first_namestringyesFirst name
person.last_namestringyesLast name
person.genderM | FnoGender
person.birth_datestringno*YYYY-MM-DD recommended (common date formats accepted). Invalid date: field ignored and reported as status:"invalid" (invalid_birth_date), HTTP 200.
person.birth_yearstringnoYear of birth (YYYY). Decomposed alternative to birth_date (Trulioo-style); the year is the required discriminator.
person.birth_monthstringnoMonth of birth (MM, 01-12). Optional, alongside birth_year.
person.birth_daystringnoDay of birth (DD, 01-31). Optional, alongside birth_year.
person.address.streetstringno*Street + number
person.address.citystringno*City
person.address.postal_codestringno*5 digits (auto zero-padding)
person.emailstringno*Email (format validation)
person.mobilestringno*Strict FR mobile (06/07). If invalid: field ignored and reported as status:"invalid" (invalid_mobile), HTTP 200.
person.landlinestringno*Strict FR landline. If invalid: field ignored and reported as status:"invalid" (invalid_landline), HTTP 200.
person.phonestringno*FR phone (auto-detect mobile/landline). Returns matches.phone.resolved_as = mobile | landline
name_thresholdnumbernoFuzzy-matching gate for person NAMES (first_name and last_name only): 0.5-1.0 (default 0.90, or the account value). A name scores 0 below this Jaro-Winkler similarity. Lower it to recover approximate names, raise it to be stricter. Out of range -> HTTP 400 invalid_name_threshold.
match_ruleobjectnoOptional boolean rule: {"and":[...]} / {"or":[...]} tree over the fields (+ alias address = street AND city AND postal_code). Returns rule.passed. Malformed -> HTTP 400 invalid_match_rule.
min_scoreintegernoThreshold 1-100 (default 50): a field is satisfied if its score ≥ min_score. Considered with match_rule only. Out of range -> HTTP 400 invalid_min_score.

* At least one discriminating criterion required in addition to first_name + last_name: birth_date, email, mobile, landline, phone, or a full address (address.street + address.city + address.postal_code). Otherwise HTTP 400 insufficient_data.

The email, mobile, landline and phone fields accept the clear-text value OR its MD5 (32 hex) / SHA256 (64 hex) digest, auto-detected. The digest is computed on the canonical form (lowercase email; 10-digit French national phone number). A hashed field returns only match or no_match.

Response 200

FieldTypeDescription
transaction_idstringUnique transaction identifier
referencestring | nullReference provided by the client (passthrough)
scorenumber | nullGlobal score 0-100 (average of fields with status match / partial / no_match). Statuses missing and not_searched are excluded from the calculation. Raw signal, not capped.
confidencenumber | nullGlobal confidence 0-100 in the identification: strength of the identity evidence (discriminating power of the matching fields, which saturates), uniqueness of the target (penalised for homonyms when no unique contact is provided) and coverage. null if no record found. Complements score rather than replacing it: a score of 100 on a homonym yields a low confidence.
confidence_levelenum | nullReadable band of confidence: high (≥70, reliable identification), medium (40-69) or low (<40, to review: little discriminating signal or homonym). null if no record.
ambiguousbooleantrue if several distinct profiles match the provided criteria (homonyms at the same name+address with divergent contacts). Informational signal, does not alter the score: the client must provide more contacts (email, mobile, DOB) to resolve the ambiguity.
matches[field].statusenummatch, partial, no_match, not_searched, missing, invalid
matches[field].scorenumber | nullScore 0-100 for this field (null if missing or not_searched)
timing_msnumberServer-side processing time
ruleobjectPresent only if match_rule is provided: {match_rule, passed, min_score} (evaluated rule + verdict; does not alter matches or score).

Code

curl -X POST https://verify.zecible.fr/api/person \
  -H 'X-API-Key: vkey-...' \
  -H 'Content-Type: application/json' \
  -d '{
    "reference": "tx-001",
    "person": {
      "first_name": "Jean",
      "last_name": "Dupont",
      "birth_date": "1980-05-15"
    }
  }'
const r = await fetch('https://verify.zecible.fr/api/person', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.VERIFY_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    reference: 'tx-001',
    person: { first_name: 'Jean', last_name: 'Dupont', birth_date: '1980-05-15' }
  })
});
const data = await r.json();
console.log(data.score, data.matches);
import requests, os
r = requests.post(
    'https://verify.zecible.fr/api/person',
    headers={'X-API-Key': os.environ['VERIFY_KEY']},
    json={
        'reference': 'tx-001',
        'person': {'first_name': 'Jean', 'last_name': 'Dupont', 'birth_date': '1980-05-15'}
    }
)
print(r.json()['score'])
<?php
$ch = curl_init('https://verify.zecible.fr/api/person');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'X-API-Key: ' . getenv('VERIFY_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'reference' => 'tx-001',
        'person'    => ['first_name' => 'Jean', 'last_name' => 'Dupont', 'birth_date' => '1980-05-15'],
    ]),
]);
$data = json_decode(curl_exec($ch), true);
echo $data['score'];

infoReal request / response pairs, frozen from the demo sandbox (100% fictitious data, no real identity). No API key required.

returned as-is in the responsetag
first name (accents accepted)person
last name (accents accepted)badge
M male / F femalewc
YYYY-MM-DD recommended (e.g. 1980-05-15)cake
year YYYY (alternative to full date)cake
month MM (01-12, optional)cake
day DD (01-31, optional)cake
street + numbersignpost
BAN normalised city labellocation_city
5 digits (zero-pad auto if < 5)markunread_mailbox
RFC 5321 format (single email)alternate_email
FR 10 digits - auto mobile/landline (returns resolved_as)phone_iphone
FR 10 digits strict mobile (06/07)smartphone
FR 10 digits strict landline (01-05, 09)call
Name threshold 0.5-1.0 (default 0.90)match_case
Optional AND/OR rule
Threshold 1-100 (default 50)speed

infoFictional demo data (no real identity).

Response
Submit the form to see the JSON response.
POST /api/business BETA

Verify a FR business (KYB) against the B2B database: SIRET, company name, contacts, executives. 0-100 score per field.

database
Database+55M FR business records
speed
LatencySynchronous real-time response, JSON format
manage_accounts
ExecutivesUp to 10 per request, independent best-match
warning
Ambiguous caseScore capped at 50 if multiple SIRETs

Required headers

HeaderValueRequiredDescription
X-API-Keyvkey-...yesAccount authentication key (otherwise HTTP 401 missing_api_key)
Content-Typeapplication/jsonyesRequest body content type JSON. The body is parsed as JSON regardless of the value.

Body

FieldTypeRequiredDescription
referencestringnoClient identifier returned as-is
business.siretstringno*14 digits (establishment)
business.sirenstringno*9 digits (legal entity)
business.vat_intracomstringnoIntra-EU VAT number (derived from SIREN, verified by key)
business.namestringno*Company name
business.legal_formstringnoLegal form (SAS, SARL, EURL...)
business.nafstringnoNAF / APE code (e.g. 7311Z)
business.address.streetstringnoStreet + number
business.address.citystringnoCity
business.address.postal_codestringno5 digits
business.emailstringnoContact email
business.mobilestringnoStrict FR mobile (06/07). If invalid: field ignored and reported as status:"invalid" (invalid_mobile), HTTP 200.
business.landlinestringnoStrict FR landline. If invalid: field ignored and reported as status:"invalid" (invalid_landline), HTTP 200.
business.phonestringnoFR phone (auto-detect mobile/landline). Returns matches.business.phone.resolved_as = mobile | landline
business.websitestringnoWebsite URL
executives[N].first_namestringnoExecutive or contact first name
executives[N].last_namestringnoExecutive or contact last name
executives[N].rolestringnoRole (president, manager, director, etc.)
name_thresholdnumbernoFuzzy-matching gate for EXECUTIVE names (first_name and last_name of executives[], not the company name): 0.5-1.0 (default 0.90, or the account value). Out of range -> HTTP 400 invalid_name_threshold.

* At least one of: business.siret, business.siren, or business.name + (business.address.city OR business.address.postal_code). executives[]: up to 10 entries per request (otherwise HTTP 400 too_many_executives). Independent best-match scoring for each entry against all contacts of the matched company's SIRET (cap 100 contacts/SIRET). When ambiguous=true, the pool is limited to Level 1 of the pipeline.

The siren, email, mobile, landline and phone fields accept the clear-text value OR its MD5 (32 hex) / SHA256 (64 hex) digest, auto-detected (siren: 9 raw digits). A hashed field returns only match or no_match.

Response 200

FieldTypeDescription
transaction_idstringUnique transaction identifier
referencestring | nullReference provided by the client (passthrough)
scorenumber | nullGlobal score 0-100 (average of fields with status match / partial / no_match). Statuses missing and not_searched are excluded from the calculation. Capped at 50 if ambiguous=true.
ambiguousbooleantrue if several distinct businesses match the provided criteria (establishments of the same group or homonyms at the same name+address with divergent contacts). The client must provide a SIRET or a unique contact (email, phone, website) to resolve the ambiguity.
matches.business.<field>.statusenummatch, partial, no_match, not_searched, missing, invalid
matches.business.<field>.scorenumber | nullField score 0-100 (null if missing or not_searched)
matches.executivesarrayAlways present (empty if no executives in input). Order preserved = input order.
matches.executives[N].<field>objectStatus + score per field for each input executive (independent best-match, same enum as business)
timing_msnumberServer-side processing time

Code

curl -X POST https://verify.zecible.fr/api/business \
  -H 'X-API-Key: vkey-...' \
  -H 'Content-Type: application/json' \
  -d '{
    "reference": "tx-biz-001",
    "business": {
      "siret": "00000000000000",
      "name": "ACME SAS",
      "legal_form": "SAS"
    },
    "executives": [
      { "first_name": "Jean", "last_name": "DUPONT", "role": "président" }
    ]
  }'
const r = await fetch('https://verify.zecible.fr/api/business', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.VERIFY_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    reference: 'tx-biz-001',
    business: { siret: '00000000000000', name: 'ACME SAS' },
    executives: [{ first_name: 'Jean', last_name: 'DUPONT' }]
  })
});
console.log((await r.json()).score);
import requests, os
r = requests.post(
    'https://verify.zecible.fr/api/business',
    headers={'X-API-Key': os.environ['VERIFY_KEY']},
    json={
        'reference': 'tx-biz-001',
        'business': {'siret': '00000000000000', 'name': 'ACME SAS'},
        'executives': [{'first_name': 'Jean', 'last_name': 'DUPONT'}]
    }
)
print(r.json()['score'])
<?php
$ch = curl_init('https://verify.zecible.fr/api/business');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'X-API-Key: ' . getenv('VERIFY_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'reference'  => 'tx-biz-001',
        'business'   => ['siret' => '00000000000000', 'name' => 'ACME SAS'],
        'executives' => [['first_name' => 'Jean', 'last_name' => 'DUPONT']],
    ]),
]);
$data = json_decode(curl_exec($ch), true);
echo $data['score'];

infoReal request / response pairs, frozen from the demo sandbox (100% fictitious data, no real identity). No API key required.

returned as-is in the responsetag
Name threshold 0.5-1.0 (default 0.90)match_case
14 digits (spaces tolerated)fingerprint
9 digits (legal entity)fingerprint
FR + 11 digits (computed from SIREN)payments
company name or trade name (fuzzy)storefront
SAS, SARL, EURL, SA, SCI...gavel
4 digits + 1 letter (e.g. 7311Z)category
e.g. 12 rue de la Paixsignpost
establishment citylocation_city
5 digitsmarkunread_mailbox
contact email (RFC 5321)alternate_email
FR 10 digits (auto mobile/landline)call
full URL (https:// included)language
FR 10 digits strict mobile (06/07)smartphone
FR 10 digits strict landline (01-05, 09)call
first name (accents OK)person
last name (accents OK)badge
president, manager, director (fuzzy)work

infoFictional demo data (no real identity).

Response
Submit the form to see the JSON response.
GET /api/usage

Consumption reporting: request count + breakdown by billable product + pre-tax total.

person
ScopeAPI user of the caller (current key)
date_range
Default periodCurrent month (1st - last day)
euro
CurrencyEUR pre-tax, actual pro-rata per product

Required headers

HeaderValueRequiredDescription
X-API-Keyvkey-...yesAccount authentication key (otherwise HTTP 401 missing_api_key)

Query params

ParamTypeRequiredDescription
startstringnoStart date YYYY-MM-DD (default: first day of current month)
stopstringnoEnd date YYYY-MM-DD (default: last day of current month)

Response 200

FieldTypeDescription
periodobject{ start, stop } - period actually queried
requestsnumberTotal number of requests
succeedednumberRequests with at least one billable product
no_matchnumberVerifications that ran but matched no billable product
errorednumber4xx error requests (not billed)
failednumberLegacy: no_match + errored (unbilled requests)
errors_completeobject{ code: count } of full 4xx errors (global rejects)
errors_partialobject{ code: count } of per-field invalid_* codes (200 responses, not exclusive of states)
total_pretaxnumberPre-tax total EUR for the period
currencystringCurrency code (EUR)
products[]arrayBreakdown by product (key, label, count, cpm_pretax, total_pretax)
timing_msnumberServer processing time in milliseconds

Code

curl 'https://verify.zecible.fr/api/usage?start=2026-05-01&stop=2026-05-31' \
  -H 'X-API-Key: vkey-...'
YYYY-MM-DD - default: first day of current monthevent_available
YYYY-MM-DD - default: last day of current monthevent_busy

infoFictional demo data (no real identity).

Response
Submit the form to see the JSON response.
429 Rate limiting

Global counter per API key, fixed 1-minute window, shared across /api/person, /api/business and /api/usage. Default limit: 60 requests per minute. Overridable per account (contact us for enterprise needs).

Response headers (all responses)

HeaderDescription
X-RateLimit-LimitMaximum quota for the current window (e.g. 60). 0 = account with no limit.
X-RateLimit-RemainingRemaining quota after this request. -1 if the account has no limit.
X-RateLimit-ResetUnix UTC timestamp of the next reset (start of next minute). 0 if account has no limit.
Retry-AfterPresent only on HTTP 429. Number of seconds until the window resets.

HTTP 429 response (quota exceeded)

FieldDescription
error"rate_limit_exceeded"
messageHuman-readable message with the number of seconds until the next reset.
limitAccount quota.
reset_unixUnix UTC timestamp of the next reset.

Recommended strategy

  • Read X-RateLimit-Remaining on every response to anticipate throttling client-side.
  • On HTTP 429, respect Retry-After before retrying (exponential backoff unnecessary: the window resets at the exact minute).
  • For volumes > 60 req/min, request a higher quota: contact us.
  • The counter starts on the first request of each UTC minute, regardless of the endpoint called.

Example 429 response

# HTTP 429 Too Many Requests
# Headers
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1779994620
Retry-After: 42

# Body
{
  "error":      "rate_limit_exceeded",
  "message":    "Limite de 60 requetes par minute atteinte. Reessayer dans 42s.",
  "limit":      60,
  "reset_unix": 1779994620
}
4XX Error codes

Exhaustive list of error codes returned by the API. Uniform response format: {"error": "code_machine", "message": "human explanation"}. Always JSON, UTF-8 charset.

Authentication (401)

CodeEndpoint(s)Cause
missing_api_keyallHeader X-API-Key absent or empty.
invalid_api_keyallUnknown or revoked key. Check the exact value (case-sensitive) and that it belongs to an active account.
account_disabledallAccount deleted or disabled on the Zecible side. Contact support for reactivation.
ip_not_allowedallCalling IP not in the account whitelist. The message contains the rejected IP. Add the IP via support.

HTTP method (405)

CodeEndpoint(s)Cause
method_not_allowedallPOST expected on /api/person and /api/business. GET expected on /api/usage.

Body validation (400)

CodeEndpoint(s)Cause
invalid_bodyperson, businessBody absent, malformed JSON, or non-object root.
invalid_referenceperson, businessThe reference field must be a string (passthrough) of at most 256 characters.
missing_required_fieldspersonperson.first_name and person.last_name are required.
missing_required_fieldsbusinessAt least one of business.siret, business.siren, or business.name + business.address.city OR business.address.postal_code.
insufficient_datapersonIdentity provided but no discriminating criterion (birth_date, phone, email or full address) to identify a target.
insufficient_databusinessNo usable combination (SIRET / SIREN / name + address / name + contact).
too_many_executivesbusinessexecutives[] contains more than 10 entries. Limit the request to 10 executives or chain multiple requests.
invalid_match_rulepersonThe match_rule field is malformed (nested {and|or:[...]} tree over known person fields expected).
invalid_min_scorepersonThe min_score field must be an integer between 1 and 100.
invalid_name_thresholdperson/businessThe name_threshold field must be a number between 0.5 and 1.0.

Per-field validation (HTTP 200, status invalid)

These codes are not HTTP errors: the request returns 200. The invalid field is ignored (excluded from the search) and reported in matches.<field>.status = invalid, with the code echoed in error. Edge case: if an invalid siret/siren leaves no usable identifier, the request is instead rejected with missing_required_fields / insufficient_data (400).

CodeEndpoint(s)Cause
invalid_birth_datepersonperson.birth_date is not a valid date (YYYY-MM-DD recommended).
invalid_mobileperson, businessThe mobile field must be a FR mobile number (10 digits starting with 06 or 07).
invalid_landlineperson, businessThe landline field must be a FR landline number (not a mobile).
invalid_phoneperson, businessThe phone field must be a valid FR phone number (10 digits).
invalid_emailperson, businessThe email field must be a valid email address.
invalid_genderpersonThe gender field must be M or F.
invalid_siretbusinessThe siret field must be a valid SIRET (14 digits, Luhn checksum).
invalid_sirenbusinessThe siren field must be a valid SIREN (9 digits, Luhn checksum).

Payload size (413)

CodeEndpoint(s)Cause
body_too_largeperson, businessRequest body exceeds 32 KB. Reduce the JSON size (e.g. limit the number of executives, shorten text fields).

Quotas (429)

CodeEndpoint(s)Cause
rate_limit_exceededallPer-minute quota reached. See the Rate limiting section for retry strategy and X-RateLimit-* headers.

Credits / budget (402)

CodeEndpoint(s)Cause
quota_exceededperson, businessPeriod credit cap reached (monthly budget or exhaustible account wallet). Headers X-Credit-Limit / X-Credit-Used / X-Credit-Remaining (EUR pre-tax) indicate the most constraining budget. Contact support to raise the cap.

Response format

# HTTP 4xx
# Content-Type: application/json; charset=UTF-8

{
  "error":   "invalid_birth_date",
  "message": "person.birth_date doit etre au format ISO 8601 (YYYY-MM-DD)."
}

The error field is stable and intended for programmatic handling. The message field is intended for display and may change between versions.