# Biometrics Flow

Guide customers through identity verification, track progress, and handle each outcome.

## Overview

After [creating a customer](/en-us/guides/customer-registration), two processes run in parallel:

1. **Credit analysis** — Dinie automatically starts evaluating the company's credit profile.
2. **KYC verification** — the customer needs to submit documents and complete biometrics at their own pace.


You don't need to wait for the credit analysis to start the verification. As soon as the customer reaches `pending_kyc` status, present the option to start biometrics.

## 1. Present the Verification to the Customer

When the `customer.created` webhook arrives (or when the status changes to `pending_kyc`), present the customer with the option to start verification. You can show an explicit button or start automatically.

![Flow from registration to biometrics session](/assets/flow-start-verification.93dfa08d0b6edfe2a811752438ba1f4a979bb096586f099406a0fc32e0c6ee76.047687d6.svg)

The key is to communicate that the customer needs to complete identity verification to receive credit offers.

## 2. Create the Biometrics Session

When the customer confirms they want to start, create a biometrics session:


```typescript Node.js
const session = await client.customers.createBiometricsSession(customer.id);

// Open this URL in a webview or redirect the customer
console.log(session.session_url);
console.log(session.expires_at); // Expires in 30 minutes
```


```ruby Ruby
session = client.customers.create_biometrics_session(customer.id)

# Open this URL in a webview or redirect the customer
puts session.session_url
puts session.expires_at # Expires in 30 minutes
```


```python Python
session = client.customers.create_biometrics_session(customer.id)

# Open this URL in a webview or redirect the customer
print(session.session_url)
print(session.expires_at)  # Expires in 30 minutes
```


```bash cURL
curl -X POST "https://sandbox.api.dinie.com.br/v3/customers/${CUSTOMER_ID}/biometrics" \
  -H "Authorization: Bearer dinie_at_..."
```

Open the `session_url` in a **webview** (mobile) or **redirect** the browser (web). The session expires in 30 minutes — if it expires, generate a new one.

> **Tip:** If you already have some of the customer's documents, you can upload them via API before generating the session. Pre-uploaded documents will already appear as complete in the session, speeding up the process for the customer. See [Advance API Uploads](/en-us/guides/kyc-requirements#advance-api-uploads).


## 3. What the Customer Will See

In the biometrics session, the customer will see the list of required documents. Documents already uploaded via API appear as complete. The customer needs to complete the pending items.

![Biometrics session: documents, selfie, and review](/assets/flow-biometrics-session.cd7f9cfac3175708f025b890fc2bcf288b5666847cdde7ce2efcdfa99bb4cfd7.047687d6.svg)

The session flow includes:

- **Document upload** — photos of ID (CNH or RG), proof of address, company documents
- **Selfie** — captured by the customer via the device camera
- **Review** — view everything submitted before confirming


At the end, the customer will have two options:

- **Save for later** — progress is kept, but analysis is not triggered
- **Submit for analysis** — triggers verification of all documents


## 4. Save for Later

If the customer chooses "Save for later", they will return to your environment. No analysis is triggered at this point — the customer can come back at any time to continue.

You can track progress in two ways:

**Via webhook:** receive `customer.kyc_updated` whenever a document is uploaded. The payload includes the updated `kyc` array.

**Via API:** query the customer and check the `kyc` field:


```typescript Node.js
const customer = await client.customers.get(customerId);

for (const req of customer.kyc) {
  const status = req.submitted ? "Submitted" : "Pending";
  console.log(`${req.label}: ${status}`);
}
```


```ruby Ruby
customer = client.customers.get(customer_id)

customer.kyc.each do |req|
  status = req["submitted"] ? "Submitted" : "Pending"
  puts "#{req["label"]}: #{status}"
end
```


```python Python
customer = client.customers.get(customer_id)

for req in customer.kyc:
    status = "Submitted" if req["submitted"] else "Pending"
    print(f"{req['label']}: {status}")
```


```bash cURL
curl "https://sandbox.api.dinie.com.br/v3/customers/${CUSTOMER_ID}" \
  -H "Authorization: Bearer dinie_at_..."

# Check customer.kyc[].submitted for each requirement
```

