KYC
Submit and inspect KYC for end-users. Required before on-ramp and off-ramp.
Overview
Brazilian regulation requires the user behind every on-ramp and off-ramp to be identified. Hodle exposes a thin pass-through over Avenia's KYC level-1 attempt API:
POST /api/kyc— submit personal data + document photos, get back anattemptId.GET /api/kyc/{attemptId}— poll for the result (PENDING→APPROVEDorREJECTED).- Webhook
kyc.completed— pushed when the attempt resolves. See Webhooks.
A user is allowed to transact (/api/deposit/asset, /api/wallet/payout, /api/withdraw/pix) only after the most recent attempt is APPROVED.
Flow
- Collect the user's data and ID photos in your UI.
POST /api/kyc— Hodle returns anattemptId.- Wait for either the
kyc.completedwebhook or pollGET /api/kyc/{attemptId}every ~30 seconds. - Once
status: APPROVED, the user can on-ramp / off-ramp.
Typical resolution is under 2 minutes for clean submissions; manual review can take up to 24h.
POST /api/kyc
Request
curl --request POST \
--url https://api.hodle.com.br/api/kyc \
--header "Authorization: Bearer $API_KEY" \
--header "Content-Type: application/json" \
--data '{
"userId": "65f1a83b6b7c2b001f3c9e21",
"fullName": "João da Silva",
"dateOfBirth": "1990-01-15",
"countryOfTaxId": "BRA",
"taxIdNumber": "12345678900",
"email": "joao@example.com",
"phone": "+5511999990000",
"country": "BRA",
"state": "SP",
"city": "São Paulo",
"zipCode": "01000-000",
"streetAddress": "Av. Paulista, 1000",
"uploadedSelfieId": "sel_8f3...",
"uploadedDocumentId": "doc_b21..."
}'const res = await fetch('https://api.hodle.com.br/api/kyc', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.HODLE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
userId,
fullName: 'João da Silva',
dateOfBirth: '1990-01-15',
countryOfTaxId: 'BRA',
taxIdNumber: '12345678900',
email: 'joao@example.com',
country: 'BRA',
state: 'SP',
city: 'São Paulo',
zipCode: '01000-000',
streetAddress: 'Av. Paulista, 1000',
uploadedSelfieId,
uploadedDocumentId,
}),
})
const data = await res.json()import os, requests
res = requests.post(
"https://api.hodle.com.br/api/kyc",
headers={
"Authorization": f"Bearer {os.environ['HODLE_API_KEY']}",
"Content-Type": "application/json",
},
json={
"userId": user_id,
"fullName": "João da Silva",
"dateOfBirth": "1990-01-15",
"countryOfTaxId": "BRA",
"taxIdNumber": "12345678900",
"email": "joao@example.com",
"country": "BRA",
"state": "SP",
"city": "São Paulo",
"zipCode": "01000-000",
"streetAddress": "Av. Paulista, 1000",
"uploadedSelfieId": uploaded_selfie_id,
"uploadedDocumentId": uploaded_document_id,
},
)
data = res.json()Parameters
| Field | Type | Required | Description |
|---|---|---|---|
userId | string | Yes | The end-user's Hodle id, returned by POST /api/user/create. |
fullName | string | Yes | Full legal name as it appears on the document. |
dateOfBirth | string | Yes | YYYY-MM-DD. |
countryOfTaxId | string | Yes | ISO-3 country code of the tax-id issuer (e.g. BRA). |
taxIdNumber | string | Yes | CPF (Brazilians) or equivalent. Digits only. |
email | string | Yes | Must match the user's email on file. |
phone | string | No | E.164 (+5511...). |
country | string | Yes | ISO-3 country code of residence. |
state | string | Yes | State / federative unit. |
city | string | Yes | City of residence. |
zipCode | string | Yes | Postal code. |
streetAddress | string | Yes | Street + number + complement. |
uploadedSelfieId | string | Yes | Identifier returned by your upload-image endpoint (provided separately). |
uploadedDocumentId | string | Yes | Identifier of the front-of-document photo. |
Response
{
"success": true,
"data": {
"attemptId": "att_8f3a...",
"status": "PENDING",
"createdAt": "2026-05-09T22:00:00.000Z"
}
}Errors
{
"success": false,
"error": "Validation failed",
"details": [{ "field": "taxIdNumber", "message": "must contain only digits" }]
}{ "success": false, "error": "User has an APPROVED KYC attempt" }GET /api/kyc/{attemptId}
Request
curl --request GET \
--url https://api.hodle.com.br/api/kyc/att_8f3a... \
--header "Authorization: Bearer $API_KEY"const res = await fetch(
`https://api.hodle.com.br/api/kyc/${attemptId}`,
{ headers: { Authorization: `Bearer ${process.env.HODLE_API_KEY}` } },
)
const data = await res.json()import os, requests
res = requests.get(
f"https://api.hodle.com.br/api/kyc/{attempt_id}",
headers={"Authorization": f"Bearer {os.environ['HODLE_API_KEY']}"},
)
data = res.json()Response
{
"success": true,
"data": {
"attemptId": "att_8f3a...",
"userId": "65f1a83b6b7c2b001f3c9e21",
"status": "APPROVED",
"level": 1,
"rejectionReason": null,
"createdAt": "2026-05-09T22:00:00.000Z",
"updatedAt": "2026-05-09T22:01:43.000Z"
}
}{
"success": true,
"data": {
"attemptId": "att_8f3a...",
"status": "REJECTED",
"rejectionReason": "DOCUMENT_BLURRY",
"createdAt": "2026-05-09T22:00:00.000Z",
"updatedAt": "2026-05-09T22:01:43.000Z"
}
}Status values
| Status | Meaning |
|---|---|
PENDING | Under review. Keep polling or wait for the webhook. |
APPROVED | User cleared. Transact endpoints will accept this user now. |
REJECTED | Failed. rejectionReason is a stable enum your UI can map. |
EXPIRED | Documents aged out. Submit a new attempt. |
Webhook payload
When the attempt resolves, Hodle POSTs to your registered webhook with event: "kyc.completed":
{
"event": "kyc.completed",
"data": {
"attemptId": "att_8f3a...",
"userId": "65f1a83b6b7c2b001f3c9e21",
"status": "APPROVED"
}
}See Webhooks for signature verification.