API reference
Every endpoint, every request/response schema, with curl examples.
Base URL: https://api.verifymx.xyz
Version: v1
Auth: All endpoints except /v1/auth/* and /v1/otp/* require Authorization: Bearer <token>.
Content-Type: application/json unless noted as multipart.
The machine-readable OpenAPI spec is at GET /v1/openapi.json.
Error Format
All errors use a consistent envelope:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Human-readable description",
"details": [...]
}
}
Common error codes:
| Code | HTTP | Meaning |
|---|---|---|
VALIDATION_ERROR | 422 | Request body or query string failed schema validation |
UNAUTHORIZED | 401 | Missing, expired, or invalid token/API key |
FORBIDDEN | 403 | Authenticated but insufficient role |
NOT_FOUND | 404 | Resource does not exist or belongs to another org |
CONFLICT | 409 | Duplicate resource (e.g. org slug already taken) |
RATE_LIMITED | 429 | Exceeded 10 req/s per org |
INTERNAL_ERROR | 500 | Unexpected server error |
Authentication
POST /v1/auth/register
Creates a new organization, admin user, and first API key. The raw API key is shown once in the response — store it immediately.
Auth required: No
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
orgName | string | Yes | 2–100 chars. Determines the org slug. |
email | string | Yes | Admin user email address |
password | string | Yes | 12–128 chars |
curl -X POST https://api.verifymx.xyz/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"orgName": "Acme Corp",
"email": "admin@acme.mx",
"password": "supersecretpass123"
}'
Response 201:
{
"org": {
"id": "org_01HXYZ...",
"name": "Acme Corp",
"slug": "acme-corp"
},
"user": {
"id": "usr_01HXYZ...",
"email": "admin@acme.mx",
"role": "OWNER"
},
"apiKey": {
"id": "key_01HXYZ...",
"name": "Default key",
"key": "vmx_a1b2c3d4e5f6g7h8i9j0...",
"prefix": "vmx_a1b2c3d4",
"createdAt": "2026-04-08T12:00:00.000Z"
}
}
POST /v1/auth/token
OAuth 2.0 client_credentials token exchange. Returns a JWT valid for 1 hour.
Auth required: No
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
grant_type | string | Yes | Must be "client_credentials" |
client_id | string | Yes | Your org.id |
client_secret | string | Yes | Your raw API key (vmx_...) |
curl -X POST https://api.verifymx.xyz/v1/auth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "client_credentials",
"client_id": "org_01HXYZ...",
"client_secret": "vmx_a1b2c3d4e5f6..."
}'
Response 200:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600
}
Verifications
POST /v1/verifications
Creates a new verification session.
Auth required: Yes
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
workflowId | string | Yes | ID of an active workflow |
metadata | object | No | Arbitrary key-value pairs stored with the verification |
callbackUrl | string | No | URI to POST results to on completion (in addition to webhooks) |
externalId | string | No | Your internal user/session ID (max 255 chars) |
curl -X POST https://api.verifymx.xyz/v1/verifications \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"workflowId": "wf_01HXYZ...",
"externalId": "user-42",
"metadata": { "planTier": "premium" }
}'
Response 201:
{
"verification": {
"id": "ver_01HXYZ...",
"status": "PENDING",
"workflowId": "wf_01HXYZ...",
"externalId": "user-42",
"metadata": { "planTier": "premium" },
"ipAddress": "203.0.113.10",
"createdAt": "2026-04-08T12:00:00.000Z",
"updatedAt": "2026-04-08T12:00:00.000Z"
}
}
GET /v1/verifications
List verifications for your organization with optional filtering.
Auth required: Yes
Query parameters:
| Param | Type | Default | Description |
|---|---|---|---|
status | string | — | Filter by status: PENDING, IN_PROGRESS, APPROVED, REJECTED, EXPIRED, MANUAL_REVIEW |
limit | integer | 20 | Results per page (1–100) |
offset | integer | 0 | Pagination offset |
search | string | — | Full-text search against external ID, metadata (max 255 chars) |
curl "https://api.verifymx.xyz/v1/verifications?status=APPROVED&limit=10&offset=0" \
-H "Authorization: Bearer $TOKEN"
Response 200:
{
"verifications": [
{
"id": "ver_01HXYZ...",
"status": "APPROVED",
"externalId": "user-42",
"createdAt": "2026-04-08T12:00:00.000Z"
}
],
"pagination": {
"total": 142,
"limit": 10,
"offset": 0,
"hasMore": true
}
}
GET /v1/verifications/:id
Retrieve a single verification with all step results and uploaded documents.
Auth required: Yes
curl "https://api.verifymx.xyz/v1/verifications/ver_01HXYZ..." \
-H "Authorization: Bearer $TOKEN"
Response 200:
{
"verification": {
"id": "ver_01HXYZ...",
"status": "APPROVED",
"workflowId": "wf_01HXYZ...",
"steps": [
{
"id": "vs_01...",
"type": "DOCUMENT_OCR",
"status": "PASSED",
"result": {
"name": "JUAN PÉREZ GARCÍA",
"curp": "PEGJ800101HMCRRN09",
"dateOfBirth": "1980-01-01"
}
}
],
"createdAt": "2026-04-08T12:00:00.000Z",
"updatedAt": "2026-04-08T12:01:00.000Z"
},
"documents": [
{
"id": "doc_01...",
"type": "front",
"mimeType": "image/jpeg",
"fileSize": 512000,
"createdAt": "2026-04-08T12:00:30.000Z"
}
]
}
PATCH /v1/verifications/:id/status
Manually set verification status. Requires REVIEWER role or higher.
Auth required: Yes (REVIEWER+)
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
status | string | Yes | APPROVED, REJECTED, or MANUAL_REVIEW |
notes | string | No | Reviewer notes (max 2000 chars) |
curl -X PATCH "https://api.verifymx.xyz/v1/verifications/ver_01HXYZ.../status" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"status": "APPROVED", "notes": "Documents verified in person"}'
Response 200:
{
"verification": {
"id": "ver_01HXYZ...",
"status": "APPROVED",
"updatedAt": "2026-04-08T12:05:00.000Z"
}
}
POST /v1/verifications/:id/inputs
Upload document images and selfies for processing. Send as multipart/form-data.
Auth required: Yes
Content-Type: multipart/form-data
Max file size: 10 MB
Max files per request: 10
Allowed types: image/jpeg, image/png, image/webp, application/pdf
Field names map to document types. Use descriptive names: front, back, selfie,
liveness. The field name is stored as document.type and used to infer which
processing step to run.
The verification must be in PENDING or IN_PROGRESS status.
curl -X POST "https://api.verifymx.xyz/v1/verifications/ver_01HXYZ.../inputs" \
-H "Authorization: Bearer $TOKEN" \
-F "front=@ine_front.jpg;type=image/jpeg" \
-F "back=@ine_back.jpg;type=image/jpeg" \
-F "selfie=@selfie.jpg;type=image/jpeg"
Response 202:
{
"uploaded": [
{ "id": "doc_01...", "type": "front", "mimeType": "image/jpeg" },
{ "id": "doc_02...", "type": "back", "mimeType": "image/jpeg" },
{ "id": "doc_03...", "type": "selfie", "mimeType": "image/jpeg" }
],
"message": "Documents queued for processing"
}
Processing is asynchronous. Subscribe to webhooks or poll GET /v1/verifications/:id
for results.
Workflows
GET /v1/workflows
List all workflows for your organization.
Auth required: Yes
curl "https://api.verifymx.xyz/v1/workflows" \
-H "Authorization: Bearer $TOKEN"
Response 200:
{
"workflows": [
{
"id": "wf_01HXYZ...",
"name": "INE + Selfie",
"isActive": true,
"steps": [
{ "type": "DOCUMENT_OCR", "order": 0 },
{ "type": "FACE_MATCH", "order": 1 }
],
"createdAt": "2026-04-08T12:00:00.000Z"
}
]
}
POST /v1/workflows
Create a new workflow. Requires ADMIN or OWNER role.
Auth required: Yes (ADMIN+)
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | 1–200 chars |
description | string | No | Up to 1000 chars |
steps | array | Yes | At least one step (see step schema below) |
Step object:
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | DOCUMENT_OCR, FACE_MATCH, LIVENESS, or GOVERNMENT_VERIFY |
order | integer | Yes | Execution order (0-indexed) |
config | object | No | Step-specific configuration options |
curl -X POST "https://api.verifymx.xyz/v1/workflows" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Full KYC",
"description": "INE OCR + liveness + government verify",
"steps": [
{ "type": "DOCUMENT_OCR", "order": 0 },
{ "type": "LIVENESS", "order": 1 },
{ "type": "FACE_MATCH", "order": 2 },
{ "type": "GOVERNMENT_VERIFY", "order": 3 }
]
}'
Response 201:
{
"workflow": {
"id": "wf_01HXYZ...",
"name": "Full KYC",
"isActive": true,
"steps": [...],
"createdAt": "2026-04-08T12:00:00.000Z"
}
}
GET /v1/workflows/:id
Get a single workflow by ID.
Auth required: Yes
curl "https://api.verifymx.xyz/v1/workflows/wf_01HXYZ..." \
-H "Authorization: Bearer $TOKEN"
Response 200: same shape as the item in the list endpoint.
PATCH /v1/workflows/:id
Update workflow name, description, or steps. Requires ADMIN or OWNER role.
Auth required: Yes (ADMIN+)
Request body: same fields as POST, all optional.
curl -X PATCH "https://api.verifymx.xyz/v1/workflows/wf_01HXYZ..." \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Full KYC v2"}'
Response 200: updated workflow object.
DELETE /v1/workflows/:id
Soft-deletes the workflow (isActive set to false). Existing verifications are
unaffected. New verifications cannot be created against an inactive workflow. Requires
ADMIN or OWNER role.
Auth required: Yes (ADMIN+)
curl -X DELETE "https://api.verifymx.xyz/v1/workflows/wf_01HXYZ..." \
-H "Authorization: Bearer $TOKEN"
Response 204: empty body.
Webhooks
GET /v1/webhooks
List all webhook endpoints. Signing secrets are never returned in list responses.
Auth required: Yes
curl "https://api.verifymx.xyz/v1/webhooks" \
-H "Authorization: Bearer $TOKEN"
Response 200:
{
"webhooks": [
{
"id": "whe_01HXYZ...",
"url": "https://myapp.mx/webhooks/verifymx",
"events": ["verification.approved", "verification.rejected"],
"isActive": true,
"createdAt": "2026-04-08T12:00:00.000Z"
}
]
}
POST /v1/webhooks
Register a new webhook endpoint. Requires ADMIN or OWNER role. The signing secret is shown once in the response — store it immediately.
Auth required: Yes (ADMIN+)
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTPS endpoint URL (max 2048 chars) |
events | array | Yes | Event types to subscribe to (at least one) |
secret | string | No | Custom signing secret (16–256 chars). Auto-generated if omitted. |
Valid event types: verification.created, verification.completed,
verification.approved, verification.rejected, verification.manual_review,
verification.updated, step.completed, step.failed
curl -X POST "https://api.verifymx.xyz/v1/webhooks" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://myapp.mx/webhooks/verifymx",
"events": ["verification.approved", "verification.rejected", "verification.manual_review"]
}'
Response 201:
{
"webhook": {
"id": "whe_01HXYZ...",
"url": "https://myapp.mx/webhooks/verifymx",
"events": ["verification.approved", "verification.rejected", "verification.manual_review"],
"secret": "a1b2c3d4e5f6...",
"isActive": true,
"createdAt": "2026-04-08T12:00:00.000Z"
}
}
DELETE /v1/webhooks/:id
Remove a webhook endpoint. Requires ADMIN or OWNER role.
Auth required: Yes (ADMIN+)
curl -X DELETE "https://api.verifymx.xyz/v1/webhooks/whe_01HXYZ..." \
-H "Authorization: Bearer $TOKEN"
Response 204: empty body.
POST /v1/webhooks/:id/test
Send a synthetic verification.created test payload to the endpoint to verify your
handler is reachable and processing signatures correctly.
Auth required: Yes
curl -X POST "https://api.verifymx.xyz/v1/webhooks/whe_01HXYZ.../test" \
-H "Authorization: Bearer $TOKEN"
Response 200:
{ "delivered": true }
OTP
OTP endpoints are unauthenticated. They are called directly from the end-user's
browser during a live verification flow, using verificationId as the capability token.
POST /v1/otp/send
Generate and deliver a 6-digit OTP via SMS or email.
Rate limit: 3 sends per destination per 10 minutes.
Auth required: No
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
verificationId | string (UUID) | Yes | Active verification session ID |
channel | string | Yes | "sms" or "email" |
destination | string | Yes | Phone number (E.164) or email address |
curl -X POST "https://api.verifymx.xyz/v1/otp/send" \
-H "Content-Type: application/json" \
-d '{
"verificationId": "ver_01HXYZ...",
"channel": "sms",
"destination": "+525512345678"
}'
Response 200:
{
"sent": true,
"channel": "sms",
"expiresIn": 300
}
POST /v1/otp/verify
Verify a 6-digit code submitted by the user. Codes expire after 5 minutes. After 5 failed attempts, the code is invalidated.
Auth required: No
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
verificationId | string (UUID) | Yes | Same session used when sending |
channel | string | Yes | "sms" or "email" |
code | string | Yes | Exactly 6 digits |
curl -X POST "https://api.verifymx.xyz/v1/otp/verify" \
-H "Content-Type: application/json" \
-d '{
"verificationId": "ver_01HXYZ...",
"channel": "sms",
"code": "847291"
}'
Response 200:
{
"valid": true,
"attemptsRemaining": 5
}
If valid is false and attemptsRemaining is 0, the code has been invalidated —
call /v1/otp/send again.
AML
POST /v1/aml/screen
Screen a full name against AML watchlists. Returns matching entries with similarity scores. Uses fuzzy matching to handle name variations and transliterations.
Auth required: Yes
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
fullName | string | Yes | 2–500 chars |
fuzzyThreshold | number | No | Similarity threshold 0–1 (default: 0.75) |
lists | string[] | No | Watchlist IDs to search. Omit to search all. |
country | string | No | ISO 3166-1 alpha-2 country code to narrow results |
curl -X POST "https://api.verifymx.xyz/v1/aml/screen" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"fullName": "Juan García López",
"fuzzyThreshold": 0.8,
"country": "MX"
}'
Response 200:
{
"matches": [
{
"list": "OFAC_SDN",
"name": "JUAN GARCIA LOPEZ",
"score": 0.98,
"entityType": "individual",
"country": "MX"
}
],
"totalMatches": 1,
"screened": true
}
GET /v1/aml/lists
List available watchlists with entry counts.
Auth required: Yes
curl "https://api.verifymx.xyz/v1/aml/lists" \
-H "Authorization: Bearer $TOKEN"
Response 200:
{
"lists": [
{ "id": "OFAC_SDN", "name": "OFAC Specially Designated Nationals", "entries": 18432 },
{ "id": "UN_SC", "name": "UN Security Council Consolidated List", "entries": 4201 },
{ "id": "EU_CONSOL", "name": "EU Consolidated Sanctions List", "entries": 2918 },
{ "id": "MX_SAT_69B", "name": "SAT Article 69-B (Mexico)", "entries": 1024 }
]
}
POST /v1/aml/monitor
Enable or disable daily re-screening for a verification. When enabled, the name is re-screened every 24 hours for 90 days; if new matches appear, a webhook fires.
Auth required: Yes
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
verificationId | string | Yes | Verification to monitor |
fullName | string | Yes | Name to re-screen daily |
enabled | boolean | Yes | true to enable, false to disable |
curl -X POST "https://api.verifymx.xyz/v1/aml/monitor" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"verificationId": "ver_01HXYZ...",
"fullName": "Juan García López",
"enabled": true
}'
Response 200:
{
"verificationId": "ver_01HXYZ...",
"monitoring": true,
"message": "Daily AML monitoring enabled"
}
Organizations
GET /v1/organizations/me
Get current organization details.
Auth required: Yes
curl "https://api.verifymx.xyz/v1/organizations/me" \
-H "Authorization: Bearer $TOKEN"
Response 200:
{
"org": {
"id": "org_01HXYZ...",
"name": "Acme Corp",
"slug": "acme-corp",
"dataRetentionDays": 365,
"createdAt": "2026-04-08T12:00:00.000Z"
}
}
PATCH /v1/organizations/me
Update organization settings. Requires ADMIN or OWNER role.
Auth required: Yes (ADMIN+)
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | 2–100 chars |
dataRetentionDays | integer | No | 30–3650 days |
curl -X PATCH "https://api.verifymx.xyz/v1/organizations/me" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"dataRetentionDays": 730}'
Response 200: updated org object.
GET /v1/organizations/me/api-keys
List all API keys for your organization. Key hashes are never returned.
Auth required: Yes
curl "https://api.verifymx.xyz/v1/organizations/me/api-keys" \
-H "Authorization: Bearer $TOKEN"
Response 200:
{
"apiKeys": [
{
"id": "key_01HXYZ...",
"name": "Default key",
"prefix": "vmx_a1b2c3d4",
"rateLimit": 10,
"isActive": true,
"lastUsedAt": "2026-04-08T11:59:00.000Z",
"createdAt": "2026-04-08T12:00:00.000Z"
}
]
}
POST /v1/organizations/me/api-keys
Create a new API key. Requires ADMIN or OWNER role. The raw key is returned once.
Auth required: Yes (ADMIN+)
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | 1–100 chars |
rateLimit | integer | No | Requests per second (0–1000, default 10) |
curl -X POST "https://api.verifymx.xyz/v1/organizations/me/api-keys" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Production key", "rateLimit": 50}'
Response 201:
{
"apiKey": {
"id": "key_02HXYZ...",
"name": "Production key",
"key": "vmx_x9y8z7w6...",
"prefix": "vmx_x9y8z7w6",
"rateLimit": 50,
"createdAt": "2026-04-08T12:00:00.000Z"
}
}
DELETE /v1/organizations/me/api-keys/:id
Revoke an API key. Requires ADMIN or OWNER role. You cannot revoke the key currently authenticating the request.
Auth required: Yes (ADMIN+)
curl -X DELETE "https://api.verifymx.xyz/v1/organizations/me/api-keys/key_02HXYZ..." \
-H "Authorization: Bearer $TOKEN"
Response 204: empty body.
System
GET /v1/openapi.json
Returns the full OpenAPI 3.1 specification generated from all registered routes.
Auth required: No
Rate limit: 60 req/min (relaxed for CI tooling)
curl "https://api.verifymx.xyz/v1/openapi.json" | jq '.paths | keys'
GET /health
Health check. Returns 200 when the server is ready; checks Redis connectivity.
Auth required: No
curl "https://api.verifymx.xyz/health"
Response 200:
{
"status": "ok",
"uptime": 3600.42,
"timestamp": "2026-04-08T12:00:00.000Z",
"redis": "ok"
}