Show the customer their verification progress — which documents have been submitted and which are still pending. To let the customer resume, just generate a new biometrics session. Previous progress is preserved.

![Save, track, and resume flow](/assets/flow-save-resume.c73f4727390691c7993309c7d57f43b851ceb09e8449e2fa60680e5377a4143c.047687d6.svg)

## 5. Submit for Analysis

When the customer clicks "Submit for analysis" in the biometrics session, Dinie starts verifying all documents. You will receive the `customer.under_review` webhook, and the customer's status will change to `under_review`.


```typescript Node.js
app.post("/webhooks/dinie", express.raw({ type: "application/json" }), (req, res) => {
  const event = client.webhooks.unwrap(req.body.toString(), req.headers);

  if (event.type === "customer.under_review") {
    updateCustomerScreen(event.data.external_id, "Under review");
  }

  res.sendStatus(200);
});
```


```ruby Ruby
post "/webhooks/dinie" do
  event = client.webhooks.unwrap(request.body.read, request.env)

  if event["type"] == "customer.under_review"
    update_customer_screen(event.dig("data", "external_id"), "Under review")
  end

  status 200
end
```


```python Python
@app.post("/webhooks/dinie")
async def handle_webhook(request: Request):
    event = client.webhooks.unwrap(await request.body(), request.headers)

    if event["type"] == "customer.under_review":
        update_customer_screen(event["data"]["external_id"], "Under review")

    return Response(status_code=200)
```


```bash cURL
# Webhook received at your endpoint:
# {
#   "type": "customer.under_review",
#   "data": {
#     "id": "cust_550e8400...",
#     "external_id": "partner-ref-123",
#     "status": "under_review"
#   }
# }
```

From here, Dinie can approve, deny, or flag specific documents for correction.

## 6. Outcome: Approved

If all documents are approved, the customer receives `active` status and you receive the `customer.active` webhook.

This **does not mean** credit offers are ready. Credit analysis runs in parallel and may take longer. Once the customer is `active`, Dinie continues processing the credit analysis.

When an offer is available, you will receive the `credit_offer.available` webhook:


```typescript Node.js
app.post("/webhooks/dinie", express.raw({ type: "application/json" }), (req, res) => {
  const event = client.webhooks.unwrap(req.body.toString(), req.headers);

  if (event.type === "customer.active") {
    updateCustomerScreen(event.data.external_id, "Verified");
  }

  if (event.type === "credit_offer.available") {
    const offers = await client.creditOffers.list(event.data.customer_id);
    notifyCustomer(event.data.external_id, offers);
  }

  res.sendStatus(200);
});
```


```ruby Ruby
post "/webhooks/dinie" do
  event = client.webhooks.unwrap(request.body.read, request.env)

  case event["type"]
  when "customer.active"
    update_customer_screen(event.dig("data", "external_id"), "Verified")
  when "credit_offer.available"
    offers = client.credit_offers.list(event.dig("data", "customer_id"))
    notify_customer(event.dig("data", "external_id"), offers)
  end

  status 200
end
```


```python Python
@app.post("/webhooks/dinie")
async def handle_webhook(request: Request):
    event = client.webhooks.unwrap(await request.body(), request.headers)

    if event["type"] == "customer.active":
        update_customer_screen(event["data"]["external_id"], "Verified")

    if event["type"] == "credit_offer.available":
        offers = client.credit_offers.list(event["data"]["customer_id"])
        notify_customer(event["data"]["external_id"], offers)

    return Response(status_code=200)
```


```bash cURL
# Webhook customer.active:
# { "type": "customer.active", "data": { "status": "active", ... } }

# Webhook credit_offer.available:
# { "type": "credit_offer.available", "data": { "approved_amount": 50000.00, ... } }

# List offers:
curl "https://sandbox.api.dinie.com.br/v3/customers/${CUSTOMER_ID}/credit-offers" \
  -H "Authorization: Bearer dinie_at_..."
```

![Waiting for offers and offer available](/assets/flow-approved-offers.9fd27113db4f3474b82c79b298486fe36c7f3dc7c6eace4b9e03e2407894a4db.047687d6.svg)

