> ## Documentation Index
> Fetch the complete documentation index at: https://docs.privue.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Try a hosted workflow

> Run a hosted verification end to end yourself, walking the journey in your own browser.

For trying the API. You play both parts: your backend, from a Jupyter notebook, and the user, in a
browser tab. Every block below is a notebook cell, run in order. Nothing here is an integration
pattern, just the shortest path to seeing a real record.

You need a workflow whose `mode` is `hosted` and a mobile number you can receive a code on.

Reach for your `uat` key, whose token begins `uat_`, so the runs below come out of a `uat` workflow's
credits rather than out of what you bought for real work. Whichever key you use, the keys below are
placeholders for your own, and a `uat` workflow's key ends `-uat`.

The verification is real either way. There is no sandbox: your documents really are read, and really
are confirmed with the bodies that issued them.

```python theme={"system"}
!pip install httpx
```

```python theme={"system"}
import os
import time

import httpx

os.environ["PRIVUE_API_KEY"] = "your key"

privue = httpx.Client(
    base_url="https://api.verify.privue.ai",
    headers={"Authorization": f"Bearer {os.environ['PRIVUE_API_KEY']}"},
    timeout=60,
)
```

<Note>
  Already keep the key in your environment? Drop the assignment and the client
  picks it up from the kernel. Nothing but the client reads it, so a plain
  `api_key = "your key"` variable does the job just as well.
</Note>

Every error comes back as `{"detail": "..."}`. A cell that fails raises on the response it already
kept in `response`, so `response.json()["detail"]` in a new cell says what went wrong.

## 1. See which workflows you have

```python theme={"system"}
response = privue.get("/workflows")
response.raise_for_status()

for workflow in response.json()["workflows"]:
    left = workflow["credits"]["remaining"]
    runs = workflow["runs"]
    room = f"{runs['live']}/{runs['limit']} going"
    print(f"{workflow['key']:28} {workflow['mode']:12} {workflow['step_count']:2} steps  {left:4} credits  {room}")
```

```text theme={"system"}
merchant-onboarding          hosted       12 steps   288 credits  17/25 going
vendor-verification          staged        5 steps     0 credits  3/25 going
```

Only one environment's workflows are listed: whichever your key is for. Pick a `hosted` key. A `staged`
workflow is walked by nobody, so the journey below does not apply to it; see
[the other recipe](/verification/recipes/staged).

`credits` and `runs` are the two limits a verification is held to. If the workflow you picked reads `0`
credits, everything below is refused with `402` until we top it up, so check this before going on. See
[Credits and limits](/verification/credits).

## 2. Create a verification

Use a mobile number you can receive a code on, because you are about to sign in as this user.

```python theme={"system"}
response = privue.post(
    "/verifications",
    json={
        "workflow_key": "merchant-onboarding",
        "mobile": "+919876543210",
        "reference_user_id": "test-001",
    },
)
response.raise_for_status()
verification = response.json()
verification_id = verification["id"]

print(verification_id)
print(verification["journey_url"])
```

<Note>
  A real integration also sends `return_url`, which is where the journey drops
  the user once they submit. It has to be one of the URLs registered for your
  account, so leave it out while you are just trying this: the journey ends on
  its own screen instead.
</Note>

## 3. Walk the journey yourself

Open that `journey_url` in a browser. Enter the same mobile number, we send you a code, and you work
through the steps as your user would.

Leave the notebook where it is; you come back to it in step 4.

<Note>
  To see what a handoff does instead, mint one and paste it into a fresh browser tab. It opens the
  journey already signed in, so no code is asked for. It is single use, so mint another for the next
  tab.

  ```python theme={"system"}
  response = privue.post(f"/verifications/{verification_id}/handoff")
  response.raise_for_status()
  print(response.json()["url"])
  ```

  Mint it before you start polling in step 4: that cell holds the kernel, so nothing else runs until it
  stops.
</Note>

## 4. Watch it settle

