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

MethodPathAuthDescription
GET?action=list-walletspublicList PUBLIC wallets on this tenant (filters phantom dirs by requiring state.json; excludes wallets marked publish=false).
GET?action=list-my-walletsbearerList 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-walletsbearerTenant-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>]bearerMint a new wallet with fresh companyId. Body: {name*, reg, juris, vat, addr}. Creator auto-enrolled as director.
POST?action=delete-wallet&bw=<cid>bearerTenant-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-phantomsbearerSweeps 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>]publicTenant-level identity if bw omitted; per-wallet identity when supplied.
GET?action=public-view&bw=<cid>publicAnonymous 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.jsonpublicDID Document for this business (path-rewritten to ?action=bw-did-doc). 404 if wallet doesn't exist.
POST?action=resolve-identitypublicBest-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

MethodAction (all require &bw=<cid>)AuthDescription
GETget-statebearerCompany profile + employees + relying parties + doc index + audit-log count.
POSTsave-companybearerUpdate company block. Body: {name, reg, juris, vat, addr, didMethod}.
POSTadd-employeebearerAdd employee. Body: {name*, email?, wallet_id?, role}. At least one of email or wallet_id required.
POSTupdate-employee-rolebearer{id, role} where role is director|secretary|legal|compliance|employee.
POSTremove-employeebearer{id}.
POSTadd-rpbearerRegister relying party. Body: {name, kind, did, email?, mobile?}. DID accepts did:web:, did:key:, did:jwk:, did:webvh:, or urn:aloaha:web-wallet:.
POSTremove-rpbearer{id}. Cascade-revokes all doc grants for that RP.

Documents (PAdES-B-B inline signer)

MethodAction (+&bw=<cid>)AuthDescription
GETlist-docsbearerAll docs' meta.json for this wallet.
POSTupload-docbearerMultipart. 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.
GETdownload-doc&id=<docId>bearerStreams the stored (signed) bytes.
POSTdelete-docbearer{id}. Removes .bin + .meta.json.
POSTgrant-docbearer{docId, rpId}. Grants an RP read access.
POSTrevoke-docbearer{docId, rpId}.
POSTshare-nowbearerSimulate fan-out reads to every granted RP. Writes audit entries.
GETlist-shared-inbearerDocuments 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}.
GETcross-download&from=<theirCid>&doc=<docId>bearerStream 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)

MethodActionAuthDescription
POSTcreate-share-link (+&bw)bearerBody: {docId, ttl_sec}. Returns {url, status_index, revoke_url}. Token layout: <cid>.<docId>.<exp>.<sig>.<statusIdx>. Canon: bw|share|<tenant>|<cid>|<docId>|<exp>|<statusIdx>.
GETshare-download&t=<token>HMACPublic. Bounces 404 if wallet doesn't exist (no side-effect dir creation).
POSTrevoke-share&index=<N>bearerSets bit N in the wallet's Bitstring Status List. Existing tokens fail with 410.
POSTinvite-countersign (+&bw)bearerBody: {docId, inviteeName, inviteeEmail, inviteeMobile, inviteeDid, status, ttl_sec}. Returns HMAC-signed invite URL.
POSTcountersigninvite OR HMAC/DID/PID/OIDCTwo modes: invite_token (public with valid invite) OR direct auth via IdentifyRegistryPusher.

Wallet-Instance-Attestation + credential issuance

MethodAction (+&bw)AuthDescription
POSTwia-for-employeebearerMints 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}}.
POSTissue-employee-credbearerOID4VCI 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}.
POSTmdoc-issuebearerEnqueues an mDoc (COSE_Sign1) signing job. Body: {empId, docType}.

Verifiable-credential vault (company-held VCs)

MethodAction (+&bw)AuthDescription
POSTstore-vcbearerBody: {vc_jwt}. Stores at vcs/<vcId>.jwt + meta. vcId = sha256(vc_jwt)[:16].
GETlist-vcbearerArray of {vcId, vct, issuer, iss, exp, iat, sub}.
GETget-vc&id=<vcId>bearerRaw JWT for client-side verification + card render.
POSTdelete-vcbearer{id}.
POSTverify-vcbearerLoopback 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:

  1. Company DIDX-Push-Auth: company + X-Push-Did: did:web:<X> where X is in this tenant's trust list. Actor logged as registry:<did>.
  2. Personal PIDX-Push-Auth: pid + X-Push-Vp: <SD-JWT VC>. VP verified via /vcdm.ashx?action=verify. Actor logged as pid:<subject>.
  3. OIDC Bearer — standard admin token. Actor logged as oidc:<email>.
MethodAction (+&bw)AuthDescription
POSTregistry-pushone of 3Multipart upload same shape as upload-doc. Notifies directors + secretaries by email/WhatsApp.

Encrypted backup + presentation scaffolding

MethodAction (+&bw)AuthDescription
POSTwallet-exportbearerBody: {passphrase}. Returns encrypted binary blob (CBBW2 header, AES-256-CBC + HMAC-SHA256 encrypt-then-MAC covering hdr+salt+IV+ct, PBKDF2 100k iterations).
POSTwallet-importbearerVerify-only in current release. Decrypts + MAC-verifies; returns {verified:true, restored:false}.
GETble-presentation&mode=qr|ble|nfcbearerReturns a 5-minute presentation nonce + deep link (mdoc-openid4vp://<host>?nonce=<N>).

Audit

MethodAction (+&bw)AuthDescription
GETaudit-log&limit=<N>bearerTail 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

MethodAction (+&bw)AuthDescription
GETlist-agentsbearerList every employee row with role=agent. Returns {agents, count}.
POSTmint-agent-tokenbearerBody {empId, ttl_sec, scope[], audience?, purpose_hint?}. Mints ES256 JWT (typ=codeb-agent-token+jwt) signed by tenant issuer.
POSTdeactivate-agentbearerBody {id}. Flips agent_active=false. Existing tokens fail verification immediately.
GETagent-manifest&emp=<empId>publicPublic descriptor: name, model, capabilities, active flag, DID, owner DID, issuer JWK, revocation URL.
GET/POSTverify-agent-tokenpublicBody {token} (POST) or ?token= (GET). Returns {verified, agent_did, owner_did, scope, exp, reason?}.
GETagent-status-listpublicW3C 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-tokenbearerBody: {aor, password, ttl, format, label}. Returns short URL + expiry.
GET?t=<token>[&format=zoiper|linphone|json]HMACSingle-use. Delete-before-write race guard.
POST?action=revoke&tid=<tid>bearerForce-expire a token.
GET?action=listbearerOutstanding 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).