Embedded Biometric Session
curl --request POST \
--url http://api.gu1.ai/api/kyc/biometric/sessions \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"entityId": "<string>",
"entityExternalId": "<string>",
"entityTaxId": "<string>",
"webhookUrl": "<string>",
"callback": "<string>",
"language": "<string>",
"workflowId": "<string>"
}
'import requests
url = "http://api.gu1.ai/api/kyc/biometric/sessions"
payload = {
"entityId": "<string>",
"entityExternalId": "<string>",
"entityTaxId": "<string>",
"webhookUrl": "<string>",
"callback": "<string>",
"language": "<string>",
"workflowId": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
entityId: '<string>',
entityExternalId: '<string>',
entityTaxId: '<string>',
webhookUrl: '<string>',
callback: '<string>',
language: '<string>',
workflowId: '<string>'
})
};
fetch('http://api.gu1.ai/api/kyc/biometric/sessions', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "http://api.gu1.ai/api/kyc/biometric/sessions",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'entityId' => '<string>',
'entityExternalId' => '<string>',
'entityTaxId' => '<string>',
'webhookUrl' => '<string>',
'callback' => '<string>',
'language' => '<string>',
'workflowId' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "http://api.gu1.ai/api/kyc/biometric/sessions"
payload := strings.NewReader("{\n \"entityId\": \"<string>\",\n \"entityExternalId\": \"<string>\",\n \"entityTaxId\": \"<string>\",\n \"webhookUrl\": \"<string>\",\n \"callback\": \"<string>\",\n \"language\": \"<string>\",\n \"workflowId\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("http://api.gu1.ai/api/kyc/biometric/sessions")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"entityId\": \"<string>\",\n \"entityExternalId\": \"<string>\",\n \"entityTaxId\": \"<string>\",\n \"webhookUrl\": \"<string>\",\n \"callback\": \"<string>\",\n \"language\": \"<string>\",\n \"workflowId\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("http://api.gu1.ai/api/kyc/biometric/sessions")
http = Net::HTTP.new(url.host, url.port)
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"entityId\": \"<string>\",\n \"entityExternalId\": \"<string>\",\n \"entityTaxId\": \"<string>\",\n \"webhookUrl\": \"<string>\",\n \"callback\": \"<string>\",\n \"language\": \"<string>\",\n \"workflowId\": \"<string>\"\n}"
response = http.request(request)
puts response.read_bodyBiometric
Embedded Biometric Session
Start a hosted biometric re-authentication session after approved KYC β iframe-ready session URL, webhooks, and Gu1 final verdict.
POST
/
api
/
kyc
/
biometric
/
sessions
Embedded Biometric Session
curl --request POST \
--url http://api.gu1.ai/api/kyc/biometric/sessions \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"entityId": "<string>",
"entityExternalId": "<string>",
"entityTaxId": "<string>",
"webhookUrl": "<string>",
"callback": "<string>",
"language": "<string>",
"workflowId": "<string>"
}
'import requests
url = "http://api.gu1.ai/api/kyc/biometric/sessions"
payload = {
"entityId": "<string>",
"entityExternalId": "<string>",
"entityTaxId": "<string>",
"webhookUrl": "<string>",
"callback": "<string>",
"language": "<string>",
"workflowId": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
entityId: '<string>',
entityExternalId: '<string>',
entityTaxId: '<string>',
webhookUrl: '<string>',
callback: '<string>',
language: '<string>',
workflowId: '<string>'
})
};
fetch('http://api.gu1.ai/api/kyc/biometric/sessions', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "http://api.gu1.ai/api/kyc/biometric/sessions",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'entityId' => '<string>',
'entityExternalId' => '<string>',
'entityTaxId' => '<string>',
'webhookUrl' => '<string>',
'callback' => '<string>',
'language' => '<string>',
'workflowId' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "http://api.gu1.ai/api/kyc/biometric/sessions"
payload := strings.NewReader("{\n \"entityId\": \"<string>\",\n \"entityExternalId\": \"<string>\",\n \"entityTaxId\": \"<string>\",\n \"webhookUrl\": \"<string>\",\n \"callback\": \"<string>\",\n \"language\": \"<string>\",\n \"workflowId\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("http://api.gu1.ai/api/kyc/biometric/sessions")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"entityId\": \"<string>\",\n \"entityExternalId\": \"<string>\",\n \"entityTaxId\": \"<string>\",\n \"webhookUrl\": \"<string>\",\n \"callback\": \"<string>\",\n \"language\": \"<string>\",\n \"workflowId\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("http://api.gu1.ai/api/kyc/biometric/sessions")
http = Net::HTTP.new(url.host, url.port)
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"entityId\": \"<string>\",\n \"entityExternalId\": \"<string>\",\n \"entityTaxId\": \"<string>\",\n \"webhookUrl\": \"<string>\",\n \"callback\": \"<string>\",\n \"language\": \"<string>\",\n \"workflowId\": \"<string>\"\n}"
response = http.request(request)
puts response.read_bodyOverview
Embedded Biometric lets you re-verify that the person completing a flow is the same individual who passed KYC earlier. Unlike the synchronous Biometric Check (where your app uploads a selfie), this flow uses a hosted capture UI that Gu1 returns assessionUrl. Your app embeds that URL in an iframe (or opens a redirect) so liveness and face capture run in a controlled environment.
Use
status from the API or webhook as the biometric result. Gu1 runs the full session lifecycle and may set rejected after organization policies (cross-entity checks, face-match score thresholds).Prerequisites
- Approved KYC in Gu1 for the person entity (
status: approvedon a session-based validation). - Reference portrait available from that KYC (selfie stored when the validation was approved). Required for session create (
NO_PORTRAITif missing). - KYC enabled for your organization (same API key and permissions as session-based validation). Gu1 provisions the internal hosted-capture configuration; integrators do not set workflows or extra secrets. If your org is not provisioned yet, create may return
BIOMETRIC_WORKFLOW_NOT_CONFIGURED(400) β contact your Gu1 account team. - Gu1 Biometric active for your organization (
global_gueno_biometric_kyc). If not enabled, create returnsNOT_ENABLED(403) β request activation from Gu1.
Flow
Create Session
POST https://api.gu1.ai/api/kyc/biometric/sessions
Body
Provide exactly one entity identifier:string
UUID of the person entity with approved KYC.
string
Your external ID for the entity (
entities.externalId in Gu1).string
Tax or document number (CUIT, CPF, DNI, etc.). Gu1 resolves it with normalized match on
entities.tax_id. The entity must exist in Gu1 (404 if not found).Sandbox entity preview:
GET /api/entities/by-tax-id/{taxId} may return a synthetic person (sandboxMock: true, id: null) for catalog test document numbers when no real row exists. That preview is read-only β you still need a persisted entity before POST /sessions.string
Optional HTTPS endpoint Gu1 POSTs to on terminal statuses (
approved, rejected, abandoned, expired). Signed with your KYC webhook secret when configured.string
Optional redirect URL after the user finishes in the hosted UI.
string
UI language, e.g.
es, en, pt.string
Optional override for the biometric workflow ID (otherwise from org KYC settings).
Response 201
{
"id": "bio-session-uuid",
"entityId": "entity-uuid",
"status": "pending",
"sessionUrl": "https://verify.example.com/session/...",
"iframeAllow": "camera; microphone; fullscreen; autoplay; encrypted-media",
"mode": "face_match",
"hostedSessionId": "hosted-session-id"
}
In sandbox mock,
status may be approved or rejected immediately (no iframe). See Sandbox mock β Embedded biometric.Recommended Integration Flow
- Ensure the person entity has KYC
approved(session-based validation). - Call
POST /api/kyc/biometric/sessionsonce per user intent. Storeid,sessionUrl, andhostedSessionId. - If the response is
409 ACTIVE_SESSION_EXISTS, cancel withPOST .../sessions/{activeSessionId}/cancel, then create again (do not retry in a loop without handling 409). - If
statusispending, embedsessionUrlin an iframe (or redirect). - Wait for
biometric.session_*webhooks (or pollGET .../sessions/:id/POST .../syncif needed). - Use
statusand optionalrejectionCodeas the final biometric result.
Do not treat repeated
POST /sessions as the normal path. Retries after timeouts or double-clicks can hit idempotency edge cases. Always handle 409, cancel active sessions explicitly, and prefer webhooks over aggressive polling.Create Errors (POST /sessions)
| HTTP | error | When | What to do |
|---|---|---|---|
| 400 | NO_KYC | No approved KYC for the entity | Complete KYC first |
| 400 | NO_PORTRAIT | Approved KYC has no usable reference selfie (real flow) | Re-run KYC with valid capture, or use sandbox mock |
| 400 | KYC_NOT_CONFIGURED | Org KYC credentials missing/disabled | Configure KYC in org settings |
| 400 | ENTITY_NOT_FOUND | Invalid entityId or wrong org | Fix entity / X-Organization-ID |
| 400 | INVALID_ENTITY_TYPE | Entity is not person | Use a person entity |
| 403 | NOT_ENABLED | global_gueno_biometric_kyc not active | Contact Gu1 to enable biometrics |
| 402 | INSUFFICIENT_CREDITS_FOR_KYC | Billing hold failed | Top up credits / pack |
| 409 | ACTIVE_SESSION_EXISTS | Latest session is pending or in_progress | Cancel activeSessionId, then create again |
| 502 | PROVIDER_ERROR | Gu1 could not start hosted capture | Retry later; use the error code and HTTP status |
| 500 | CREATION_FAILED | Unexpected persistence failure | Check logs; cancel any stuck session; retry once after fix |
409 ACTIVE_SESSION_EXISTS (additive β existing clients that ignore extra fields are unchanged):
{
"error": "ACTIVE_SESSION_EXISTS",
"message": "Cannot create new biometric session. There is already a pending session for this entity. Please cancel or complete the existing session first.",
"activeSessionId": "7618e10c-b1b1-408b-b361-1901077ced73"
}
POST /sessions while a session is still open always returns this 409. Gu1 does not return 201 with the same pending session.
Prior Sessions and βCurrentβ Biometric
An entity may have many biometric sessions over time (approved, rejected, cancelled, etc.).| API | Meaning |
|---|---|
GET .../entities/:entityId/current | Latest created session (any status) β use to recover a pending iframe or read the last attempt |
GET .../entities/by-tax-id/:taxId/current | Same as above, resolved by entityTaxId |
GET .../entities/by-external-id/:externalId/current | Same as above, resolved by entityExternalId |
GET .../sessions?entityId=... (or entityTaxId / entityExternalId) β currentSessionId | Latest approved session β active step-up verification for the entity |
List data[] | Full history ordered by createdAt |
pending / in_progress) triggers 409 ACTIVE_SESSION_EXISTS.
hostedSessionId, Cancel, and Retries
hostedSessionId is the hosted capture session reference returned at create. Gu1 stores it uniquely and uses it for webhooks and sync.
Correct recovery:
409 β POST .../sessions/{activeSessionId}/cancel β POST .../sessions (once)
hostedSessionId is seen again) applies only after cancel or other terminal outcomes β not while a session is still pending or in_progress.
Sandbox mock: hostedSessionId is prefixed with sandbox-mock-bio- and each mock create gets a new ID.
Embed in Your App
<iframe
src="{{ sessionUrl }}"
style="width: 100%; height: 700px; border: none;"
allow="{{ iframeAllow }}"
></iframe>
sessionUrl, or a compatible Web SDK initialized with the same URL.
Poll or Sync Status
GET /api/kyc/biometric/sessions/:idβ one session by IDGET /api/kyc/biometric/entities/:entityId/currentβ current session = most recent bycreatedAt(any status). Returns200withnullif none.GET /api/kyc/biometric/entities/by-tax-id/:taxId/currentβ same, resolved by tax IDGET /api/kyc/biometric/entities/by-external-id/:externalId/currentβ same, resolved by external IDGET /api/kyc/biometric/sessions?entityId=...β list; also acceptsentityTaxIdorentityExternalId(one at a time).currentSessionIdis the latestapprovedsession.POST /api/kyc/biometric/sessions/:id/syncβ refresh session state from Gu1 if webhooks are delayed.POST /api/kyc/biometric/sessions/:id/cancelβ manually cancelpendingorin_progresssessions (markscancelledin Gu1).
POST /sessions returns 409 ACTIVE_SESSION_EXISTS, the body includes activeSessionId so you can cancel without calling /current.
The platform may report In Review while capture is reviewed; Gu1 maps that to
in_progress until the final verdict (approved or rejected). Only terminal outcome states define the entity current session.Webhooks
Two channels:- Per-request
webhookUrlβ only if you sent it inPOST /sessions; fires on terminal status. - Organization webhooks β subscribe to
biometric.session_*events in Webhook configuration. See Biometric webhook events.
Comparison with Synchronous Biometric
POST /api/kyc/biometric | POST /api/kyc/biometric/sessions | |
|---|---|---|
| Capture | Your app sends image | Hosted UI (iframe) |
| Fraud resistance | Lower | Higher (liveness in hosted UI) |
| Async | No | Yes |
webhookUrl per request | No | Yes |
Sandbox Mock Sessions
In sandbox, when the entity has an approved KYC mock (testtaxId such as 99990001, or KYC with metadata.sandboxMock: true), POST /api/kyc/biometric/sessions can return an immediate mock result (approved or rejected) without hosted capture.
- Default:
99990001β biometric approved;99990011β biometric rejected (KYC still approved). - Override on any eligible entity:
"metadata": { "sandboxMockOutcome": "rejected" }. - Mock responses use
hostedSessionIdprefixed withsandbox-mock-bio-and may returnstatus: approvedorrejectedin the create response (not onlypending).
Was this page helpful?