<!-- markdown mirror of https://mintid.net/en/sdk/agent-clients — generated at build time -->

> The runtime side of an agent credential, in Python and TypeScript: it obtains the credential, keeps its witnesses current, answers a seller inside the payment flow, and refuses before anything is sent whatever the delegation does not cover.

index — 22 documents

1.  [overviewsdk/00](/en/sdk)
2.  [getting startedsdk/01](/en/sdk/getting-started)
3.  [python sdksdk/02](/en/sdk/python)
4.  [service modesdk/03](/en/sdk/service-mode)
5.  [typescript clientsdk/04](/en/sdk/typescript-client)
6.  [mcp serversdk/04a](/en/sdk/mcp-server)
7.  [conformancesdk/05](/en/sdk/conformance)
8.  [reason codessdk/06](/en/sdk/reason-codes)
9.  [substrate readssdk/07](/en/sdk/substrate-reads)
10.  [credentials & holderssdk/08](/en/sdk/credentials-and-holders)
11.  [holder · wasmsdk/holder](/en/sdk/holder)
12.  [agent clientssdk/15](/en/sdk/agent-clients)
     *   [the runtime side](#overview)
     *   [install and test](#install-and-test)
     *   [the surface](#surface)
     *   [the payment loop](#payment-loop)
     *   [witness refresh cadence](#refresh-cadence)
     *   [checking a policy first](#checking-a-policy-before-any-challenge)
     *   [what fails closed](#fails-closed)
     *   [key custody](#key-custody)
     *   [minting from a separate holder](#minting-from-a-separate-holder)
     *   [bounded agents](#bounded-agents)
     *   [agent profile v2](#agent-v2)
     *   [the disclosure relay](#disclosure-relay)
     *   [not in this version](#not-in-this-version)
13.  [agent-to-agent handshakesdk/14](/en/sdk/agent-to-agent-handshake)
14.  [payment identitysdk/09](/en/sdk/payment-identity-profile)
15.  [becoming a verifiersdk/10](/en/sdk/becoming-a-verifier)
16.  [protocol statesdk/11](/en/sdk/protocol-state)
17.  [verify a releasesdk/18](/en/sdk/verify-a-release)
18.  [browser checksdk/19](/en/sdk/check-a-presentation-in-the-browser)
19.  [record · assurance vocabularyrecord/01](/en/sdk/kyc-trust-model)
20.  [record · packagingrecord/02](/en/sdk/packaging)
21.  [record · multi-languagerecord/03](/en/sdk/multi-language-clients)
22.  [record · service contractrecord/04](/en/sdk/verifier-service-contract)

SDK docs · sdk/15

# Agent clients

The runtime side of an agent credential, in Python and TypeScript: it obtains the credential, keeps its witnesses current, answers a seller inside the payment flow, and refuses before anything is sent whatever the delegation does not cover.

## The runtime side of an agent credential

An agent credential is what an automated buyer presents on the payment rail: it proves that a person, verified at an issuer, delegated _this_ agent to pay _this_ kind of counterparty within stated bounds — and nothing about who that person is. The agent client is the runtime side of that credential: it obtains it, keeps its witnesses current, answers a seller's identity requirement inside the payment flow, and refuses — before anything is sent — whatever the delegation does not cover.

Two clients ship, with the same surface: **Python** (`mintid-agent`, over the holder library's Python binding) and **TypeScript** (the agent module of the Node package, over the holder library process — one JSON request per line, the same transport the Go binding uses). Neither carries cryptography of its own: proofs are the holder library's, signatures are an injected signer's.

## Install and test

Both clients are on the public package registries, built from the main project, [https://gitlab.com/mintid/mintid](https://gitlab.com/mintid/mintid) (read-only mirrors below):

Package

Registry

Version

What you get

`mintid-agent`

PyPI

0.5.0

the Python agent client, pure Python (`sdk/python/mintid-agent/`, mirror [https://gitlab.com/mintid/sdk-python](https://gitlab.com/mintid/sdk-python))

`mintid-proof-core`

PyPI

0.5.3

the Python binding of the holder library, which `mintid-agent` needs (0.5.0 requires 0.5.2 or a later 0.5)

`mintid-verifier-sdk`

PyPI

0.5.1

the relying party's side: the verifier SDK and service ([Python SDK](/en/sdk/python))

`@mintid-net/verifier-client`

npm

0.4.0

the TypeScript client, its agent module included (`sdk/typescript/`, mirror [https://gitlab.com/mintid/sdk-typescript](https://gitlab.com/mintid/sdk-typescript))

`@mintid-net/verifier-mcp`

npm

0.3.1

the MCP server ([MCP server](/en/sdk/mcp-server))

*   **Python** (3.12 or later): `pip install "mintid-agent[ed25519]"` (it pulls in `mintid-proof-core`; the extra is the software Web Bot Auth signer, not needed with your own). `mintid-proof-core` and `mintid-verifier-sdk` compile the Rust proof core in, and the registry carries a prebuilt wheel for **Linux x86-64 only** (manylinux). On any other platform — macOS, Linux on ARM, Windows — pip builds the package from its source distribution, which needs a Rust toolchain (`rustup`) and takes about a minute. The same holds from a clone of the main project: `pip install ./identity-proof-core/python`, then `pip install -e "./sdk/python/mintid-agent[ed25519]"`.
*   **TypeScript** (Node 20 or later): `npm install @mintid-net/verifier-client`. The agent module drives the `mintid-holder` binary, which is on no registry: build it from a clone of the main project (`cargo build --release -p mintid-holder` in `identity-proof-core/`) and put it on `PATH` ([TypeScript client](/en/sdk/typescript-client)).

The registries carry released versions; the main project can be ahead of them. What a page here says is the main project's behaviour, and where a released version differs, the page says so.

**Upgrading from `mintid-agent` 0.1 / `@mintid-net/verifier-client` 0.1.** Two behaviour changes:

*   **Challenges must be signed, and the client needs a verifier key source.** The holder checks the verifier's signature over every challenge before it builds anything ([the rule and its reason codes](/en/sdk/credentials-and-holders)). Give the client a source of verifiers' request keys — `verifiers=` in Python (`ProvenVerifierKeys` over a proven chain read, or `StaticVerifierKeys` of records you pinned), `verifierKeys` in TypeScript (a `VerifierKeySource` of your own, or `staticVerifierKeys([...])`) — and pass the verifier's `{request, signature_hex}` unmodified. Without a source, or for an unsigned or wrongly signed challenge, the client refuses (`ChallengeRefused` in Python, `ChallengeSignatureError` in TypeScript) and nothing is presented or paid. A verifier that signs nothing (`--insecure-unsigned-challenges`) is a devnet only; the per-call opt-out that answers it is for that case alone and warns every time. TypeScript: build `mintid-holder` from the same release, since the check runs in it.
*   **`usable` waits for scope anchoring.** `refresh()` reports `usable` only once a finalised root anchors every scope value of the delegation ([below](#when-a-new-agent-can-present)); with the 0.1 releases it reflected the status witness alone.

Test on [the public testnet](/en/developers) and only there (check its status line first): a principal comes from its KYC sandbox with the outcome you choose, its issuer makes agent offers to that principal under the same mint limits as everywhere (5 agents per person per hour, 10 active), its sandbox verifier answers challenges, and its privacy relay takes the kill switch. Nothing minted there carries weight, and the testnet is reset when the software changes shape. The single-node production chain is not for third parties and is never a place to test.

The client reaches the testnet through the network registry its proof core carries: with `MINTID_NETWORK=testnet` (or `network="testnet"` when you build the client) the kill switch takes the testnet's relay and node, and `endpoint("issuer")` names its issuer for the sandbox journey ([the testnet's endpoints](/en/developers)). An endpoint you set yourself wins over the registry.

## The surface

Operation

What it does

issue from an offer

the principal's identity credential answers the issuer's mint challenge, the agent credential is obtained blind, and the scope witnesses of its delegation are adopted

mint a token, then complete the issuance

the same two steps apart: the token step beside the principal's holder, the credential step in the agent's runtime ([below](#minting-from-a-separate-holder))

refresh

the status witness first; once a finalised root anchors it, the issuer's public scope log is replayed in bulk from the client's position — never a query about one value. It reports the agent **usable** only when a verifier can prove its delegation too ([below](#when-a-new-agent-can-present))

present

answers a verifier's challenge, or refuses naming the predicate the credential cannot satisfy

can satisfy

says whether a claim policy is within the credential, naming what is not, before any challenge and with nothing sent ([below](#checking-a-policy-before-any-challenge))

pay

one paid request under the payment-identity profile, below

renew

redeems a renewal offer: renewal is replacement — a new credential, the old one revoked on issuance and dropped from custody

relay a disclosure

hands a relying party's signed disclosure offer or identification request to the agent's own issuer, with a proof that it is the agent that relying party saw; the answer is an outcome, never a field — Python client; see [the disclosure relay](#disclosure-relay)

self-revoke

the agent's kill switch: builds the credential's self-revocation, saves custody (the agent refuses to present from then on, also after a restore), and submits it through a privacy relay that pays for it (or hands the message back for any relay); the receipt is confirmed against the chain through the node in the client's configuration, "submitted" until the chain shows it — Python client; see [revoking your own credential](/en/sdk/credentials-and-holders)

The operator — not the agent — decides the delegation: scope, limit, custody class, lease. The client chooses nothing about what it holds.

## The payment loop

1.  The request goes out, signed. A `402` that carries an identity requirement starts the loop; one that carries none is paid without identity.
2.  The client validates the declaration it is about to answer: a profile it runs, an `https` challenge endpoint, the token-exchange session mode and — for the buyer profile — exactly one `pairwise_pseudonym` entry (mandatory since contract 1.4.0) and an `unexpired` entry (mandatory since 1.5.0). Anything else stops here.
3.  If it holds a live session for that host, it skips to step 6 with the session token.
4.  It refreshes its witnesses, then asks the declared endpoint for a challenge. A challenge whose audience is neither the resource's origin nor the endpoint's own is not answered — a vendor fronting someone else's challenge gets nothing.
5.  The holder checks the challenge's signature against the verifier's registered request key — from the source you configured (`verifiers=` in Python, `verifierKeys` in TypeScript; see [the challenge signature](/en/sdk/credentials-and-holders)) — and refuses an unsigned or wrongly signed challenge. Then it proves the declared policy or refuses. The pseudonym entry needs nothing from you: the holder derives the agent's pairwise pseudonym for the challenge's own verifier, and refuses a challenge whose entry names any other. **A refusal ends the payment before it exists**: no presentation is sent and the payer callback is never called.
6.  The payer callback builds the payment header (the payment scheme is yours), and the request is retried with the payment header and either the presentation or the session token.
7.  On success the session token the vendor returns is kept for that host; the next payment sends it instead of a presentation — one presentation per session.

A session answer (`identity_required`, `session_expired`, `session_key_mismatch`) or a stale root gets one fresh presentation; every other identity rejection is terminal for the attempt and surfaces with its reason code. There is no retry-shopping and no loop.

## Witness refresh cadence

Refresh right before presenting — the pay loop does it every time, and it does it _before_ asking for the challenge, so the 10-second window belongs to the proof. Roots move on a short heartbeat; a witness behind the anchored root cannot build an acceptable presentation. A withdrawn delegation takes effect at the next root: after the refresh that replays it, the holder refuses that value, while the rest of the delegation keeps working. A client that presents without refreshing does not get around a withdrawal — its proof over superseded anchors is refused by the verifier — it only loses the payment attempt.

### When a new agent can present

A new agent's status witness can be usable at once, but a verifier checks every scope value the policy names against the issuer's **anchored scope directory**: the scope anchors, one leaf per delegated value and the delta-log position, all of the last finalised root a verifier can prove. A value that no such root carries yet — typically the first agent ever delegated to a given seller — cannot be proven, and every presentation is refused with `scope_axis_unprovable` until the issuer's next root is finalised and provable: one or two heartbeats (30 seconds each on the public testnet).

`refresh()` accounts for it. It reads the anchored directory before replaying the delta log, and reports `usable` only when the status witness is usable **and** the directory carries a leaf for every value of the delegation at or past the position its scope witnesses were issued at. Once true, the client does not read the directory again. So, after minting or renewing an agent, poll `refresh()` until `usable`, then present: Python `agent.refresh().usable` (and `agent.scope_anchored`), TypeScript `(await agent.refresh()).usable` (and `agent.scopeAnchored`).

The check ships in `mintid-agent` 0.2.0 and `@mintid-net/verifier-client` 0.2.0. With the 0.1.0 packages `usable` reflects the status witness alone.

## Checking a policy before any challenge

`can_satisfy` (Python) / `canSatisfy` (TypeScript) answers whether the credential can satisfy a verifier's claim policy — a seller's declaration or a signed request — before any challenge exists, from the holder's own view of the credential: no challenge is requested, no proof is built, nothing is sent.

`unsatisfied` lists the output keys the credential cannot prove, in the policy's order — the same keys a refused presentation names (`counterparty_includes_<value_id>`, `key_custody_class_gte_<rank>`, `transaction_limit_gte_<minor>_<unit>`, `assurance_grade_gte_a<n>`, `unexpired`…), never a scope value. It is a pre-check, and `present` stays the authority:

*   a revocation is known only once a refresh has seen it — refresh first when it matters; a credential that revoked itself is reported (`credential_active`);
*   the pairwise pseudonym is not judged: the holder proves it for whichever verifier's challenge asks, and refuses a challenge naming another;
*   an age floor is reported unsatisfiable for an agent of profile v1, and left to the holder for v2;
*   a claim the client does not know is left to the holder and the verifier.

The Python and TypeScript clients apply the same rule (shared test vectors).

pythonsdk/15 · can\_satisfy

```
result = agent.can_satisfy(declaration["claim_policy"])
if not result.ok:
    reason = result.unsatisfied[0]      # e.g. counterparty_includes_<value_id>
```

tssdk/15 · canSatisfy

```
const { ok, unsatisfied } = await agent.canSatisfy(declaration.claim_policy);
```

## What fails closed at the client

Condition

Outcome

the policy asks for a value outside the delegation, a higher custody class, a larger limit or another currency

refused by the holder, naming the output key; nothing presented, nothing paid

the credential is revoked (by the operator, by renewal, or because its principal was)

the refresh fails; nothing presented

a declaration with an unknown profile, a non-`https` endpoint, an unoffered session mode, or a buyer-profile policy without exactly one pseudonym entry (`pseudonym_required`) or without `unexpired` (`unexpired_required`)

refused before any challenge is requested

a challenge whose pseudonym entry names a verifier other than the challenge's own

refused by the holder; nothing presented, nothing paid

a challenge for another audience

refused before presenting

a challenge without a signature, signed by a key not registered for its verifier, under a retired key, or changed after signing

refused by the holder with its reason code (`challenge_unsigned`, `challenge_key_unknown`, `challenge_key_not_active`, `challenge_signature_invalid` …); nothing presented, nothing paid

a terminal rejection from the vendor

surfaced with its reason code, not retried

## Key custody

Keys never leave the client. The credential's private bytes — they contain the link secret — stay in the holder library and reach exactly one place: the credential store **you inject**, as one opaque record per credential. **Encryption at rest is yours**: wrap the store with the key that protects the agent's payment key (a KMS envelope, an OS keychain, enclave sealing). The in-memory store is for tests and ephemeral runtimes. The Web Bot Auth key is behind an injected signer — an HSM, a KMS, an enclave, or a software key for development — and the client only ever asks it to sign. Publish the signer's directory document at the URL your requests name as their signature agent.

## Minting from a separate holder

Issuing from an offer takes two steps, and only the first needs the principal. The **token step** is where the principal's identity credential answers the issuer's mint challenge; the issuer answers with a one-use bearer and an issuance nonce. The **credential step** is where the agent's secret is drawn, the credential is obtained blind under that nonce, and the scope witnesses are adopted. When the principal's holder and the agent's runtime are different places — a person's device and an operator's server — run the two steps apart, so the agent's custody is born in the runtime and the principal's credential never leaves the person's holder.

`issue from an offer` is exactly these two calls back to back, in one process.

**The material** is a small JSON object, the same in both languages (a material written by one client completes in the other): the issuer's URL, the agent configuration the offer names, the bearer, the issuance nonce, `expires_at`, the offer's delegated scope (axis, value and value id per entry), the issuer's scope-log URL and, when set, the bundle id and the id of the credential a renewal replaces. It carries **nothing of the principal**: no presentation, no status reference, no credential of the person. It is still a secret until it is spent — whoever holds it can complete the issuance — so hand it to the runtime once, over an authenticated channel, and do not log it.

**Its lifetime** is the issuer's token lifetime: five minutes, or less when the offer itself expires sooner; `expires_at` is counted from the token step's clock. Complete the issuance before then.

Condition at the credential step

Outcome

`expires_at` has passed

`material_expired`, before anything is sent — mint a new token from a fresh offer (the principal again)

the material was already used (its nonce is consumed), or the issuer let it lapse first

`material_consumed`, from the issuer's refusal of the bearer

not a material this client reads

`material_invalid`, naming the field

Python raises `MaterialExpired`, `MaterialConsumed` (also a `HolderError` of the credential step) and `MaterialInvalid` (a `ValueError`), all with `reason`; TypeScript throws `MintMaterialError` with `reason` (a `HolderStepError`). No message carries the bearer, the nonce or a scope value.

**Renewal** splits the same way: `mint_token(offer, principal, renews_credential_id=agent.credential_id)` beside the principal, then `agent.complete_renewal(material)` in the runtime (TypeScript: `mintToken(offer, principal, { renewsCredentialId })`, then `completeIssuance` or `agent.completeRenewal`). The replaced credential's record leaves the runtime's store.

pythonsdk/15 · mint\_token / complete\_issuance

```
# Where the principal's holder is:
from mintid_agent import mint_token
material = mint_token(offer, principal_holder, transport=transport)
send_to_runtime(material.to_json())          # once, over a channel you trust

# In the agent's runtime:
from mintid_agent import AgentClient, MintMaterial
agent = AgentClient.complete_issuance(
    MintMaterial.from_json(received), transport=transport, store=store, verifiers=verifiers
)
```

tssdk/15 · mintToken / completeIssuance

```
// Where the principal's holder process is:
const material = await mintToken(offer, principalHolder, { fetch });
sendToRuntime(mintMaterialToJson(material));

// In the agent's runtime, with its own holder process:
const agent = await AgentClient.completeIssuance(mintMaterialFromJson(received), runtimeRpc, {
  fetch, store, verifierKeys,
});
```

## Bounded agents (scope bundles)

A **bounded agent** is an agent credential minted from a named **bundle** of the issuer's catalogue instead of a hand-assembled scope. A bundle bounds every axis of the delegation: the actions are fixed, categories and regions are chosen inside an allowed list, the sellers are an allowlist in the standard counterparty forms, and the value per transaction and the lease have caps. The catalogue is versioned and content-hashed, written only in values of the current scope vocabulary, and every bundle is private-only — the credential carries no disclosure-class commitment, which means private-only: the agent proves the same predicates as any agent and never discloses who stands behind it. MintID specifies the catalogue's format and rules; the **content is the issuer operator's**: the operator loads its own catalogue file at the issuer (an invalid file stops the service from starting), and without one the issuer runs the **reference catalogue** MintID ships as an example — three bundles: `payments` (digital goods and APIs from named sellers), `procurement` (buying from named suppliers and reading their catalogues) and `data-read` (news, research and market data, paying per access). Whichever catalogue is in force, the issuer reports its identifier, content digest and source (`reference` or `operator`), and an offer names the identifier and digest.

*   **Operator side.** The operator lists the bundles on the issuer's operator API, then asks for an agent offer naming the bundle and its choices inside it (the sellers, the regions, optionally categories, unit, limit and custody class). Anything outside the bundle is refused before an offer exists; a bundle cannot be combined with a raw scope. The offer names the bundle and carries the operator's own price line for it — the per-bundle mint fee is charged by the operator; MintID charges nothing and nothing about it reaches the chain. A renewal names its bundle too and cannot leave it. An issuer may run bundles-only.
*   **Client side.** Redeeming a bundle offer is redeeming any offer; the client keeps the bundle's name (never the price) next to the credential.
*   **Relying-party side.** `policy_preset` (Python, verifier SDK) and `agentPolicyPreset` (TypeScript) build the claim policy for agents of a bundle: the bundle's action, your counterparty, a category and region of the bundle, the minimum limit you need, the mandatory `unexpired` and pairwise-pseudonym entries. The preset covers every axis — so the fail-closed rule above a value threshold always holds — and refuses to ask for anything the bundle cannot grant. Pass the result to challenge creation as any agent policy. A preset uses the catalogue you give it, the reference one by default. For an operator's own catalogue, load it first with `catalogue_from(document, expected_digest=…)` (Python) or `bundleCatalogueFrom(document, expectedDigest?)` (TypeScript): the document is the operator's catalogue file or the issuer's served catalogue view (prices are dropped), and the content is checked against the served digest and against the digest you expect (`catalogue_invalid`, `catalogue_digest_mismatch`). The issuer's catalogue read is an operator route: on a deployment it sits behind the operator's authentication, so a relying party obtains the catalogue file and its digest from the operator. The public testnet opens it to anyone as part of its sandbox journey ([the testnet](/en/developers)), and on the local stack, which puts nothing in front of the issuer, it answers on your machine.

A preset is a library helper, not a new contract: the verifier service validates the result at challenge creation exactly as it validates a hand-written agent policy, and the MCP server does not expose presets (it carries the three contract operations only).

## Agents that prove their principal's age (agent profile v2)

An agent credential of profile `agent_v2` carries its principal's **age floor**: the age threshold, in whole years, the person behind the agent proved to the issuer when the agent was minted. The agent proves `age_over N` against it, and the verifier learns `age_over_<N>` and nothing else — no date of birth, no identity. The floor never becomes false; a higher threshold needs a re-mint.

*   **Minting.** The operator's agent offer asks an age floor (one of the issuer's admitted thresholds, 16, 18 and 21 by default). The mint challenge then carries `age_over`, and both clients prove it without any extra call: `AgentClient.issue_from_offer` (Python) and `issueFromOffer` (TypeScript) follow the offer's configuration (`mintid_agent_v2`) and pass the challenge's `age_over` to the holder. A principal younger than the threshold is refused at its own holder, naming `age_over_<N>`; nothing is sent.
*   **Presenting.** A relying party asks a challenge with `profile: "agent_v2"` (verifier contract 1.7.0) and, besides the mandatory agent entries (pairwise pseudonym, `unexpired`), at most one `age_over N`. An `agent_v1` credential never answers it.
*   **Where.** The public testnet's verifier accepts `agent_v2`; its issuer mints `agent_v1` until stated otherwise (an offer with an age floor is refused). Production does not offer it yet.

## The disclosure relay (built, disabled, fail-closed)

A bounded agent is private-only. Beyond it, SPEC-3 defines two ways identity detail can leave on the commercial rail, both decided by the **issuer**, never by the agent: a **consented disclosure** (the principal's policy or a live prompt) and a **mandated identification**: the principal gives its agent a bounded mandate under which the issuer delivers the fields that a governed **obligation class** names (the first classes are three acts of travel: guest registration, vehicle rental, passenger identification) to a counterparty the principal nominated and that the law obliges to record them. The relying party never chooses the fields, and the issuer reads them from its identity provider at the moment of release and keeps none of them.

The agent's part is only to relay. `relay_disclosure(issuer_url, message, prove)` (Python agent client) takes the relying party's signed offer or identification request, asks the agent's own issuer for a single-use relay challenge, and sends the message back with a proof built over that challenge. It refuses before sending anything a message that is not a commercial-rail offer or identification request, or one that carries a consent or decision of its own: the agent never consents. The answer is `{state, reason}`; a release goes from the issuer straight to the relying party's registered endpoint and never passes through the agent.

What to expect today:

*   **The rail is off.** Issuers ship it disabled; a disabled issuer answers `404`, as for a route that does not exist. Mandated identification has a second switch of its own. Turning either on is a governance decision that waits for counsel, per jurisdiction.
*   **Every relay is refused even when it is on.** The relay proof needs a statement set the pinned proof core does not have yet, so there is no default prover, `prove` is yours to supply, and the issuer resolves no agent from any proof until that statement set ships (`subject_unresolved`).
*   **Relying parties** receive releases on an inbox of their verifier service that is likewise off by default — see [service mode](/en/sdk/service-mode).

## Not in this version

*   **Sub-delegation** — an agent minting or delegating to another agent. Gated.

Where to go next:

*   [Payment-identity profile](/en/sdk/payment-identity-profile) — the declaration, headers and session the loop speaks.
*   [Credentials & holders](/en/sdk/credentials-and-holders) — the holder library under both clients.
*   [Reason codes](/en/sdk/reason-codes) — the vocabulary a refusal carries.

[previous← holder · wasm](/en/sdk/holder)[nextagent-to-agent handshake →](/en/sdk/agent-to-agent-handshake)

---
Source: https://mintid.net/en/sdk/agent-clients · Agent clients · MintID SDK docs
