Verification API Reference
This page is the HTTP contract for the Verification API — request parameters, response schemas, session states, and error codes. For the conceptual walkthrough — integration patterns, assurance tiers, and what each evidence field means — start with Get Started or Verify from a Web App.
Prerequisites
A Blerify service account with the
verifications.apirole — see AuthenticationA verification template UUID, created in the Blerify Portal
Your backend can reach the Blerify API base URL for your environment
Authentication
Every request your backend makes must carry a service account token:
Authorization: Bearer <service_account_token>Tokens are obtained via the OAuth 2.0 client_credentials grant using private_key_jwt as the client authentication method (RFC 7523). See Authentication for how to create a service account and exchange credentials for a token.
Cache the token and refresh before expiry — don't request a new one on every call. A 401 means your token has expired. A 403 on a template that exists means your service account hasn't been granted access to that project in the Portal.
Base URL and conventions
All paths are relative to https://api.blerify.com. Your Blerify admin provides the exact base URL for staging and production environments.
Path namespaces. The runtime endpoints live under two distinct prefixes:
Verifier (your backend)
/client/api/v2/openid4vp/organizations/:oid/projects/:pid/verifications
Authorization: Bearer <service_account_token>
Your backend
Wallet / public
/public/api/v1/openid4vp/organizations/:oid/projects/:pid/verifications
None — the one-time request UUID is the bearer secret
The user's wallet
Content type. All endpoints accept and return application/json.
Session TTL. 5 minutes from creation by default, configurable per session or per template in the Portal. After expiry, all endpoints for that session return 410 Gone. The user needs a fresh session to retry.
Error shape. Every error response uses:
For the full OpenAPI spec, see https://dev.blerify.com.
Endpoints
Your backend calls exactly two endpoints. Everything else — the wallet fetching the credential request, the wallet submitting the presentation, attestation data exchange — happens between the wallet and Blerify directly. You don't implement any of that.
Start a verification session
Creates a new session from the template identified by {vid}. Call this from your backend when the user is ready to verify.
Request body
nonce
Yes
A fresh, random, unguessable value your backend generates per session. It cryptographically binds the user's presentation to this session — replay protection. Never reuse a nonce. Minimum 6 characters.
expirationTime
No
Session lifetime in seconds. Defaults to 300 (5 minutes), or the template-level value if one is set in the Portal.
redirect_uri
No
Where the user's browser returns after completing the flow. Must match a URI you whitelisted in the Portal. Required for same-device web flows.
client_context.session_ip
No
The user's IP address as your frontend sees it. Blerify compares this against the wallet's IP as a relay-attack signal at Standard tier and above. Include it from the start.
client_context.session_id
No
Your internal session identifier. Echoed back in the result's evidence so you can link Blerify's record to your own audit trail.
Response — 200
transaction_id
Your key for polling this session. Pass it to every subsequent call.
request_id
One-time UUID identifying the signed credential request for this session. Use it to build the wallet Universal Link (see below). Single-use and short-lived.
request_uri_method
post or get. Copy verbatim into the wallet Universal Link.
client_id_scheme
did or x509_san_dns. Copy verbatim into the wallet Universal Link.
client_id
The verifier identifier the user's wallet will display and validate. Copy verbatim into the wallet Universal Link.
Errors
Status
error
What to do
400
invalid_request
Malformed JSON or invalid field. Fix and retry.
401
invalid_token
Token missing or expired. Refresh your service account token.
403
forbidden
Your service account isn't authorized for this template. Check your Portal configuration.
404
not_found
Template doesn't exist. Verify the UUID.
Build the wallet Universal Link
Wrap the session response into a URL that opens the Blerify Wallet:
The request URL you encode into request_uri is the public wallet endpoint for this session, built from request_id:
All four query parameters are required. URL-encode the request_uri value. Build a fresh link per session — request_id is single-use.
Use this same URL for all three integration patterns:
Same-device (mobile web): render it as an anchor or button
Cross-device (desktop): encode it in a QR code
App-to-app (native mobile): open it via your platform's URL-opening API — see Mobile Integration
Retrieve the result
Returns the current session status and, once the session reaches COMPLETED or FAILED, the full result. Poll this every 1–2 seconds.
Note the query parameter name: transaction-id (hyphen), while the response body field is transaction_id (underscore).
Query parameters
transaction-id
Yes
The UUID returned by the session-creation call.
Errors
Status
error
What to do
400
invalid_request
Missing transaction-id. Fix and retry.
401
invalid_token
Token expired. Refresh and retry.
403
forbidden
Your service account can't read this session.
404
not_found
transaction-id doesn't exist.
410
session_expired
Session TTL elapsed. Start a new one.
Session states
A session moves through these states in order:
While the session is in CREATED, PENDING, SUBMITTED, or VALIDATING, the poll response is:
Treat all in-progress states the same way: keep polling.
COMPLETED vs FAILED. COMPLETED means Blerify produced a verdict — including negative ones. A revoked or expired credential returns COMPLETED with the relevant flags set inside credentials[].result. FAILED means no trustworthy verdict could be produced: malformed request, replay or nonce mismatch, expired session, a required credential or attestation item was never submitted, or an unrecoverable internal error.
A credential whose signature doesn't verify also returns COMPLETED, but that entry's data is withheld (data: null) because the claims can't be trusted.
Response shapes
assurance_level and effective_tier
Every completed response carries both fields:
assurance_level— what your template requestedeffective_tier— what the evidence actually supports
On a COMPLETED result, the two always match — required inputs all arrived and were valid. effective_tier is retained in the envelope for backward compatibility. Always enforce your risk policy against effective_tier.
Per-credential result fields
Each entry in credentials[] carries a result object:
credential_valid
boolean
Overall verdict. true only when every component check passes. A single failing check sets this to false.
signature_valid
boolean
The issuer's cryptographic signature is valid. When false, that entry's data is withheld.
issuer_trusted
boolean
The issuer is on the trust list configured in your template.
revoked
boolean
The issuer revoked this credential before presentation.
expired
boolean
The credential's validity period has passed.
holder_binding
string
VERIFIED when the presenter cryptographically proved they hold the credential's signing key.
COMPLETED reports facts — it never automatically blocks. You apply your own policy.
Basic tier
Basic evidence is minimal: a timestamp and the cryptographic validation results per credential. No hardware attestation, no network signals, no signed_evidence JWT.
The data.namespaces structure reflects the ISO 18013-5 mobile document format. Fields are grouped by namespace; most identity document fields live under org.iso.18013.5.1. The exact fields present depend on what your template requested — the user only disclosed what you asked for.
Standard tier — Android
Standard adds network signals, key attestation, device metadata, a document render image, and a signed audit JWT.
Standard tier — iOS
On iOS, three fields differ from the Android response. All three reflect a structural difference in what Apple exposes to third-party apps, not a failure or a downgrade signal:
device_metadata.attestedisfalse— Apple's attestation format doesn't include device model or OS version, so the wallet self-reports themkey_attestation.security_level_attestedisfalse— Apple provides no API for third-party apps to cryptographically prove Secure Enclave key residencykey_biometric_binding.attestedisfalse— Apple's attestation format carries no key access-control flags
The Secure Enclave still enforces biometric requirements locally — the limitation is that this can't be proven remotely. See Platform Attestation for the full breakdown.
device_attestation_reasonisnot_requestedhere, notplatform_not_available. Standard never requests device attestation on either platform — the reason reflects your template configuration, not a platform limitation. The valueplatform_not_availableonly appears at Premium on iOS, where device attestation is requested but iOS has no equivalent API to fulfill it.
Premium tier — Android
Premium adds three fields to the Standard evidence: device_attestation, liveness_verification, and coercion_check.
Premium tier — iOS
On iOS at Premium, device_attestation is always null. iOS has no Play Integrity equivalent available to third-party apps. The Premium delta on iOS comes entirely from the biometric layer — liveness_verification and coercion_check work identically on both platforms.
Failed session
FAILED means the verification couldn't run to a trustworthy verdict. It is never used for credential-level outcomes — a revoked, expired, or untrusted-issuer credential returns COMPLETED with those facts set inside credentials[].result.
Evidence field reference
Core (all tiers)
timestamp
When Blerify completed the verification (ISO-8601 UTC). Use this as your audit timestamp.
Network signals (Standard+)
wallet_ip
IP address of the user's phone as Blerify observed it when the wallet connected.
client_reported_ip
IP your frontend sent in client_context.session_ip. Self-declared — Blerify preserves it as received.
client_session_id
The identifier you passed in client_context.session_id, echoed back for audit correlation.
ip_match
Whether the two IPs agree within Blerify's tolerance rules. false is a relay-attack signal; it isn't conclusive on its own — VPN, NAT, and mobile carriers frequently route through different IPs.
geolocation
IP-derived approximate location for both sides. country_match is the useful signal; city-level accuracy varies by ISP. source is always "ip_geolocation" today.
Device metadata (Standard+)
device_metadata.device_model
Phone model.
device_metadata.os_version
OS and version.
device_metadata.attested
true on Android — values come from the hardware-signed attestation certificate. false on iOS — Apple's attestation format doesn't include device metadata, so the wallet self-reports.
device_metadata.attestation_source
android_key_attestation on Android; wallet_self_reported on iOS.
Key attestation (Standard+)
Proves the credential's signing key lives in secure hardware and requires biometric authentication to use. For a deeper explanation of the Android/iOS differences, see Platform Attestation.
key_attestation.verified
Blerify validated the attestation chain against the platform's root trust.
key_attestation.hardware_backed
The signing key lives in the device's secure hardware (TEE, StrongBox, or Secure Enclave).
key_attestation.security_level
STRONG_BOX or TRUSTED_ENVIRONMENT on Android; SECURE_ENCLAVE on iOS.
key_attestation.security_level_attested
true on Android — hardware-signed. false on iOS — self-reported, because Apple provides no API for third-party apps to cryptographically prove Secure Enclave key residency.
key_attestation.platform
android or ios.
key_attestation.format
android_key_attestation or app_attest.
key_biometric_binding.biometric_required
The key requires biometric or device PIN to sign. Prevents the key from being used without user interaction.
key_biometric_binding.auth_type
Authentication methods bound to the key: FINGERPRINT, FACE, or DEVICE_CREDENTIAL (PIN/pattern/password, on devices without a strong biometric sensor).
key_biometric_binding.auth_timeout_seconds
0 means biometric is required for every signature, no caching.
key_biometric_binding.invalidated_on_enrollment_change
true means the key is destroyed if someone adds a new biometric to the device.
key_biometric_binding.attested
true on Android — the access-control properties above are hardware-signed. false on iOS — Apple's attestation format carries no key access-control flags.
Document render (Standard+)
A wallet-generated image of the credential showing exactly what the user disclosed. Disclosed fields are legible; every undisclosed field is blurred. Delivered as one image per document face. Use this for manual review, dispute resolution, and customer service — it's a human-viewable complement to the machine-readable credentials[].data, not a substitute for it.
document_render.format
MIME type of the images, e.g. image/jpeg.
document_render.redaction
How undisclosed fields are obscured. blur today.
document_render.images[]
One entry per document side. Each has side (front or back) and data (base64-encoded image). Single-sided documents return one entry.
Device attestation (Premium only)
Proves the wallet app is genuine and the device is unmodified — not an emulator, not rooted, not a sideloaded binary.
On iOS, device_attestation is always null with device_attestation_reason: "platform_not_available". iOS has no Play Integrity equivalent available to third-party apps. This is a structural platform limitation, not a session failure.
device_attestation.verified
The attestation token was successfully validated.
device_attestation.platform
android (Play Integrity).
device_attestation.device_integrity
Android only. true means the device passes the integrity check — no unlocked bootloader, no root, no emulator.
device_attestation.app_integrity
The wallet binary is recognized as a genuine, unmodified build.
device_attestation.nonce_match
Confirms the attestation token was generated for this specific session's nonce. false indicates a possible replay attempt.
Liveness verification (Premium only)
The wallet captures a liveness challenge during the session. Blerify compares the result against the portrait embedded in the credential server-side. You never receive raw biometric data — only the result and a confidence score.
liveness_verification.verified
The face match passed and the anti-spoofing check found no attack.
liveness_verification.match_confidence
0–1 confidence score from the face comparison.
liveness_verification.spoof_detected
true if the anti-spoofing check flagged a photo, video, 3D mask, or deepfake.
liveness_verification.challenge_completed
The user completed the liveness challenge.
Coercion check (Premium only)
Silent gaze-tracking and micro-expression analysis on the liveness frames. Reports whether the user showed signs of distress. Invisible to both the user and any potential coercer — they see only the standard liveness challenge.
Blerify never blocks a flow on this signal. It reports the fact; you decide how to act.
coercion_check.status
CLEAR, GAZE_ANOMALY, EXPRESSION_ANOMALY
GAZE_ANOMALY: persistent off-camera gaze toward a fixed point. EXPRESSION_ANOMALY: involuntary stress or fear expressions detected.
coercion_check.method
"facial"
Always "facial" in this version.
coercion_check.alert
boolean
Convenience flag: false iff status == "CLEAR".
Signed evidence (Standard+)
signed_evidence is a JWT containing the full evidence object, signed by Blerify. Store it as your tamper-proof audit record. Blerify does not retain it after the session expires — you are the sole custodian.
To validate it later: verify the JWT signature against Blerify's public key at the JWKS endpoint described at https://dev.blerify.com.
_reason fields
Every optional evidence field has a companion _reason field. When the main field is populated, _reason is null. When it's null, _reason explains why.
null
The field is populated — nothing to explain
N/A
not_requested
Your template didn't configure this tier or feature
Expected absence
platform_not_available
The platform has no API for this (e.g., iOS device_attestation at Premium)
Structural limit — accept, or apply a platform-specific policy
wallet_not_supported
The wallet doesn't implement Blerify extensions
Capability gap — accept Basic or require a compatible wallet
user_declined
The user denied a required permission (camera for liveness, biometric prompt)
Active refusal — a different risk signal from a platform limitation
failed
The check was attempted but rejected (invalid token, spoof detected)
Treat as a strong fraud signal
attestation_not_supported
Older Android device — key is hardware-backed but cryptographic attestation is unavailable
Likely a legitimate older device — accept or downgrade per your policy
The distinction between platform_not_available and user_declined carries meaningfully different risk implications. Handle them separately in your policy logic.
Polling strategy
Poll every 1–2 seconds. Stop on COMPLETED, FAILED, or HTTP 410.
Typical session duration: 5–15 seconds for Basic and Standard; 15–30 seconds for Premium (the liveness capture adds time).
If you haven't received a terminal status after your UX timeout (2 minutes for same-device flows, 5 minutes for cross-device), stop polling and offer the user a "try again" button that starts a fresh session. The same transaction_id cannot be reused.
Next steps
Continue to Verify a Credential — the complete session lifecycle, how to read every evidence field, and exhaustive error handling including negative credential outcomes.
See also: Platform Attestation · Assurance Tiers · Biometric Binding · Full OpenAPI spec
Last updated

