Reconnaissance régulateur
La Doctrine CREPMF 2026 reconnaît la portabilité du KYC signé cryptographiquement comme équivalent à un onboarding traditionnel. Voir la page Conformité pour le détail réglementaire.
Voir /compliance#crepmfUne attestation KYC Inopay est un objet JSON signé Ed25519. Elle est vérifiable hors-ligne par n'importe quel intégrateur, sans appel à un service tiers. Reconnue par la Doctrine CREPMF 2026 comme équivalent à un onboarding traditionnel.
Champs requis et types. La canonisation suit RFC 8785 (JSON Canonicalization Scheme) avant signature Ed25519.
{
"$schema": "https://schemas.getinopay.com/kyc-attestation/v1.json",
"type": "object",
"required": ["version", "subject_id", "level", "issued_at",
"expires_at", "issuer", "verifications",
"signature", "key_id"],
"properties": {
"version": { "type": "string", "const": "1.0" },
"subject_id": { "type": "string", "description": "Pseudonymous subject id" },
"level": { "type": "string", "enum": ["KYC1", "KYC2", "KYC3"] },
"issued_at": { "type": "string", "format": "date-time" },
"expires_at": { "type": "string", "format": "date-time" },
"issuer": { "type": "string", "const": "inopay" },
"verifications": {
"type": "array",
"items": {
"type": "object",
"properties": {
"type": { "enum": ["id_doc", "address", "selfie",
"liveness", "source_of_funds", "pep"] },
"verified_at":{ "type": "string", "format": "date-time" },
"method": { "type": "string" }
}
}
},
"jurisdictions": { "type": "array", "items": { "enum": ["UEMOA", "CEMAC", "GHANA"] } },
"signature": { "type": "string", "description": "Ed25519 signature, base64url" },
"key_id": { "type": "string", "description": "e.g. staging-2026-04" }
}
}Attestation tier_2 émise pour la zone UEMOA. La clé publique d'émission est exposée via /.well-known/inopay.
{
"version": "1.0",
"subject_id": "ino_sub_8a2f9c1d",
"level": "KYC2",
"issued_at": "2026-04-25T08:00:00Z",
"expires_at": "2027-04-25T08:00:00Z",
"issuer": "inopay",
"verifications": [
{ "type": "id_doc", "verified_at": "2026-04-25T07:58:11Z", "method": "OCR+MRZ" },
{ "type": "selfie", "verified_at": "2026-04-25T07:58:32Z", "method": "passive-liveness" },
{ "type": "address", "verified_at": "2026-04-25T07:59:05Z", "method": "utility-bill" }
],
"jurisdictions": ["UEMOA"],
"signature": "MEUCIQDk9HVx...redacted...base64url...QqL2",
"key_id": "staging-2026-04"
}Aucun appel réseau requis après récupération initiale de la clé publique. Snippets Python, Node et Go ci-dessous.
# pip install pynacl requests
import json, base64, requests
from nacl.signing import VerifyKey
from nacl.exceptions import BadSignatureError
# 1. Fetch issuer public key (cache locally with rotation hints)
WK = requests.get("https://www.getinopay.com/.well-known/inopay", timeout=5).json()
pub_b64 = next(k["public_key"] for k in WK["keys"] if k["key_id"] == attestation["key_id"])
verify_key = VerifyKey(base64.urlsafe_b64decode(pub_b64 + "=="))
# 2. Canonicalize (RFC 8785 — keys sorted, no whitespace) and verify
payload = {k: v for k, v in attestation.items() if k != "signature"}
canonical = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
sig = base64.urlsafe_b64decode(attestation["signature"] + "==")
try:
verify_key.verify(canonical, sig)
print("OK — attestation signature valid")
except BadSignatureError:
raise SystemExit("REJECT — invalid signature")// npm install libsodium-wrappers node-fetch
import sodium from 'libsodium-wrappers';
await sodium.ready;
const wk = await fetch('https://www.getinopay.com/.well-known/inopay').then(r => r.json());
const pub = wk.keys.find(k => k.key_id === attestation.key_id).public_key;
const pubBytes = sodium.from_base64(pub, sodium.base64_variants.URLSAFE_NO_PADDING);
const { signature, ...payload } = attestation;
const canonical = JSON.stringify(
Object.fromEntries(Object.entries(payload).sort(([a], [b]) => a.localeCompare(b)))
);
const sigBytes = sodium.from_base64(signature, sodium.base64_variants.URLSAFE_NO_PADDING);
const valid = sodium.crypto_sign_verify_detached(
sigBytes, sodium.from_string(canonical), pubBytes
);
if (!valid) throw new Error('REJECT — invalid attestation signature');// import: encoding/base64 · encoding/json · crypto/ed25519 · net/http
package main
import (
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"sort"
)
func verifyAttestation(att map[string]any) error {
// 1. Fetch issuer public key from .well-known
resp, _ := http.Get("https://www.getinopay.com/.well-known/inopay")
defer resp.Body.Close()
var wk struct{ Keys []struct{ KeyID, PublicKey string } }
json.NewDecoder(resp.Body).Decode(&wk)
var pub ed25519.PublicKey
for _, k := range wk.Keys {
if k.KeyID == att["key_id"].(string) {
b, _ := base64.URLEncoding.DecodeString(k.PublicKey)
pub = ed25519.PublicKey(b)
}
}
// 2. Canonicalize (RFC 8785 stub — sorted keys, no whitespace)
sig := att["signature"].(string)
delete(att, "signature")
keys := make([]string, 0, len(att))
for k := range att { keys = append(keys, k) }
sort.Strings(keys)
canon := make(map[string]any, len(att))
for _, k := range keys { canon[k] = att[k] }
body, _ := json.Marshal(canon)
sigBytes, _ := base64.URLEncoding.DecodeString(sig)
if !ed25519.Verify(pub, body, sigBytes) {
return fmt.Errorf("REJECT — invalid signature")
}
return nil
}Expose les clés publiques courantes (avec key_id), la version du protocole, les capacités et les contacts. Mise à jour signalée par rotation explicite.
# GET https://www.getinopay.com/.well-known/inopay
{
"version": "1.0",
"issuer": "inopay",
"protocol": "kyc-attestation/v1",
"keys": [
{
"key_id": "staging-2026-04",
"algorithm": "Ed25519",
"public_key": "WkZmS2pYNXRxQU5wYTU0X3FHc...base64url",
"valid_from": "2026-04-01T00:00:00Z",
"valid_until": "2026-10-01T00:00:00Z"
},
{
"key_id": "staging-2026-10",
"algorithm": "Ed25519",
"public_key": "M0FoNDF6dG9LZjN1WXFwT09...base64url",
"valid_from": "2026-10-01T00:00:00Z",
"valid_until": "2027-04-01T00:00:00Z"
}
],
"crl_endpoint": "https://api.getinopay.com/v1/kyc/crl",
"contact": "security@getinopay.com"
}Trois niveaux progressifs alignés sur les exigences UEMOA / CEMAC. Les niveaux supérieurs incluent toutes les vérifications des niveaux inférieurs.
| Niveau | Exigences | Plafond / horizon |
|---|---|---|
| KYC1 | Pièce d'identité OCR + selfie | Comptes lite, 100 000 FCFA / mois |
| KYC2 | KYC1 + justificatif de domicile + vidéo selfie liveness | Investisseur particulier, 5 M FCFA / mois |
| KYC3 | KYC2 + déclaration source des fonds + vérification PEP | Investisseur qualifié, illimité |
Une attestation est révocable à tout moment. La CRL (Certificate Revocation List) est publiée signée et synchronisable. Intervalle de check recommandé : 15 min. Période de grâce post-révocation : 24 h.
La Doctrine CREPMF 2026 reconnaît la portabilité du KYC signé cryptographiquement comme équivalent à un onboarding traditionnel. Voir la page Conformité pour le détail réglementaire.
Voir /compliance#crepmfUne attestation KYC Inopay est un objet JSON signé Ed25519. Elle est vérifiable hors-ligne par n'importe quel intégrateur, sans appel à un service tiers. Reconnue par la Doctrine CREPMF 2026 comme équivalent à un onboarding traditionnel.
Retour aux développeurs