> **Info:** Between `customer.active` and `credit_offer.available`, show the customer a message like "Verification complete, we're finding the best offers for you". This conveys that the process is still moving forward.


## 7. Outcome: Documents Pending

If Dinie finds issues with any document, you will receive the `customer.kyc_updated` webhook with details. Check the `kyc` field to see which documents were rejected:


```typescript Node.js
const customer = await client.customers.get(customerId);

for (const req of customer.kyc) {
  if (req.submitted?.review_status === "rejected") {
    console.log(`${req.label}: Rejected — ${req.submitted.review_reason}`);
  }
}
```


```ruby Ruby
customer = client.customers.get(customer_id)

customer.kyc.each do |req|
  submitted = req["submitted"]
  if submitted && submitted["review_status"] == "rejected"
    puts "#{req["label"]}: Rejected — #{submitted["review_reason"]}"
  end
end
```


```python Python
customer = client.customers.get(customer_id)

for req in customer.kyc:
    submitted = req.get("submitted")
    if submitted and submitted.get("review_status") == "rejected":
        print(f"{req['label']}: Rejected — {submitted['review_reason']}")
```


```bash cURL
curl "https://sandbox.api.dinie.com.br/v3/customers/${CUSTOMER_ID}" \
  -H "Authorization: Bearer dinie_at_..."

# Check customer.kyc[].submitted.review_status and review_reason
```

Show the customer which documents need correction and the rejection reason. You don't need to do anything directly via the API — just present a button for the customer to open the biometrics session again:


```typescript Node.js
// Generate a new session for the customer to fix documents
const session = await client.customers.createBiometricsSession(customer.id);
redirectCustomer(session.session_url);
```


```ruby Ruby
# Generate a new session for the customer to fix documents
session = client.customers.create_biometrics_session(customer.id)
redirect_customer(session.session_url)
```


```python Python
# Generate a new session for the customer to fix documents
session = client.customers.create_biometrics_session(customer.id)
redirect_customer(session.session_url)
```


```bash cURL
# Generate a new session
curl -X POST "https://sandbox.api.dinie.com.br/v3/customers/${CUSTOMER_ID}/biometrics" \
  -H "Authorization: Bearer dinie_at_..."
```

In the session, rejected documents will appear highlighted with the rejection reason. The customer fixes them, resubmits, and the cycle repeats until all are approved.

![Document correction flow](/assets/flow-documents-pending.4b5ca813494ad2787378318755dc7ea2480f228b1b4823246f4b15ace1167ca3.047687d6.svg)

## 8. Outcome: Denied

If the customer is permanently denied, you will receive the `customer.denied` webhook. This means Dinie could not approve the customer in the KYC analysis (e.g., fraud detected or company closed).

In this case, inform the customer that **credit is not available at this time**. Unlike the approved-without-offers scenario (where the customer may receive offers in the future), the `denied` status is final.

![Evaluation and denied outcome](/assets/flow-denied.556e249c1276b85997fb898bb42584f0246b1b1027270bdd28395c32fd42f1be.047687d6.svg)

> **Warning:** Do not communicate the specific denial reason to the customer. Only inform them that it was not possible to proceed with the analysis at this time.


## Flow Summary

| Step | Partner action | API / Webhook |
|  --- | --- | --- |
| Customer created | Show verification button | `customer.created` webhook |
| Customer starts | Create biometrics session | `POST biometrics` |
| Customer in session | Wait | — |
| Save for later | Show progress | `customer.kyc_updated` webhook or `GET customer` |
| Submit for analysis | Show "under review" | `customer.under_review` webhook |
| Approved | Wait for offers | `customer.active` + `credit_offer.available` webhooks |
| Document rejected | Show reason + button to fix | `customer.kyc_updated` webhook |
| Denied | Inform customer | `customer.denied` webhook |


## Related Guides

- [Registration & Offers](/en-us/guides/customer-registration) for the full customer creation flow
- [KYC Requirements](/en-us/guides/kyc-requirements) for details on document types
- [Simulation & Origination](/en-us/guides/credit-workflow) for the next step after receiving offers
- [Webhooks](/en-us/apis/concepts/webhooks) to configure notification endpoints