Metadata-Version: 2.4
Name: humanos
Version: 1.0.5
Summary: Official Humanos API SDK for Python with automatic request signing and webhook verification
Author-email: Humanos <tech@humanos.tech>
Maintainer-email: Humanos <tech@humanos.tech>
License: MIT
Project-URL: Homepage, https://humanos.id
Project-URL: Documentation, https://humanos.mintlify.app
Project-URL: Repository, https://github.com/Humanos-App/humanos-sdks
Project-URL: Bug Tracker, https://github.com/Humanos-App/humanos-sdks/issues
Project-URL: Changelog, https://github.com/Humanos-App/humanos-sdks/blob/main/python/CHANGELOG.md
Keywords: humanos,api,sdk,verifiable-credentials,did,identity,kyc,authentication,credentials
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Internet :: WWW/HTTP
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.25.0
Requires-Dist: cryptography>=3.4.0
Requires-Dist: urllib3>=1.25.3
Requires-Dist: python-dateutil>=2.8.0
Requires-Dist: pydantic>=2.0.0
Requires-Dist: typing-extensions>=4.0.0
Provides-Extra: flask
Requires-Dist: flask>=2.0.0; extra == "flask"
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.68.0; extra == "fastapi"
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.20.0; extra == "dev"
Requires-Dist: python-dotenv>=1.0.0; extra == "dev"
Dynamic: license-file

# Humanos SDK for Python

