Verify an individual's identity against the FR individual records database. 0-100 score per field + aggregated global score.
Required headers
| Header | Value | Required | Description |
|---|---|---|---|
| X-API-Key | vkey-... | yes | Account authentication key (otherwise HTTP 401 missing_api_key) |
| Content-Type | application/json | yes | Request body content type JSON. The body is parsed as JSON regardless of the value. |
Body
| Field | Type | Required | Description |
|---|---|---|---|
| reference | string | no | Client identifier returned as-is |
| person.first_name | string | yes | First name |
| person.last_name | string | yes | Last name |
| person.gender | M | F | no | Gender |
| person.birth_date | string | no* | Strict ISO 8601 format YYYY-MM-DD (otherwise HTTP 400 invalid_birth_date) |
| person.address.street | string | no* | Street + number |
| person.address.city | string | no* | City |
| person.address.postal_code | string | no* | 5 digits (auto zero-padding) |
| person.email | string | no* | Email (format validation) |
| person.mobile | string | no* | Strict FR mobile (otherwise HTTP 400 invalid_mobile) |
| person.landline | string | no* | Strict FR landline (otherwise HTTP 400 invalid_landline) |
| person.phone | string | no* | FR phone (auto-detect mobile/landline). Returns matches.phone.resolved_as = mobile | landline |
| match_rule | object | no | Optional 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_score | integer | no | Threshold 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
| Field | Type | Description |
|---|---|---|
| transaction_id | string | Unique transaction identifier |
| reference | string | null | Reference provided by the client (passthrough) |
| score | number | null | Global 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. |
| ambiguous | boolean | true if several distinct profiles match the provided criteria (homonyms at the same name+address with divergent contacts). The client must provide more contacts (email, mobile, DOB) to resolve the ambiguity. |
| matches[field].status | enum | match, partial, no_match, not_searched, missing |
| matches[field].score | number | null | Score 0-100 for this field (null if missing or not_searched) |
| timing_ms | number | Server-side processing time |
| rule | object | Present only if match_rule is provided: {passed, min_score} (rule verdict; does not alter matches or score). |
Example
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'];
Verify a FR business (KYB) against the B2B database: SIRET, company name, contacts, executives. 0-100 score per field.
Required headers
| Header | Value | Required | Description |
|---|---|---|---|
| X-API-Key | vkey-... | yes | Account authentication key (otherwise HTTP 401 missing_api_key) |
| Content-Type | application/json | yes | Request body content type JSON. The body is parsed as JSON regardless of the value. |
Body
| Field | Type | Required | Description |
|---|---|---|---|
| reference | string | no | Client identifier returned as-is |
| business.siret | string | no* | 14 digits (establishment) |
| business.siren | string | no* | 9 digits (legal entity) |
| business.vat_intracom | string | no | Intra-EU VAT number (derived from SIREN, verified by key) |
| business.name | string | no* | Company name |
| business.legal_form | string | no | Legal form (SAS, SARL, EURL...) |
| business.naf | string | no | NAF / APE code (e.g. 7311Z) |
| business.address.street | string | no | Street + number |
| business.address.city | string | no | City |
| business.address.postal_code | string | no | 5 digits |
| business.email | string | no | Contact email |
| business.mobile | string | no | Strict FR mobile (otherwise HTTP 400 invalid_mobile) |
| business.landline | string | no | Strict FR landline (otherwise HTTP 400 invalid_landline) |
| business.phone | string | no | FR phone (auto-detect mobile/landline). Returns matches.business.phone.resolved_as = mobile | landline |
| business.website | string | no | Website URL |
| executives[N].first_name | string | no | Executive or contact first name |
| executives[N].last_name | string | no | Executive or contact last name |
| executives[N].role | string | no | Role (president, manager, director, etc.) |
* 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
| Field | Type | Description |
|---|---|---|
| transaction_id | string | Unique transaction identifier |
| reference | string | null | Reference provided by the client (passthrough) |
| score | number | null | Global 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. |
| ambiguous | boolean | true 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>.status | enum | match, partial, no_match, not_searched, missing |
| matches.business.<field>.score | number | null | Field score 0-100 (null if missing or not_searched) |
| matches.executives | array | Always present (empty if no executives in input). Order preserved = input order. |
| matches.executives[N].<field> | object | Status + score per field for each input executive (independent best-match, same enum as business) |
| timing_ms | number | Server-side processing time |
Example
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'];
Consumption reporting: request count + breakdown by billable product + pre-tax total.
Required headers
| Header | Value | Required | Description |
|---|---|---|---|
| X-API-Key | vkey-... | yes | Account authentication key (otherwise HTTP 401 missing_api_key) |
Query params
| Param | Type | Required | Description |
|---|---|---|---|
| start | string | no | Start date YYYY-MM-DD (default: first day of current month) |
| stop | string | no | End date YYYY-MM-DD (default: last day of current month) |
Response 200
| Field | Type | Description |
|---|---|---|
| period | object | { start, stop } - period actually queried |
| requests | number | Total number of requests |
| succeeded | number | Requests with at least one billable product |
| failed | number | Requests with no billable product |
| total_pretax | number | Pre-tax total EUR for the period |
| currency | string | Currency code (EUR) |
| products[] | array | Breakdown by product (key, label, count, cpm_pretax, total_pretax) |
Example
curl 'https://verify.zecible.fr/api/usage?start=2026-05-01&stop=2026-05-31' \ -H 'X-API-Key: vkey-...'
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)
| Header | Description |
|---|---|
| X-RateLimit-Limit | Maximum quota for the current window (e.g. 60). 0 = account with no limit. |
| X-RateLimit-Remaining | Remaining quota after this request. -1 if the account has no limit. |
| X-RateLimit-Reset | Unix UTC timestamp of the next reset (start of next minute). 0 if account has no limit. |
| Retry-After | Present only on HTTP 429. Number of seconds until the window resets. |
HTTP 429 response (quota exceeded)
| Field | Description |
|---|---|
| error | "rate_limit_exceeded" |
| message | Human-readable message with the number of seconds until the next reset. |
| limit | Account quota. |
| reset_unix | Unix UTC timestamp of the next reset. |
Recommended strategy
- Read
X-RateLimit-Remainingon every response to anticipate throttling client-side. - On HTTP 429, respect
Retry-Afterbefore 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 }
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)
| Code | Endpoint(s) | Cause |
|---|---|---|
| missing_api_key | all | Header X-API-Key absent or empty. |
| invalid_api_key | all | Unknown or revoked key. Check the exact value (case-sensitive) and that it belongs to an active account. |
| account_disabled | all | Account deleted or disabled on the Zecible side. Contact support for reactivation. |
| ip_not_allowed | all | Calling IP not in the account whitelist. The message contains the rejected IP. Add the IP via support. |
HTTP method (405)
| Code | Endpoint(s) | Cause |
|---|---|---|
| method_not_allowed | all | POST expected on /api/person and /api/business. GET expected on /api/usage. |
Body validation (400)
| Code | Endpoint(s) | Cause |
|---|---|---|
| invalid_body | person, business | Body absent, malformed JSON, or non-object root. |
| invalid_reference | person, business | The reference field exceeds 256 characters. Shorten the client passthrough. |
| missing_required_fields | person | person.first_name and person.last_name are required. |
| missing_required_fields | business | At least one of business.siret, business.siren, or business.name + business.address.city OR business.address.postal_code. |
| insufficient_data | person | Identity provided but no discriminating criterion (birth_date, phone, email or full address) to identify a target. |
| insufficient_data | business | No usable combination (SIRET / SIREN / name + address / name + contact). |
| invalid_birth_date | person | person.birth_date is not in strict ISO 8601 format YYYY-MM-DD. |
| invalid_mobile | person, business | The mobile field must be a FR mobile number (10 digits starting with 06 or 07). |
| invalid_landline | person, business | The landline field must be a FR landline number (not a mobile). |
| invalid_phone | person, business | The phone field must be a valid FR phone number (10 digits). |
| invalid_email | person, business | The email field must be a valid email address. |
| invalid_gender | person | The gender field must be M or F. |
| invalid_siret | business | The siret field must be a valid SIRET (14 digits, Luhn checksum). |
| invalid_siren | business | The siren field must be a valid SIREN (9 digits, Luhn checksum). |
| too_many_executives | business | executives[] contains more than 10 entries. Limit the request to 10 executives or chain multiple requests. |
Payload size (413)
| Code | Endpoint(s) | Cause |
|---|---|---|
| body_too_large | person, business | Request body exceeds 32 KB. Reduce the JSON size (e.g. limit the number of executives, shorten text fields). |
Quotas (429)
| Code | Endpoint(s) | Cause |
|---|---|---|
| rate_limit_exceeded | all | Per-minute quota reached. See the Rate limiting section for retry strategy and X-RateLimit-* headers. |
Credits / budget (402)
| Code | Endpoint(s) | Cause |
|---|---|---|
| quota_exceeded | person, business | Period 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.