Back in the notebook. Poll until the verification leaves `open`.

```python theme={"system"}
for _ in range(60):
    response = privue.get(f"/verifications/{verification_id}")
    response.raise_for_status()
    record = response.json()
    print(record["status"], record["progress"])
    if record["status"] != "open":
        break
    time.sleep(5)
```

Run this while you are still working through the journey and you will see the counts move as each step
settles.

The cell holds the kernel while it polls, and stops after five minutes rather than running for ever.
Still `open` when it stops, because you are still filling the journey in? Run it again: reading a
verification changes nothing, and the last `record` it read is what step 5 reads.

## 5. Read what it found

```python theme={"system"}
for step in record["steps"]:
    notes = [note["message"] for note in step["reasons"] + step["gaps"]]
    print(f"{step['key']:22} {step['state']:14} {' | '.join(notes)}")
```

A step that `passed` with a **gap** stood up without something optional, such as a name that matched
closely without matching exactly, and is the case worth looking at by hand. A step that `failed`
carries the **reasons** it did not stand up. A step in `error` was never checked, because a source
could not be reached.

## 6. Go round again

Reset clears every answer and file and reopens the journey, so you can walk it again with different
documents without creating a new verification.

```python theme={"system"}
response = privue.post(f"/verifications/{verification_id}/reset")
response.raise_for_status()
```

Creating is idempotent per mobile number and workflow, so calling step 2 again returns this same
verification rather than a fresh one, answering `200` instead of `201` to tell you so. Reset is how you
get a clean run.

A reset runs the subject through again, so it uses a credit of its own when the run it opens ends. It
also draws one of the workflow's resets where the run it cleared had not ended, which in testing is
most of them. `credits_consumed` on the record counts every run of it, which is how a round of
testing shows up:

```python theme={"system"}
response = privue.get(f"/verifications/{verification_id}")
response.raise_for_status()
print(response.json()["credits_consumed"])  # 1 after the first completed run
```

## 7. Clean up

When you are done testing, purge the verification. Everything you and the user provided is deleted, and
the record stays readable saying what each step concluded and that you asked for the purge.

```python theme={"system"}
response = privue.post(f"/verifications/{verification_id}/purge")
response.raise_for_status()
purged = response.json()
print(purged["status"], purged["expired_by"])  # expired client
```

## The record you just read

A finished hosted record, from a merchant-onboarding workflow that exercises eleven of the sixteen
step types. Yours differs: which steps a verification runs, and the field names inside a `form` step,
come from the workflow configured for your integration. The envelope around them is the same for
everyone.