Official Python SDK for the [Humanos](https://humanos.id) API. Auto-signs every request, verifies and decrypts webhooks, and ships full type definitions (pydantic models).

[![PyPI version](https://badge.fury.io/py/humanos.svg)](https://pypi.org/project/humanos/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## Concepts

Humanos gives your agent a cryptographic permission slip — issued by the user, scoped to a specific action, and verified at runtime.

- **Action** — a reusable policy template you publish through the dashboard. Declares `userParams` (values the human approves), `executionParams` (what the agent must supply at execution time), and the rules that compare them.
- **Mandate** — a signed W3C credential (type `POLICY`) the user issues to your agent. References one action plus the `userParams` values that bind this specific approval.
- **Verifiable Presentation (VP)** — a short-lived, signed proof derived from a mandate. The agent presents one each time it wants to act.
- **Verify** — Humanos checks the VP's signatures, expiry, revocation status, and the action's rules against the agent's `executionParams`. Returns allow or deny.
- **Revoke** — kills a mandate. Any further VP issuance or `verify` is denied.

## Lifecycle

A mandate's lifetime breaks into two flows: **request and approval** (one-time, gets you a mandate ID) and **issuance and verification** (repeats every time the agent acts).

**Flow 1 — Request and approval**

```
 ┌──────────┐    ┌──────────────┐    ┌──────────┐    ┌──────────┐         ┌─ accepts ─▶  mandate issued
 │  Action  │ ─▶ │ Agent needs  │ ─▶ │  Agent   │ ─▶ │   User   │ ─▶ ─────┤
 │ defined  │    │  permission  │    │ requests │    │ decides  │         └─ rejects ─▶  flow ends
 └──────────┘    └──────────────┘    └──────────┘    └──────────┘
   step 1                              step 2          step 3
```

**Flow 2 — Issuance and verification**

```
 ┌──────────┐    ┌──────────────────────────────┐         ┌─ true  ─▶  allow (agent acts)
 │  Agent   │ ─▶ │ Humanos verifies VP against  │ ─▶ ─────┤
 │issues VP │    │       executionParams        │         └─ false ─▶  deny (blocked)
 └──────────┘    └──────────────────────────────┘
   step 4                       step 5
```

## Installation

```bash
pip install humanos
```

Requires Python 3.8 or newer.

## Configuration

Sign up at [humanos.id](https://humanos.id), then grab two sets of credentials from the dashboard:

1. **API credentials** — **Settings → API Keys**. Copy the **API Key** and **Signature Secret**.
2. **Webhook credentials** — **Settings → Webhooks**. Copy the **Webhook Signature Secret**, **Webhook Encryption Secret**, and **Webhook Encryption Salt**.

Add them to your `.env`:

```env
HUMANOS_API_KEY=<your-api-key>
HUMANOS_SIGNATURE_SECRET=<your-signature-secret>

HUMANOS_WEBHOOK_SIGNATURE_SECRET=<your-webhook-signature-secret>
HUMANOS_WEBHOOK_ENCRYPTION_SECRET=<your-webhook-encryption-secret>
HUMANOS_WEBHOOK_ENCRYPTION_SALT=<your-webhook-encryption-salt>
```

Everything else (action IDs, mandate IDs, etc.) is application data — keep it in code or your database, not in `.env`.

## Quick start

```python
import os
from humanos_sdk import HumanosClient, HumanosClientConfig

client = HumanosClient(HumanosClientConfig(
    base_path="https://api.humanos.id",
    api_key=os.environ["HUMANOS_API_KEY"],
    signature_secret=os.environ["HUMANOS_SIGNATURE_SECRET"],
))

# Sanity check — confirms the API key, signature secret, and signing handshake.
requests = client.requests.list()
print(requests)
```

A `200 OK` with a (possibly empty) list means you're wired up. The walkthrough below exercises the rest of the flow.

## Integration walkthrough

Six steps, matching the lifecycle diagram above. Run through them once with a test action to get an end-to-end feel.

### 1. Define an action

In the dashboard, create an **action**. An action has three parts:

- **`executionParams`** — the values the agent supplies at `verify()` time.
- **`userParams`** — the constants the user pins at decision time.
- **Rules** — deterministic CEL expressions comparing the two above.

Example:

| Part              | Field               | Type            |
| ----------------- | ------------------- | --------------- |
| `executionParams` | `amount`            | number          |
|                   | `category`          | string          |
| `userParams`      | `maxAmount`         | number          |
|                   | `allowedCategories` | array\<string\> |

Rules then express things like `executionParams.amount <= userParams.maxAmount` and `executionParams.category in userParams.allowedCategories`.

Once defined, **publish the action** and **copy its ID**. Hold the ID as a constant in your code or store it alongside the rule it represents.

### 2. Issue a mandate request

Ask Humanos to issue a mandate to a user. The request bundles the contact, the action ID, and the `userParams` values the user is being asked to approve.

```python
from humanos_sdk_generated.models import (
    GenerateRequestDto,
    CredentialDto,
    ActionRefDto,
)

action_id = "urn:via:action:<uuid>"  # from step 1

request = client.requests.create(
    GenerateRequestDto(
        contacts=["user@example.com"],
        security_level="CONTACT",
        credentials=[
            CredentialDto(
                scope="agent.execute",
                type="POLICY",
                name="Action mandate",  # required, shown to the user
                action=ActionRefDto(
                    action_urn=action_id,
                    user_params={
                        "maxAmount": 10000,
                        "allowedCategories": ["BOOKS", "OFFICE"],
                    },
                ),
            )
        ],
    )
)

print("Request ID:", request.id)
```

Persist `request.id` if you want to track pending approvals. The mandate ID itself becomes available once the user approves (next step).

### 3. User approval and mandate issuance

Humanos handles the user-facing approval flow. The user reviews the `userParams` from step 2 and either approves or rejects.

**Identity verification.** The security code (OTP) is always delivered by **email or SMS** — that's how Humanos proves the user is reachable at the contact you supplied.

**Where the approval UI shows up.** Two options:

- **Hosted (default)** — the email or SMS message contains a link to the Humanos-hosted approval page. The user clicks through, reviews, decides.
- **Embedded iframe** — your application embeds the Humanos approval UI directly. The user never leaves your app. See the [iframe integration guide](https://humanos.mintlify.app/essentials/iframe-integration) for setup.

If KYC is required on the action, the user completes that first. If the user rejects (or KYC fails), no mandate is issued.

**On accept, Humanos issues the mandate and emits a `credential` webhook event.** The mandate ID — `urn:via:credential:…` — is the durable artifact: persist it on the rule record in your database (e.g., `rule_id → mandate_id`).

The SDK ships Flask and FastAPI handler factories that verify signatures and decrypt payloads automatically. FastAPI:

```python
import os
from fastapi import FastAPI, Request
from humanos_sdk import WebhookConfig, create_fastapi_webhook_handler

app = FastAPI()

config = WebhookConfig(
    signature_secret=os.environ["HUMANOS_WEBHOOK_SIGNATURE_SECRET"],
    encryption_secret=os.environ["HUMANOS_WEBHOOK_ENCRYPTION_SECRET"],
    encryption_salt=os.environ["HUMANOS_WEBHOOK_ENCRYPTION_SALT"],
)


def on_event(payload):
    event_type = payload["eventType"]

    if event_type == "credential":
        if payload["decision"]["action"] != "accept":
            # user rejected — mark rule unenforceable, notify operator
            return
        mandate_id = payload["credential"]["id"]
        # persist: rule_id → mandate_id

    elif event_type == "identity":
        # KYC / identity verification completed
        pass

    elif event_type == "otp.failed":
        # user failed OTP — possible fraud signal
        pass

    elif event_type == "test":
        # "Send test event" from the dashboard
        pass


handler = create_fastapi_webhook_handler(config, on_event)


@app.post("/webhook")
async def webhook(request: Request):
    return await handler(request)
```

Flask is symmetric — use `create_flask_webhook_handler(config, on_event)` and call the returned function from your route.

The `credential` payload shape:

```python
{
    "eventType": "credential",
    "requestId": str,
    "internalId": str | None,
    "issuerDid": str,
    "user": {"contact": str, "id": str, "internalId": str | None},
    "credential": CredentialEntity,        # full W3C VC; ["id"] is the mandate ID
    "decision": {"action": "accept" | "reject", "date": str},  # ISO 8601
}
```

For local development, expose your server with [ngrok](https://ngrok.com):

```bash
ngrok http 8000
```

Set the ngrok URL (e.g. `https://xxxx.ngrok-free.app/webhook`) under **Settings → Webhooks**.

If you're using the iframe channel, the same `credential` payload is also delivered via `window.postMessage` to the parent window — useful for live UI updates without a backend round-trip.

> **Dev shortcut:** during development you can copy the mandate ID directly from the dashboard's activity table (look for the `MANDATE_ISSUE` entry) instead of wiring a webhook.

### 4. Agent issues a VP

When the agent wants to act, your backend (or whichever component enforces the rule) asks Humanos for a fresh **Verifiable Presentation** bound to the mandate ID captured in step 3. VPs are short-lived and single-purpose — issue a new one for every verify.

```python
from humanos_sdk_generated.models import CreatePresentationDto

mandate_id = "urn:via:credential:<id>"  # captured in step 3

vp = client.credentials.issue_vp(
    mandate_id,
    CreatePresentationDto(
        # target_verifier="did:web:your-verifier.example",  # optional, see below
    ),
)
```

Field-by-field:

- **`mandate_id`** (first positional arg) — the mandate ID captured in step 3.
- **`target_verifier`** (optional body field) — DID of the intended verifier. When provided, the VP is bound to that audience via `proof.domain` plus a challenge nonce.

The response is a `PresentationResponseEntity` — `presentation` and `receipt`. Pass `vp.presentation` to step 5.

### 5. Humanos verifies the VP

Hand the VP plus the agent's `executionParams` to `verify()`. Humanos checks four things: signatures, expiry, revocation status, and rule compliance. `200 OK` means allow; `403` means at least one check failed (the response body names the failing rule under `evaluations`).

```python
from humanos_sdk_generated.models import VerifyPresentationDto

client.credentials.verify(
    VerifyPresentationDto(
        presentation=vp.presentation,
        execution_params={"amount": 5000, "category": "BOOKS"},
    )
)
```

Field-by-field:

- **`presentation`** — the signed VP from step 4. Read it from `vp.presentation`.
- **`execution_params`** — what the agent wants to do. Field names must match those declared on the action and are referenced inside rules as `executionParams.<field>`.

Expected outcomes for the example action:

| Case          | `execution_params`                       | Result                                      |
| ------------- | ---------------------------------------- | ------------------------------------------- |
| In-bounds     | `{"amount": 5000, "category": "BOOKS"}`  | allow (200) + signed receipt                |
| Out-of-bounds | `{"amount": 50000, "category": "FLIGHTS"}` | deny (403) — body names the failing rule(s) |

### 6. Revoke a mandate

Mandates are immutable — to retire one (rule deletion, rule update, user pulling consent), call `credentials.revoke()`. Once revoked, the mandate is dead: `issue_vp` errors with `credential_revoked`, and any VP still held by an agent fails `verify()` with the same reason. Stale rules cannot be enforced; that's the safety property.

```python
from humanos_sdk_generated.models import RevokeCredentialDto

client.credentials.revoke(
    mandate_id,
    revoke_credential_dto=RevokeCredentialDto(reason="user_initiated"),
)
```

Field-by-field:

- **`credential_id`** (first arg) — the mandate ID.
- **`reason`** — free-text label recorded on the credential and in the `MANDATE_REVOKED` receipt. Recommended values from the VIA protocol: `user_initiated`, `organization_policy`, `system_expiry`.

The response is a `RevokeCredentialEntity` — `credential_id`, `status="REVOKED"`, `revoked_at`, and a `MANDATE_REVOKED` receipt. Store it for audit if useful. Enforcement uses the revocation status itself, not the receipt.

## Humanos audit trail

For compliance, Humanos persists every consequential event in a mandate's lifecycle as an **immutable, cryptographically signed record**. You don't need to log these yourself — they're available for query at any time via `client.activity.list()`.

| Event                  | Trigger                            | Captured                                                                |
| ---------------------- | ---------------------------------- | ----------------------------------------------------------------------- |
| Mandate issued         | User accepts a request             | Action ID, `userParams`, signed credential, timestamp                   |
| Mandate revoked        | `credentials.revoke()` succeeds    | Mandate ID, reason, timestamp                                           |
| Mandate canceled       | Request canceled before approval   | Request ID, canceler, timestamp                                         |
| Human decision: accept | User approves a request            | User, request, OTP channel (email or SMS), UI surface (hosted / iframe) |
| Human decision: reject | User rejects a request             | User, request, OTP channel, UI surface                                  |
| VP issue               | `credentials.issue_vp()` succeeds  | Mandate ID, VP, target verifier, receipt                                |
| VP issue denied        | `credentials.issue_vp()` rejected  | Mandate ID, reason code (e.g., `credential_revoked`)                    |
| Verify accept          | `credentials.verify()` returns 200 | VP, `executionParams`, rule evaluations, signed receipt                 |
| Verify deny            | `credentials.verify()` returns 403 | VP, `executionParams`, failing rule(s), signed receipt                  |

## Webhook details

The Flask / FastAPI handler factories decrypt and verify the payload, then call your callback with the decoded dict. Switch on `payload["eventType"]` to dispatch:

| Event        | Trigger                                                                            |
| ------------ | ---------------------------------------------------------------------------------- |
| `credential` | A credential request was approved or rejected. `credential["id"]` is the mandate ID. |
| `identity`   | An identity / KYC check completed.                                                 |
| `otp.failed` | A user failed OTP entry.                                                           |
| `test`       | "Send test event" from the dashboard.                                              |

**Operational notes**

- **Send a test event** from **Settings → Webhooks** before going live — confirms your URL is reachable and your handler decodes the payload.
- **Retries:** failed deliveries are retried with backoff. Make your handler idempotent (key off `requestId` + `eventType`).
- **Order:** events for unrelated requests may arrive out of order. Within a single request, `credential` precedes any follow-up events.

## API reference

Every method below is fully typed via pydantic; signatures live in the generated package.

```python
# Requests — credential request lifecycle
client.requests.list()                              # paginate / filter your requests
client.requests.create(generate_request_dto)        # issue a credential request to a user
client.requests.detail(request_id)                  # full request including credentials and subjects
client.requests.cancel(request_id)                  # cancel a pending request
client.requests.resend_otp(request_id)              # resend OTP via the original channel

# Credentials — mandates and verification
client.credentials.detail(credential_id)            # fetch a credential by ID
client.credentials.evidence(evidence_id)            # download attached evidence
client.credentials.issue_vp(vc_id, create_presentation_dto)   # issue a fresh VP from a mandate
client.credentials.verify(verify_presentation_dto)            # evaluate a VP at runtime
client.credentials.revoke(credential_id, revoke_credential_dto=...)  # permanently revoke a mandate

# Actions — published policy templates
client.actions.list()
client.actions.versions(action_id)

# Activity — audit log
client.activity.list()

# Approval workflows (forms, consents, documents)
client.approval.list()
client.approval.workflows()

# Users
client.users.create(create_subject_dto_list)
client.users.detail(contact=...)

# DID resolution
client.did.resolve(did)
```

## API versioning

Humanos uses **date-based API versions** (e.g., `2026-03-24`). The SDK pins each release to a default version and sends it as an `api-version` header on every request — your integration stays isolated from API changes, and you upgrade by bumping the SDK on your own schedule.

The current default is exported as `API_VERSION`:

```python
from humanos_sdk import API_VERSION
print(API_VERSION)  # "2026-03-24"
```

Every method also accepts an optional `api_version` keyword argument for per-call overrides — useful when testing a new version against a single endpoint before bumping globally:

```python
client.credentials.verify(
    VerifyPresentationDto(
        presentation=vp.presentation,
        execution_params={"amount": 5000, "category": "BOOKS"},
    ),
    api_version="2025-12-01",  # api version for this call only
)
```

To pin globally, subclass `SignedApiClient` and override the `api-version` header — or upgrade the SDK release once you're ready to migrate.

**Action versions are independent of API versions.** When you republish an action in the dashboard, mandates already issued against the older version keep referencing that version — they don't break. List published versions with `client.actions.versions(action_id)`.

## Error handling

The SDK raises `ApiException` (from `humanos_sdk_generated.exceptions`) on non-2xx responses. The exception carries the HTTP status and response body:

```python
from humanos_sdk_generated.exceptions import ApiException

try:
    client.requests.create(generate_request_dto)
except ApiException as e:
    print("Status:", e.status)
    print("Body:", e.body)
```

## Troubleshooting

**`401` on every request.** Signature mismatch. Double-check the signature secret matches the dashboard exactly — no trailing whitespace or stray newline.

**`400` on `requests.create()` with "name is required".** Each `CredentialDto` needs a `name` field. The example in step 2 uses `"Action mandate"`.

**Webhook never fires.** Confirm the URL in **Settings → Webhooks** matches your live endpoint. Send a test event from the dashboard. For local dev, the ngrok tunnel must be open when the user approves.

**Webhook fires but the handler errors with a signature mismatch.** Make sure your framework hands the **raw request body** to `process_webhook` (not a parsed/reformatted version). The provided Flask / FastAPI handlers do this correctly.

**`verify()` returns 403 with no obvious failing rule.** Check that the `execution_params` field names exactly match the action's declared fields. A typo silently fails rule evaluation.

**`issue_vp()` returns 400 after revoke.** Expected — once a mandate is revoked, VP issuance is blocked at source with `credential_revoked`. Existing VPs also fail `verify()` for the same reason.

## Documentation & related

- [Full API documentation](https://humanos.mintlify.app/)
- [Humanos website](https://humanos.id)
- Sister SDKs: [TypeScript](../typescript/README.md), [C#](../csharp/README.md)

## Support

- **Email**: tech@humanos.tech

## License

MIT License - see [LICENSE](./LICENSE) file for details.
