Everything a lender's engineering team needs to wire one signed, time-bounded lending voucher
into their underwriting flow. Plaintext JSON for the bound index fields, opaque
ciphertext for the encrypted financial claim, persisted to
lending_credentials and reflected on the
/lending and
/agent-identity explainers within one render.
/api/lending/credential/issuePlaintext JSON for the bound voucher index fields, opaque ciphertext for the encrypted financial history the integrator produced on-device. One voucher per signature; replay within the validity window re-issues the same voucher deterministically.
The issuer signs a canonical UTF-8 string built from a structured object whose keys are sorted
alphabetically and rendered with no whitespace. The signed bytes cover
voucher_id, user_pubkey, claim_id,
ciphertext_hash, the issuer name, issued_at,
valid_until, and validity_window_seconds. The signed
voucher_id — not the encrypted financial claim — is the audit point:
a regulator confirms issuance without ever holding the borrower's plaintext history.
The time-bounded voucher lifetime is clamped to 60…86400s. The same shape also accepts an absolute ISO-8601 valid_until; the two are mutually exclusive.
JSON.stringify(obj) with keys sorted alphabetically and no whitespace.
voucher_id = "vch_<sha256[:32]>" over
(user_pubkey + ":" + claim_id + ":" + ciphertext_hash + ":" + floor(issued_at_ms / (validity_seconds * 1000)) + ":" + issuer).
Replay short-circuits on the same
(user_pubkey, claim_id, ciphertext_hash, replay_window) tuple — the issuer
returns the existing voucher (same signature, same payload_string, replay: true)
instead of writing a new row.
The signing key is server-owned. When LENDING_ISSUER_PRIVATE_KEY is unset the
endpoint still works but issues vouchers with an ephemeral key (mock_key: true);
those vouchers will not verify across restarts. The fingerprint below is the 16-character SPKI
body prefix a visitor can compare against the issuer detail on
/lending/applications/<voucher_id>
to confirm the issuance event and the lookup describe the same signing identity.
mock_key — LENDING_ISSUER_PRIVATE_KEY is not set on the server. The full PEM below was generated at boot and is unstable across restart; voucher signatures will not verify once the process is replaced.
-----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEA1S5zx5DeS9xUSSBnME3/cxPFhCbPMgpMc5WFrXcaWQ4= -----END PUBLIC KEY-----
All fields are required, except where noted. Any missing or invalid field produces a 400 with the offending fields listed in errors.
| Field | Type | Description |
|---|---|---|
| user_pubkey | string | Borrower public key identifier — the project's chosen "deterministic per-user ID" field. Used as the schema-level partition for the voucher and replay lookup. |
| claim_id | string | Borrower-supplied claim identifier (e.g. "clm_01J9Q"). |
| ciphertext | string | Opaque encrypted financial-history bytes. The server treats this as opaque and never decrypts; it is hashed and persisted verbatim for audit. |
| validity_seconds | integer | Time-bounded voucher lifetime in seconds (60…86400). Mutually exclusive with valid_until. |
| valid_until | string | Absolute expiry timestamp. Server clamps to issued_at + 24h. |
The values below come straight from the live contract — what the integrator sends and what the server signs.
{
"user_pubkey": "pk_loan_4f7e2c1a",
"claim_id": "clm_01J9Q",
"ciphertext": "0x9f3a8c1d_demo_ciphertext_octets",
"validity_seconds": 3600
}
One command — sends the example envelope to the live issuer. The host is whatever the request landed on; the path is the same everywhere.
curl -X POST https://automa-labs-ai.polsia.app/api/lending/credential/issue \
-H "Content-Type: application/json" \
-d '{
"user_pubkey": "pk_loan_4f7e2c1a",
"claim_id": "clm_01J9Q",
"ciphertext": "0x9f3a8c1d_demo_ciphertext_octets",
"validity_seconds": 3600
}'
user_pubkey and claim_id with the per-borrower values your underwriting service generated; ciphertext is the opaque encrypted financial-history bytes the browser produced on-device — the server treats it as opaque, hashes it, persists it for audit, and never decrypts it. validity_seconds and valid_until are mutually exclusive: supply exactly one, in [60, 86400]… seconds or an ISO-8601 timestamp clamped to issued_at + 24h.
Every terminal status the integrator needs to handle.
| Status | Body | When |
|---|---|---|
| 200 | {"ok":"true","voucher_id":"string (vch_<sha256[:32]>)","user_pubkey":"string","claim_id":"string","issuer":"string","issued_at":"ISO-8601 string","valid_until":"ISO-8601 string","signature":"string (128-hex Ed25519)","payload_string":"string (canonical UTF-8 bytes the issuer signed)","ciphertext_hash":"string (64-hex sha256 over the opaque ciphertext)","determinism":{"replay_window_seconds":"integer"},"issuer_pub_key_pem":"string (PEM SPKI Ed25519)","mock_key":"boolean","replay":"boolean — true if this is a same-window replay (no DB write)"} |
Voucher issued or replayed: signed canonical payload persisted to lending_credentials; voucher_id, signature, payload_string, and issuer_pub_key_pem returned. replay: true indicates a same-window replay (no DB write). |
| 400 | {"ok":"false","errors":["string"]} |
Validation error — one or more required fields are missing, wrong type, or validity_seconds / valid_until was supplied incorrectly. |
A borrower produces an encrypted financial-history claim on-device, the issuer signs a
deterministic time-bounded voucher over the claim hash, and a lender settles against the
voucher without ever receiving plaintext financials — the
/lending flow end-to-end. Apply at
/lending/apply, track any
vch_<hex> voucher at
/lending/applications/<voucher_id>,
and drop the curl above into your underwriting pipeline to issue one signer-on-behalf flow.
Two distinct borrowers always produce two distinct voucher IDs even when the ciphertext
itself stays opaque: the vch_<sha256[:32]> namespace is portable, the
replay window is floored by validity_seconds, and the signing identity is
server-owned — the issuance event is the audit point, never the encrypted financial
history.
Every accepted voucher is written to the lending_credentials table, keyed UNIQUE on
voucher_id, and indexed on user_pubkey and valid_until.
The same contract — including the example envelope and the response shapes — is
published as plain JSON at
https://automa-labs-ai.polsia.app/api/lending/credential/issue, so an integrator
can pull the spec straight off the live endpoint rather than caching this page.