Reference · Business Wallet API · v1
CodeB Business Wallet — REST API reference
Every endpoint below is served by business-wallet.ashx on the tenant hostname. All endpoints are HTTPS-only. Authentication is one of: none (public), OIDC bearer (admin), HMAC token (public but proof-signed), or invite HMAC (public, invite-signed).
Multi-wallet model. One tenant hosts many business wallets. Each wallet has a companyId ([A-Za-z0-9_-]{4,40}) selected via ?bw=<companyId>. Path-scoped DID: did:web:<tenant>:business-wallets:<companyId>. Storage: App_Data/<tenant>/business-wallets/<companyId>/.
Wallet lifecycle
| Method | Path | Auth | Description |
| GET | ?action=list-wallets | public | List PUBLIC wallets on this tenant (filters phantom dirs by requiring state.json; excludes wallets marked publish=false). |
| GET | ?action=list-my-wallets | bearer | List wallets on this tenant where the caller (email or wallet_id) appears in the employees table. Includes private wallets the caller belongs to. |
| GET | ?action=list-all-wallets | bearer | Tenant-admin enumeration. Returns every wallet regardless of the publish flag, including _admin sink and .deleted-* tombs categorised in the response for the admin dashboard. |
| POST | ?action=create-wallet[&slug=<hint>] | bearer | Mint a new wallet with fresh companyId. Body: {name*, reg, juris, vat, addr}. Creator auto-enrolled as director. |
| POST | ?action=delete-wallet&bw=<cid> | bearer | Tenant-admin only. Moves the wallet directory to .deleted-<cid>-<ts> tomb (never hard-deleted). Bearer requires tenant-admin scope; the wallet's own directors do not qualify. |
| POST | ?action=delete-phantoms | bearer | Sweeps wallet directories missing state.json (orphans) into the trash and returns a categorised report {orphans, tombs, admin_sinks, kept}. Idempotent. |
| GET | ?action=identity[&bw=<cid>] | public | Tenant-level identity if bw omitted; per-wallet identity when supplied. |
| GET | ?action=public-view&bw=<cid> | public | Anonymous read-only projection of a wallet. Returns {company{name,reg,juris,vat,addr,did}, employees[{name,role}], doc_count, vc_count, tenant, wallet_id, bw}. No email, no wallet_id per employee, no doc contents, no audit, no RPs. Private wallets return 404 wallet_not_found to unauthenticated callers (indistinguishable from a wallet that does not exist). |
| GET | /business-wallets/<cid>/did.json | public | DID Document for this business (path-rewritten to ?action=bw-did-doc). 404 if wallet doesn't exist. |
| POST | ?action=resolve-identity | public | Best-effort lookup of {name, email} from a DID (fetches remote OpenID Federation entity statement). SSRF-hardened, HTTPS-only, no redirects. |
list-wallets response
{
"tenant": "phone.aloaha.com",
"count": 2,
"wallets": [
{ "id": "acme-ltd",
"name": "Acme Ltd",
"reg": "C-99999",
"juris": "MT",
"vat": "MT12345678",
"did": "did:web:phone.aloaha.com:business-wallets:acme-ltd",
"did_doc_url": "https://phone.aloaha.com/business-wallets/acme-ltd/did.json",
"url": "https://phone.aloaha.com/web-business-wallet.html?bw=acme-ltd",
"wallet_id": "urn:aloaha:web-wallet:xyz9YtGL8W...",
"created": "2026-08-07T09:12:44Z" }
]
}
create-wallet request
POST /business-wallet.ashx?action=create-wallet&slug=acme-ltd
Authorization: Bearer <oidc-token>
Content-Type: application/json
{ "name": "Acme Ltd",
"reg": "C-99999",
"juris":"MT",
"vat": "MT12345678",
"addr": "Level 3, Sky Parks, Malta" }
State + directory
| Method | Action (all require &bw=<cid>) | Auth | Description |
| GET | get-state | bearer | Company profile + employees + relying parties + doc index + audit-log count. |
| POST | save-company | bearer | Update company block. Body: {name, reg, juris, vat, addr, didMethod}. |
| POST | add-employee | bearer | Add employee. Body: {name*, email?, wallet_id?, role}. At least one of email or wallet_id required. |
| POST | update-employee-role | bearer | {id, role} where role is director|secretary|legal|compliance|employee. |
| POST | remove-employee | bearer | {id}. |
| POST | add-rp | bearer | Register relying party. Body: {name, kind, did, email?, mobile?}. DID accepts did:web:, did:key:, did:jwk:, did:webvh:, or urn:aloaha:web-wallet:. |
| POST | remove-rp | bearer | {id}. Cascade-revokes all doc grants for that RP. |
Documents (PAdES-B-B inline signer)
| Method | Action (+&bw=<cid>) | Auth | Description |
| GET | list-docs | bearer | All docs' meta.json for this wallet. |
| POST | upload-doc | bearer | Multipart. Fields: file, title, type, issuer, sign_by_uploader, wallet_seal. When either checkbox is set and the file is a PDF, PadesInlineSigner.SignPdfBB seals the PDF synchronously before storage. |
| GET | download-doc&id=<docId> | bearer | Streams the stored (signed) bytes. |
| POST | delete-doc | bearer | {id}. Removes .bin + .meta.json. |
| POST | grant-doc | bearer | {docId, rpId}. Grants an RP read access. |
| POST | revoke-doc | bearer | {docId, rpId}. |
| POST | share-now | bearer | Simulate fan-out reads to every granted RP. Writes audit entries. |
| GET | list-shared-in | bearer | Documents that OTHER wallets on this tenant have granted to this wallet (via an RP enrollment that names our did:web or urn:aloaha:web-wallet:). Read-only, capped at 500 shared docs. Skips _admin sink and .deleted-* tombs. Returns {shared:[{doc_id, doc_title, doc_type, doc_vct, doc_issuer, doc_size, shared_by_wallet_id, shared_by_wallet_name, shared_by_did, granted_via_rp_name, download_url, shared_at}], count, scanned_wallets}. |
| GET | cross-download&from=<theirCid>&doc=<docId> | bearer | Stream bytes of a doc that theirCid has granted TO the caller's wallet. Server re-verifies the grant (RP enrolled by theirCid must carry our DID or URN, and meta.grants must include that RP). 403 not_granted_to_you if the check fails. Audit lines land in BOTH wallets. |
External sharing (HMAC + invites)
| Method | Action | Auth | Description |
| POST | create-share-link (+&bw) | bearer | Body: {docId, ttl_sec}. Returns {url, status_index, revoke_url}. Token layout: <cid>.<docId>.<exp>.<sig>.<statusIdx>. Canon: bw|share|<tenant>|<cid>|<docId>|<exp>|<statusIdx>. |
| GET | share-download&t=<token> | HMAC | Public. Bounces 404 if wallet doesn't exist (no side-effect dir creation). |
| POST | revoke-share&index=<N> | bearer | Sets bit N in the wallet's Bitstring Status List. Existing tokens fail with 410. |
| POST | invite-countersign (+&bw) | bearer | Body: {docId, inviteeName, inviteeEmail, inviteeMobile, inviteeDid, status, ttl_sec}. Returns HMAC-signed invite URL. |
| POST | countersign | invite OR HMAC/DID/PID/OIDC | Two modes: invite_token (public with valid invite) OR direct auth via IdentifyRegistryPusher. |
Wallet-Instance-Attestation + credential issuance
| Method | Action (+&bw) | Auth | Description |
| POST | wia-for-employee | bearer | Mints a WIA JWT (typ=wallet-attestation+jwt, ES256) over a caller-supplied public JWK. Client keeps the private half; server never sees it. Body: {empId, cnf_jwk:{kty:"EC",crv:"P-256",x,y}}. |
| POST | issue-employee-cred | bearer | OID4VCI pre-authorised push from company to employee. Writes offer under App_Data/<tenant>/vci/offers/<offerId>.json (contract matches vci.ashx exactly). Body: {empId, vct, claims}. Returns {offer_uri, deep_link, pre_authorized_code, expires_utc}. |
| POST | mdoc-issue | bearer | Enqueues an mDoc (COSE_Sign1) signing job. Body: {empId, docType}. |
Verifiable-credential vault (company-held VCs)
| Method | Action (+&bw) | Auth | Description |
| POST | store-vc | bearer | Body: {vc_jwt}. Stores at vcs/<vcId>.jwt + meta. vcId = sha256(vc_jwt)[:16]. |
| GET | list-vc | bearer | Array of {vcId, vct, issuer, iss, exp, iat, sub}. |
| GET | get-vc&id=<vcId> | bearer | Raw JWT for client-side verification + card render. |
| POST | delete-vc | bearer | {id}. |
| POST | verify-vc | bearer | Loopback to /vcdm.ashx?action=verify; falls back to in-process ES256 verify. Returns {ok, verified, reason}. |
Registry push (external submitters)
Banks, business registries, notaries push documents into a wallet without needing an OIDC account. Three auth methods:
- Company DID —
X-Push-Auth: company + X-Push-Did: did:web:<X> where X is in this tenant's trust list. Actor logged as registry:<did>.
- Personal PID —
X-Push-Auth: pid + X-Push-Vp: <SD-JWT VC>. VP verified via /vcdm.ashx?action=verify. Actor logged as pid:<subject>.
- OIDC Bearer — standard admin token. Actor logged as
oidc:<email>.
| Method | Action (+&bw) | Auth | Description |
| POST | registry-push | one of 3 | Multipart upload same shape as upload-doc. Notifies directors + secretaries by email/WhatsApp. |
Encrypted backup + presentation scaffolding
| Method | Action (+&bw) | Auth | Description |
| POST | wallet-export | bearer | Body: {passphrase}. Returns encrypted binary blob (CBBW2 header, AES-256-CBC + HMAC-SHA256 encrypt-then-MAC covering hdr+salt+IV+ct, PBKDF2 100k iterations). |
| POST | wallet-import | bearer | Verify-only in current release. Decrypts + MAC-verifies; returns {verified:true, restored:false}. |
| GET | ble-presentation&mode=qr|ble|nfc | bearer | Returns a 5-minute presentation nonce + deep link (mdoc-openid4vp://<host>?nonce=<N>). |
Audit
| Method | Action (+&bw) | Auth | Description |
| GET | audit-log&limit=<N> | bearer | Tail N entries (default 100, max 2000). Each entry: {ts, actor, action, target}. |
AI Agent Identity
Any employee whose role is agent becomes a machine identity in the wallet. Each agent gets a fragment DID under the wallet, has a stored capability list and a maximum token TTL, and can be issued short-lived ES256 capability tokens signed by the tenant issuer. Downstream services fetch the public agent manifest to learn what the agent is + who owns it, then call verify-agent-token on any received token. See the explainer at ai-agent-identity.html.
Agent-aware add-employee body
POST /business-wallet.ashx?action=add-employee&bw=<cid>
Authorization: Bearer <OIDC token>
Content-Type: application/json
{
"name": "invoice-triage",
"role": "agent",
"agent_model": "large-model",
"agent_provider_hint": "acme-agents",
"agent_purpose": "Reads incoming invoices and files them",
"agent_capabilities": ["list-docs","download-doc","verify-vc","file-invoice"],
"agent_max_ttl_sec": 3600
}
Non-agent roles that carry any agent_* field are rejected with 400 agent_fields_only_valid_for_agent_role. Capability entries limited to 64 chars, [a-zA-Z0-9._-]. TTL clamped to 60–86400 s.
Endpoints
| Method | Action (+&bw) | Auth | Description |
| GET | list-agents | bearer | List every employee row with role=agent. Returns {agents, count}. |
| POST | mint-agent-token | bearer | Body {empId, ttl_sec, scope[], audience?, purpose_hint?}. Mints ES256 JWT (typ=codeb-agent-token+jwt) signed by tenant issuer. |
| POST | deactivate-agent | bearer | Body {id}. Flips agent_active=false. Existing tokens fail verification immediately. |
| GET | agent-manifest&emp=<empId> | public | Public descriptor: name, model, capabilities, active flag, DID, owner DID, issuer JWK, revocation URL. |
| GET/POST | verify-agent-token | public | Body {token} (POST) or ?token= (GET). Returns {verified, agent_did, owner_did, scope, exp, reason?}. |
| GET | agent-status-list | public | W3C BitstringStatusList v1.0 stub. Per-token bit-flipped revocation to follow. |
mint-agent-token response
{
"ok": true,
"token": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImNvZGViLWFnZW50LXRva2VuK2p3dCJ9....",
"exp": 1786320000,
"iat": 1786316400,
"agent_did": "did:web:tenant.example:business-wallets:acme-ltd#agent-e_abc123",
"owner_did": "did:web:tenant.example:business-wallets:acme-ltd",
"scope": ["list-docs","download-doc","verify-vc"],
"typ": "codeb-agent-token+jwt",
"ttl_sec": 3600
}
agent-manifest response
{
"id": "e_abc123",
"name": "invoice-triage",
"role": "agent",
"agent_model": "large-model",
"agent_provider_hint": "acme-agents",
"agent_purpose": "Reads incoming invoices and files them",
"agent_capabilities": ["list-docs","download-doc","verify-vc"],
"agent_max_ttl_sec": 3600,
"agent_active": true,
"agent_did": "did:web:tenant.example:business-wallets:acme-ltd#agent-e_abc123",
"owner_did": "did:web:tenant.example:business-wallets:acme-ltd",
"issuer": "did:web:tenant.example:business-wallets:acme-ltd",
"manifest_url": "https://tenant.example/business-wallet.ashx?action=agent-manifest&bw=acme-ltd&emp=e_abc123",
"did_doc_url": "https://tenant.example/business-wallets/acme-ltd/did.json",
"token_type": "codeb-agent-token+jwt",
"issuer_key": { "kty":"EC","crv":"P-256","x":"...","y":"...","use":"sig","alg":"ES256" },
"revocation": {
"type": "BitstringStatusList",
"url": "https://tenant.example/business-wallet.ashx?action=agent-status-list&bw=acme-ltd"
}
}
verify-agent-token response
{
"verified": true,
"reason": "ok",
"agent_did": "did:web:...#agent-e_abc123",
"owner_did": "did:web:...",
"iss": "did:web:...",
"exp": 1786320000,
"scope": ["list-docs","download-doc"],
"typ": "codeb-agent-token+jwt"
}
Failure reasons: not_compact_jws, malformed_segment:*, unsupported_alg:*, token_expired, iss_not_this_tenant, iss_bad_companyId, agent_did_missing_fragment, issuer_pfx_missing, bad_signature, agent_not_found, employee_not_agent, agent_deactivated. Verifier never leaks internal state on failure — every reason is a stable machine-readable slug.
Sibling endpoints
SIP softphone provisioning (sipprov.ashx)
Short-TTL HMAC token that resolves to a Zoiper XML, Linphone .rc, or JSON provisioning payload. Prevents plaintext SIP passwords in QR codes.
| POST | ?action=create-token | bearer | Body: {aor, password, ttl, format, label}. Returns short URL + expiry. |
| GET | ?t=<token>[&format=zoiper|linphone|json] | HMAC | Single-use. Delete-before-write race guard. |
| POST | ?action=revoke&tid=<tid> | bearer | Force-expire a token. |
| GET | ?action=list | bearer | Outstanding tokens for this tenant. |
Standards
OID4VP 1.0 FINAL · OID4VCI Draft-15 · SD-JWT VC draft-07 · W3C VCDM 2.0 (JOSE envelope) · W3C DID Core 1.0 · W3C Bitstring Status List v1.0 · ETSI TS 119 602 Trusted List · ETSI EN 319 102-1 PAdES-B-B · HAIP ยง5.11 Wallet Attestation · European Digital Identity Wallet (eIDAS 2.0, ARF 3.0).