<Accordion title="GET /verifications/{verification_id}">
  ```json theme={"system"}
  {
    "id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
    "mode": "hosted",
    "status": "completed",
    "workflow_key": "merchant-onboarding",
    "version": 2,
    "mobile": "+919876543210",
    "reference_user_id": "merchant-42",
    "context": {
      "channel": "field-sales",
      "state": "MH"
    },
    "return_url": "https://yourapp.com/kyc/done",
    "journey_url": "https://verify.privue.ai/acme/merchant-onboarding",
    "created_at": "2026-08-10T14:02:11",
    "run_requested_at": null,
    "submitted_at": "2026-08-10T14:41:06",
    "completed_at": "2026-08-10T14:41:09",
    "cancelled_at": null,
    "gated_at": null,
    "gated_by_step": null,
    "failed_at": null,
    "expired_at": null,
    "expired_by": null,
    "last_activity_at": "2026-08-10T14:41:06",
    "expires_after_days": 30,
    "acknowledgement": null,
    "callback": {
      "status": "delivered",
      "settled_at": "2026-08-10T14:41:09",
      "reason": null
    },
    "credits_consumed": 1,
    "resets": 0,
    "progress": {
      "total": 12,
      "passed": 10,
      "failed": 0,
      "awaiting": 0,
      "enriching": 0,
      "error": 0,
      "declined": 1,
      "not_supplied": 0,
      "undetermined": 0,
      "not_applicable": 1
    },
    "steps": [
      {
        "key": "contact",
        "title": "Before we start",
        "label": "contact_email",
        "type": "form",
        "required": true,
        "state": "passed",
        "reasons": [],
        "gaps": [],
        "collected": {
          "answer": {
            "email": "owner@acmetraders.in"
          },
          "outputs": {
            "email": "owner@acmetraders.in"
          }
        },
        "documents": []
      },
      {
        "key": "gst",
        "title": "GST certificate",
        "label": "tax_registration",
        "type": "gst-certificate",
        "required": true,
        "state": "passed",
        "reasons": [],
        "gaps": [],
        "collected": {
          "answer": {},
          "outputs": {
            "gstin": "27AABCA1234F1Z5",
            "legal_name": "ACME TRADERS PRIVATE LIMITED",
            "trade_name": "Acme Traders",
            "registration_status": "Active",
            "registration_date": "2020-01-13",
            "business_constitution": "Private Limited Company",
            "taxpayer_type": "Regular",
            "e_invoice_mandated": true,
            "aggregate_turnover": "Slab: Rs. 5 Cr. to 25 Cr.",
            "authorized_signatories": [
              "Rahul Mehta",
              "Priya Mehta"
            ],
            "nature_of_business_activities": [
              "Wholesale Business",
              "Warehouse / Depot"
            ],
            "business_details": {
              "goods_details": [
                {
                  "goods_description": "Filtering machinery",
                  "hsn_code": "84212190"
                }
              ],
              "service_details": []
            },
            "filings": [
              {
                "return_type": "GSTR1",
                "financial_year": "2026-2027",
                "tax_period": "May",
                "status": "Filed",
                "filing_date": "2026-06-04",
                "mode_of_filing": "ONLINE"
              },
              {
                "return_type": "GSTR3B",
                "financial_year": "2026-2027",
                "tax_period": "May",
                "status": "Filed",
                "filing_date": "2026-06-23",
                "mode_of_filing": "ONLINE"
              },
              {
                "return_type": "GSTR9",
                "financial_year": "2024-2025",
                "tax_period": "Annual",
                "status": "Filed",
                "filing_date": "2025-12-18",
                "mode_of_filing": "ONLINE"
              }
            ],
            "business_pan": "AABCA1234F",
            "sole_proprietor_pan": null,
            "addresses": [
              {
                "address": "12 Industrial Estate, Pune, Maharashtra 411026",
                "tag": "Principal Business Address"
              }
            ],
            "primary_business_address": {
              "street": "12 Industrial Estate",
              "city": "Pune",
              "state": "Maharashtra",
              "country": "India",
              "postal_code": "411026"
            },
            "other_business_address": null,
            "names": [
              {
                "name": "ACME TRADERS PRIVATE LIMITED",
                "tag": "Legal Name"
              },
              {
                "name": "Acme Traders",
                "tag": "Trade Name"
              }
            ],
            "promoters": [
              "Rahul Mehta",
              "Priya Mehta"
            ],
            "business_email": "accounts@acmetraders.in",
            "business_mobile": "9820011223"
          }
        },
        "documents": [
          {
            "id": "bb9909b4-44e4-414a-af82-bad690d434ed",
            "slot": "gst-certificate",
            "kind": "gst-certificate",
            "label": "gst_certificate",
            "filename": "gst-certificate.pdf",
            "content_type": "application/pdf",
            "size_bytes": 182044,
            "sha256": "63f90f32e899197edb4f038322e354f3c6b907602a44a58a0ae52b40a2cd9811",
            "received_at": "2026-08-10T14:21:58"
          }
        ]
      },
      {
        "key": "pan",
        "title": "PAN card",
        "label": "entity_pan",
        "type": "pan-card",
        "required": true,
        "state": "passed",
        "reasons": [],
        "gaps": [],
        "collected": {
          "answer": {},
          "outputs": {
            "pan": "AABCA1234F",
            "name": "ACME TRADERS PRIVATE LIMITED",
            "date_of_birth_or_incorporation": "2011-06-14",
            "card_document_id": "4fd79a54-a8a7-4e07-b902-a2e75af3e467",
            "status": "valid",
            "aadhaar_seeded": null,
            "names": [
              {
                "name": "ACME TRADERS PRIVATE LIMITED",
                "tag": "PAN Holder"
              }
            ]
          }
        },
        "documents": [
          {
            "id": "4fd79a54-a8a7-4e07-b902-a2e75af3e467",
            "slot": "pan-card",
            "kind": "pan-card",
            "label": "entity_pan",
            "filename": "pan-card.jpg",
            "content_type": "image/jpeg",
            "size_bytes": 92412,
            "sha256": "42b7e28cf9a26dcef6e275de42f7516ad36ee86c6aa45888821a81217e62a670",
            "received_at": "2026-08-10T14:09:12"
          }
        ]
      },
      {
        "key": "identity",
        "title": "Identity",
        "label": null,
        "type": "digilocker",
        "required": false,
        "state": "passed",
        "reasons": [],
        "gaps": [
          {
            "code": "digilocker-document-missing",
            "message": "Your pan document was not shared from DigiLocker.",
            "details": {
              "doc_type": "pan"
            }
          }
        ],
        "collected": {
          "answer": {
            "session_id": "e0d4a1f6-7c93-4b28-9f51-3a8c62d0be47"
          },
          "outputs": {
            "name": "Rahul Mehta",
            "names": [
              {
                "name": "Rahul Mehta",
                "tag": "DigiLocker Holder"
              }
            ],
            "email": "rahul.mehta@example.com",
            "mobile": "9876543210",
            "date_of_birth": "14/03/1986",
            "gender": "Male",
            "pan": null,
            "masked_aadhaar": "XXXXXXXX4172",
            "photo_document_id": "c48b2e57-9a10-4d3f-b6e8-1f7c9a05d24b"
          }
        },
        "documents": [
          {
            "id": "7e5c1a90-2f83-4a12-9c47-6d0b58e2af31",
            "slot": "digilocker-docs",
            "kind": "aadhaar",
            "label": null,
            "filename": "aadhaar.pdf",
            "content_type": "application/pdf",
            "size_bytes": 148820,
            "sha256": "e17c4b8025d9a63f04e7b158c92da370465f8b1e07c3d924a6b508f1c73e2d05",
            "received_at": "2026-08-10T14:14:37"
          },
          {
            "id": "c48b2e57-9a10-4d3f-b6e8-1f7c9a05d24b",
            "slot": "digilocker-docs",
            "kind": "photo",
            "label": null,
            "filename": "photo.jpg",
            "content_type": "image/jpeg",
            "size_bytes": 18734,
            "sha256": "b8043f7e1c592a6d0e34b871f5c9d206a47e3b9018c5d62faf7b04e39c1a5867",
            "received_at": "2026-08-10T14:14:38"
          }
        ]
      },
      {
        "key": "selfie",
        "title": "Selfie",
        "label": null,
        "type": "selfie",
        "required": false,
        "state": "declined",
        "reasons": [],
        "gaps": [],
        "collected": null,
        "documents": []
      },
      {
        "key": "bank",
        "title": "Bank account",
        "label": "payout_account",
        "type": "bank-account",
        "required": true,
        "state": "passed",
        "reasons": [],
        "gaps": [],
        "collected": {
          "answer": {
            "source": "typed",
            "account_number": "50100234567890",
            "ifsc": "HDFC0001234"
          },
          "outputs": {
            "source": "typed",
            "account_number": "50100234567890",
            "ifsc": "HDFC0001234",
            "registered_name": "ACME TRADERS PRIVATE LIMITED",
            "bank_name": null,
            "branch": null,
            "names": [
              {
                "name": "ACME TRADERS PRIVATE LIMITED",
                "tag": "Bank Account Holder"
              }
            ]
          }
        },
        "documents": []
      },
      {
        "key": "trade-licence",
        "title": "Trade licence",
        "label": null,
        "type": "documents",
        "required": false,
        "state": "passed",
        "reasons": [],
        "gaps": [],
        "collected": {
          "answer": {},
          "outputs": {}
        },
        "documents": [
          {
            "id": "2a6f8d13-4c95-4e7b-8f30-5b1e6c9a7d02",
            "slot": "licence",
            "kind": "licence",
            "label": "trade_licence",
            "filename": "shop-establishment-licence.pdf",
            "content_type": "application/pdf",
            "size_bytes": 221893,
            "sha256": "0a9c7f1b5e2d48a6c3f07b91d4e85a2c6f13b8074de92a5c1f6b3e08d7a24c95",
            "received_at": "2026-08-10T14:27:03"
          },
          {
            "id": "8b31c4e6-7d92-4a58-b0f1-3e6d2c94a17f",
            "slot": "lease",
            "kind": "lease",
            "label": "lease_agreement",
            "filename": "lease-agreement.pdf",
            "content_type": "application/pdf",
            "size_bytes": 654201,
            "sha256": "7d4e1c08b3a95f26d07e4b1a8c53f902e6b7d148a05c39f2b8e6104d7c3a95e2",
            "received_at": "2026-08-10T14:28:44"
          }
        ]
      },
      {
        "key": "premises",
        "title": "Premises photos",
        "label": null,
        "type": "premises-photos",
        "required": false,
        "state": "passed",
        "reasons": [],
        "gaps": [],
        "collected": {
          "answer": {
            "locations": {
              "5c9e07b2-8f41-4d6a-9e35-2b7a1c8f60d4": {
                "latitude": 18.629812,
                "longitude": 73.813104,
                "address": "12, Industrial Estate Rd, Bhosari, Pune, Maharashtra 411026, India",
                "place_id": "ChIJhTQr8kW_wjsR1nQm2eF0hQY"
              },
              "e6142f8a-3b57-4c90-8d21-9a4f7e0b53c6": {
                "latitude": 18.629774,
                "longitude": 73.813251,
                "address": "12, Industrial Estate Rd, Bhosari, Pune, Maharashtra 411026, India",
                "place_id": "ChIJhTQr8kW_wjsR1nQm2eF0hQY"
              }
            }
          },
          "outputs": {
            "addresses": [
              "12, Industrial Estate Rd, Bhosari, Pune, Maharashtra 411026, India"
            ]
          }
        },
        "documents": [
          {
            "id": "5c9e07b2-8f41-4d6a-9e35-2b7a1c8f60d4",
            "slot": "storefront",
            "kind": "storefront",
            "label": "storefront_photo",
            "filename": "storefront.jpg",
            "content_type": "image/jpeg",
            "size_bytes": 1442310,
            "sha256": "c1f5b70e934a26d8071c4be95f3a20d7e846b1c95023f7ad6b48e01c9f735a26",
            "received_at": "2026-08-10T14:33:19"
          },
          {
            "id": "e6142f8a-3b57-4c90-8d21-9a4f7e0b53c6",
            "slot": "interior",
            "kind": "interior",
            "label": "interior_photo",
            "filename": "interior.jpg",
            "content_type": "image/jpeg",
            "size_bytes": 1180955,
            "sha256": "9b0d3e64a17c58f2e40b96d7c85a13f0246e9b7d1c503a8f6b2e74d09c1a5837",
            "received_at": "2026-08-10T14:34:02"
          }
        ]
      },
      {
        "key": "premises-proof",
        "title": "Proof of premises",
        "label": null,
        "type": "address-proof",
        "required": false,
        "state": "passed",
        "reasons": [],
        "gaps": [],
        "collected": {
          "answer": {},
          "outputs": {
            "verified_tags": [
              "Principal Business Address"
            ],
            "unverified_tags": [],
            "readings": [
              {
                "slot": "utility-bill",
                "kind": "electricity",
                "label": "electricity_bill",
                "premises_address": "12, Industrial Estate Rd, Bhosari, Pune, Maharashtra 411026, India",
                "verified_tags": [
                  "Principal Business Address"
                ]
              }
            ]
          }
        },
        "documents": [
          {
            "id": "0c73de91-6a24-4b8f-95d0-7e41b25c8a63",
            "slot": "utility-bill",
            "kind": "electricity",
            "label": "electricity_bill",
            "filename": "electricity-bill.pdf",
            "content_type": "application/pdf",
            "size_bytes": 96410,
            "sha256": "3e8b1d95c072a46f8b5309e7d41c6a28b90f7e35c1d84620a7f3b9e05d268c14",
            "received_at": "2026-08-10T14:36:50"
          }
        ]
      },
      {
        "key": "addresses",
        "title": "Tag the addresses",
        "label": null,
        "type": "address-tagging",
        "required": false,
        "state": "passed",
        "reasons": [],
        "gaps": [],
        "collected": {
          "answer": {
            "addresses": []
          },
          "outputs": {
            "addresses": [
              {
                "address": "12, Industrial Estate Rd, Bhosari, Pune, Maharashtra 411026, India",
                "tag": "Principal Business Address"
              }
            ],
            "recognised": [
              "12, Industrial Estate Rd, Bhosari, Pune, Maharashtra 411026, India"
            ]
          }
        },
        "documents": []
      },
      {
        "key": "review",
        "title": "Review",
        "label": "applicant_signoff",
        "type": "review",
        "required": true,
        "state": "passed",
        "reasons": [],
        "gaps": [],
        "collected": {
          "answer": {
            "declarations": [
              {
                "key": "terms-of-service",
                "label": "tos_consent",
                "title": "Terms of service",
                "content": "I accept the [terms of service](https://yourapp.com/terms) and the [privacy policy](https://yourapp.com/privacy).",
                "accepted": true
              },
              {
                "key": "information-true",
                "label": "truth_declaration",
                "title": null,
                "content": "Everything I have provided is true and correct to the best of my knowledge.",
                "accepted": true
              },
              {
                "key": "contact-on-whatsapp",
                "label": "whatsapp_optin",
                "title": null,
                "content": "You may contact me about this application on WhatsApp.",
                "accepted": false
              }
            ]
          },
          "outputs": {}
        },
        "documents": []
      }
    ]
  }
  ```
