Six étapes, et vous avez terminé : créez l'application, prenez une clé de production, créez une vérification, redirigez la personne, recevez le webhook, vérifiez le résultat.
Toutes les routes attendent votre clé dans l'en-tête :Authorization: Bearer fkyc_live_…
Un seul environnement : la production. Les clés fkyc_live_…ne voient que les vérifications de votre application.
curl -X POST https://kyc.fedtopup.com/api/v1/verifications \
-H "Authorization: Bearer $FEDKYC_API_KEY" \
-H "Idempotency-Key: commande-4821" \
-H "Content-Type: application/json" \
-d '{
"external_user_id": "customer_98734",
"required_level": "standard",
"country": "HT",
"document_types": ["national_id", "driver_license", "passport"],
"return_url": "https://votre-site.com/kyc/retour"
}'Idempotency-Key est obligatoire. Deux appels avec la même clé rendent la même vérification : un double clic, une reprise après coupure réseau, un rejeu de file — rien ne crée de doublon.
external_user_id est votre identifiant. Il peut être opaque : nous n'avons besoin ni de l'e-mail, ni du nom, ni de quoi que ce soit d'autre. Et « 123 » chez vous n'a aucun rapport avec « 123 » chez une autre application.
La réponse porte verification_url. Envoyez-y la personne. Tout s'y passe : la caméra, le document, le visage. Votre site ne manipule jamais ces images.
return_url n'est pas une preuve. N'importe qui peut ouvrir cette adresse. La preuve, c'est le webhook signé, ou un appel àGET /v1/verifications/{id}.| created | Créée, la personne n'a pas encore ouvert le lien. |
| started | Le parcours est ouvert. |
| document_submitted | Le document est déposé. |
| processing | Analyse en cours. |
| retry_required | Les preuves ne suffisent pas. La personne peut recommencer. |
| verified | Identité vérifiée. |
| rejected | Refusée. |
| unable_to_verify | Tentatives épuisées sans conclusion. |
| expired | Le lien a expiré. |
| cancelled | Annulée par votre application. |
| technical_error | Incident de notre côté. |
Il n'existe aucun statut d'attente d'approbation humaine. Un résultat incertain devient retry_required, jamais verified.
Déclarez vos points de réception dans le portail. Chaque envoi porte :
FedKYC-Signature: t=1789962474,v1=9f2a…c1
FedKYC-Timestamp: 1789962474
FedKYC-Event-Id: fkyc_evt_3K8…
FedKYC-Event-Type: verification.verifiedLa signature est HMAC-SHA256(secret, `$${t}.$${corpsBrut}`), en hexadécimal. Vérifiez-la sur le corps brut, avant tout parsing JSON — un JSON.parse suivi d'un JSON.stringify ne redonne pas les mêmes octets.
Node.js
import crypto from "node:crypto";
export function verifier(corpsBrut, entete, secret) {
const p = Object.fromEntries(entete.split(",").map(x => x.split("=")));
const t = Number(p.t);
if (!Number.isFinite(t)) return false;
if (Math.abs(Date.now() / 1000 - t) > 300) return false; // anti-rejeu
const attendu = crypto.createHmac("sha256", secret)
.update(`${t}.${corpsBrut}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(attendu), Buffer.from(p.v1));
}Python
import hmac, hashlib, time
def verifier(corps_brut: bytes, entete: str, secret: str) -> bool:
p = dict(x.split("=", 1) for x in entete.split(","))
t = int(p["t"])
if abs(time.time() - t) > 300:
return False
attendu = hmac.new(secret.encode(),
f"{t}.".encode() + corps_brut,
hashlib.sha256).hexdigest()
return hmac.compare_digest(attendu, p["v1"])PHP
function verifier(string $corpsBrut, string $entete, string $secret): bool {
parse_str(str_replace(',', '&', $entete), $p);
$t = (int) ($p['t'] ?? 0);
if (abs(time() - $t) > 300) return false;
$attendu = hash_hmac('sha256', $t . '.' . $corpsBrut, $secret);
return hash_equals($attendu, $p['v1'] ?? '');
}FedKYC-Event-Id est unique : gardez-le et ignorez un identifiant déjà traité. Un webhook peut arriver deux fois — coupure réseau, reprise automatique, relance manuelle depuis votre portail.
En cas d'échec, nous reprenons avec un intervalle qui double : 30 s, 1, 2, 4, 8, 16, 32 min, puis une heure. Au bout de huit tentatives, la livraison part en lettre morte et vous pouvez la relancer à la main.
POST /v1/verifications/{id}/proof rend un JWT signé en EdDSA (Ed25519). Vous le vérifiez chez vous avec le JWKS public, sans nous appeler :https://kyc.fedtopup.com/.well-known/jwks.json
Par défaut, ce jeton ne contient aucune donnée personnelle : ni nom, ni date de naissance, ni numéro de document. Seulement le statut, le niveau, le pays, le type de document et les dates.
Toutes les erreurs ont la même forme. Testez le code, jamais le message : le code est stable, le message peut être reformulé.
{ "error": { "code": "RATE_LIMITED",
"message": "Trop d'appels. Réessayez dans un instant.",
"request_id": "req_8f2c…" } }| INVALID_API_KEY | 401 | Clé inconnue, révoquée, ou utilisée dans le mauvais environnement. |
| INSUFFICIENT_SCOPE | 403 | La clé ne porte pas le scope requis. |
| LIVE_NOT_ENABLED | 403 | L'accès production n'est pas encore accordé à l'application. |
| IDEMPOTENCY_KEY_REQUIRED | 400 | L'en-tête Idempotency-Key manque sur la création. |
| INVALID_RETURN_URL | 400 | return_url ne correspond à aucune URL déclarée. |
| VERIFICATION_NOT_FOUND | 404 | Vérification inconnue pour cette application. |
| INVALID_STATE | 409 | L'état actuel n'autorise pas cette opération. |
| MAX_ATTEMPTS_REACHED | 409 | Le plafond de tentatives est atteint. |
| VERIFICATION_EXPIRED | 410 | Le lien de vérification a expiré. |
| RATE_LIMITED | 429 | Trop d'appels sur cette fenêtre. |
| API_DISABLED | 503 | L'API est momentanément fermée. |
request_id figure aussi dans l'en-têtex-request-id et dans vos journaux d'appels : c'est avec lui qu'on retrouve un appel précis.
FedKYC ne propose pas de bac à sable : toute vérification est réelle et se fait avec une clé fkyc_live_…. Aucune donnée simulée n'existe.