</Accordion>

* **The record covers the steps this user was asked for.** `alt-business-proof` is absent: this
  workflow asks for it only when the GST certificate fails, and here it passed, so the journey never
  reached it. Reading the verification with `include_not_applicable=true` shows it. `selfie` is
  different: it reads `declined`, because it was offered and the user refused.
* **`answer` is what was given; `outputs` is what the checks established.** All the GST step was given
  is one PDF. Its outputs are the registry-confirmed identity, which is what to write into your own
  systems, and they carry values only because the step passed.
* **A passed step can note gaps.** `identity` passed while the PAN, which this workflow treats as
  optional, was never shared: its `gaps` carries `digilocker-document-missing`, and the outputs carry
  `pan` as `null`. The step published its outputs, so every field its type publishes is there; one
  with nothing to say is `null`. Branch on `code`, never on `message`: `reasons` and `gaps` both carry
  a stable code alongside wording written for the user's screen.
* **A step can settle without asking the user anything.** `addresses` presents the places the premises
  photos were taken at, and the one here is already on the GST registration, so the match tagged it and
  `recognised` says so. Its `answer` is empty because there was nothing left to ask.

[Reading the result](/verification/results) is the field-by-field reference for a step.

<Info>
  When you move from trying it to building against it, read [Hosted](/verification/hosted), [Handoff](/verification/handoff),
  [Callbacks](/verification/callbacks), which is what you use instead of
  polling, and [Going live](/verification/going-live) before your first real
  subject.
</Info>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.