delivery_mode field so the lender knows which mode was selected.
Delivery modes
| Mode | What lender renders | What Vault returns | Description |
|---|---|---|---|
embedded | An <iframe> pointing at embed_url | embed_url, expires_at, token, asset_transfer_instruction_id | The borrower stays inside the lender’s chrome. The lender listens to postMessage lifecycle events from the iframe. |
hosted | A full-page redirect to redirect_url | redirect_url, expires_at, token, asset_transfer_instruction_id | The borrower is redirected to a Vault-branded page and returned to the lender via a return_url. |
- The same
POST /api/v1/loans/{loan_ref}/transfer-launchendpoint. - The same
Idempotency-Keyand bearer-token contract. - The same underlying provider transfer instruction (
asset_transfer_instruction_idis the canonical correlation key in both modes).
delivery_mode field tells the lender which mode was selected so the frontend can render accordingly.
Architecture overview
- Borrower is authenticated in lender UI.
- Lender backend calls
POST /api/v1/loans/{loan_ref}/transfer-launch. - Vault resolves the delivery mode, loan/asset/custody context, creates a mode-specific session, and returns either
embed_url(embedded) orredirect_url(hosted), plus a short-livedtokenand theasset_transfer_instruction_id. - Lender reads
delivery_modefrom the response and renders accordingly:- Embedded: mounts an iframe pointed at
embed_urland listens forvault.asset_transfer.*events viapostMessage. - Hosted: navigates the borrower to
redirect_url. Vault’s hosted page runs the provider flow and redirects the borrower back toreturn_urlwith?status=...&transfer_id=...query parameters.
- Embedded: mounts an iframe pointed at
Prerequisites
- Vault API reachable from lender backend (for example
http://localhost:8000in dev). - Vault UI reachable from borrower browser (for example
http://localhost:5173in dev). - Lender backend has a Vault bearer token with scopes
collateral:writeandloans:read. - Lender frontend origin (for example
http://localhost:3000) should be sent as theOriginheader on the launch call (required when Vault resolves to embedded mode). - Lender provides a
return_urlin the request body (required when Vault resolves to hosted mode).
Backend launch call
Both modes use the same endpoint:POST /api/v1/loans/{loan_ref}/transfer-launch- Required scopes:
collateral:writeandloans:read - Required headers:
Authorization: Bearer <token>Idempotency-Key: stable client-generated key for one launch action.Origin: required when Vault resolves to embedded mode (for parent-frame pinning). Safe to send unconditionally.
- Path parameters:
loan_ref(required string).
- Request body fields:
asset_ref(string, required)quantity(number or numeric string, required)custody_provider_code(string, required)return_url(string, optional). The lender URL the borrower returns to after success/cancel/error. Required when Vault resolves to hosted mode; ignored for embedded mode.lender_display_name(string, optional, max 120 chars). Brand string rendered to the borrower in the Vault-hosted page header. Used only in hosted mode. When omitted, Vault falls back to the calling lender party’slegal_namefromentity_registry.lender_logo_url(string, optional, max 2048 chars). URL of the lender’s logo, rendered next to the lender name in the hosted page header. Used only in hosted mode. Must behttpsin production;httpis permitted only when the launch request itself is made overhttp(covers local dev).data:and other non-http(s) schemes are rejected with 422. There is no fallback for the logo — when omitted, the page renders only the lender name.
return_url, Origin, and branding fields on every request. Vault uses only the fields relevant to the resolved mode and ignores the rest.
Response shape
A successful launch always returns:status("ok"on success)delivery_mode(echoes the resolved mode)expires_attoken(short-lived, single-use)asset_transfer_instruction_id
embed_url(whendelivery_mode = "embedded") — the URL to mount in the iframe; the token is in the path andparent_originis appended as a query param.redirect_url(whendelivery_mode = "hosted") — the URL to navigate the browser to; the token is in the path.
null. Lender code should treat the launch artifacts as conditional on status == "ok" and on the expected URL field being present, and should fail closed otherwise.
Validation rules
| Condition | HTTP status | Notes |
|---|---|---|
Idempotency-Key header missing | 422 | Required for all launches. |
Vault resolves to embedded + Origin header missing | 422 | Embedded launches must pin the parent origin. |
Vault resolves to hosted + return_url missing/empty | 422 | Hosted launches must provide a borrower-return URL. |
lender_logo_url not http(s) (e.g. data:, javascript:) | 422 | Only http(s) URLs are accepted. |
lender_logo_url is http://... and the launch request is https://... | 422 | http URLs only allowed when the launch request itself is on http. |
Origin header and include a return_url in the body, so the request succeeds regardless of which mode Vault selects.
Example backend route (Node/Express)
This example sends the core transfer fields plusreturn_url and Origin unconditionally, then reads delivery_mode from the Vault response to decide what to return to the frontend:
Embedded mode — frontend iframe + event listener
RenderembedUrl in an iframe and consume only the documented contract events from Vault:
vault.asset_transfer.readyvault.asset_transfer.completedvault.asset_transfer.errorvault.asset_transfer.closed
Example frontend component (React)
Embedded event contract
Vault posts messages to the parent window using theparent_origin value baked into embed_url.
vault.asset_transfer.ready— payload: none.vault.asset_transfer.completed— payload:{ status }with valuespending,succeeded,failed,success(alias ofsucceeded),failure(alias offailed), orunknown.vault.asset_transfer.error— payload:{ message }.vault.asset_transfer.closed— payload: none.
- Validate
event.originagainst the Vault UI origin. - Consume only documented contract event types and ignore anything else.
- Keep the iframe mounted until
vault.asset_transfer.closed. - Use
vault.asset_transfer.completedto capture the transfer outcome — do not advance lender workflow onclosedalone. - If
closedarrives beforecompleted, treat the launch as cancelled or abandoned.
Hosted mode — full-page redirect + return URL
Hosted mode replaces the iframe +postMessage contract with a full-page navigation to a Vault-owned page and a redirect back to the lender’s return_url when the flow ends.
Lifecycle
-
Lender backend calls
transfer-launchwith areturn_urlin the body. Vault resolves to hosted mode. -
Lender frontend (or backend) navigates the borrower to the
redirect_urlreturned in the response (a/asset_transfer/hosted/{token}URL on Vault). -
Vault’s hosted page resolves the token, runs the provider transfer flow, and on terminal events redirects the borrower’s browser to:
-
Lender’s return-URL handler reads
statusandtransfer_idfrom the query string and finishes the borrower’s lender-side flow.
Status enum on the return URL
status | Meaning |
|---|---|
completed | Provider reported a terminal success (success exit or transferFinished callback observed). |
cancelled | Borrower closed the Vault page without a terminal success and without a provider error. |
error | Provider reported a terminal error or Vault could not run the flow to completion. |
transfer_id query param is always Vault’s asset_transfer_instruction_id for the launch — use it as the canonical correlation key for any follow-up reconciliation.
Example: lender backend kicks off a hosted launch and 302s the borrower
Example: lender return-URL handler
Hosted-mode requirements
return_urlshould be HTTPS in non-dev environments and should resolve to a lender-owned host.- Do not assume the borrower returns synchronously — always reconcile by
transfer_idif the user closes the tab before redirect. - Treat
?status=strictly as one of the documented values; reject unknown statuses defensively. - Do not parse provider-specific data from the query string; Vault deliberately exposes only
statusandtransfer_id.
Hosted-page branding
The Vault-hosted page renders a header band with the Vault wordmark on the left and the lender’s brand on the right. The lender brand has two parts:- Lender display name. Used as the right-side label in the header and substituted into the borrower-facing copy (“You’ll be returned to when finished” and the error helpers). When the lender does not supply
lender_display_name, Vault looks up the calling lender party’slegal_namefromentity_registryand uses that. If neither source is available the page falls back to the generic “your lender” copy and shows just the Vault wordmark in the header. - Lender logo. Rendered as a small image to the left of the display name when
lender_logo_urlis supplied. There is no fallback. The image is loaded withreferrer-policy: no-referrer; if the URL fails to load the image is hidden silently and the display name still renders.
Understanding the delivery modes
Vault selects the delivery mode. The lender frontend should handle both modes by reading thedelivery_mode field from the launch response and rendering accordingly:
- embedded (
embed_urlpresent): mount an iframe and wire up thepostMessagelistener. Best when the lender app can host an iframe and you control its CSP /frame-ancestors. - hosted (
redirect_urlpresent): navigate the borrower to the redirect URL. Best for mobile webviews, third-party shells, or environments where iframes are restricted.
Origin and security requirements
- Never expose the Vault bearer token to the browser.
- For embedded mode, the
Originheader on the launch request must exactly match the lender UI origin (scheme + host + port). Avoid mixinglocalhostand127.0.0.1— they are distinct origins. - For embedded mode, validate
event.originagainst the Vault UI origin in the frontend listener. - For hosted mode, treat
return_urlas part of your security boundary: only register URLs you control, and validate?statusand?transfer_iddefensively before acting on them. - Treat all session tokens (
tokenreturned from the launch) as short-lived and single-use. Re-launch on any retry; never reuse a staleembed_urlorredirect_url.
Idempotency and duplicate-launch prevention
The launch endpoint is idempotent on theIdempotency-Key header per (party, loan_ref). The fingerprint covers the caller-supplied request fields (asset_ref, quantity, custody_provider_code, return_url, etc.), so reusing a key with different request fields returns 409 — generate a fresh key when the borrower starts a new launch action.
Lender-side controls that apply to both modes:
- Single-flight guard per idempotency key in the lender backend.
- Disable the launch UI while the call is in flight.
- Carry
X-Request-Idthrough logs for cross-system traceability.
- Embedded: render only one iframe per
embed_url; do not auto-mount on every framework lifecycle hook. - Hosted: issue the redirect once per launch; if the borrower hits the back button, re-launch (do not reuse the prior
redirect_url).
Observability and dashboards
All Vault asset transfer logs and metrics are tagged withdelivery_mode so embedded vs hosted volumes and error rates can be split:
- Logs. Vault logs the resolved
delivery_modeon launch (transfer_launch requested ... delivery_mode=...), session creation, event ingestion, and terminal events. Filter or facet ondelivery_modewhen investigating a launch. - Metrics / dashboards. Recommend a dashboard panel split per
delivery_modefor at minimum:- launch success vs failure rate,
- terminal outcome distribution (
completed/cancelled/errorfor hosted;completedstatus /error/closed-without-completedfor embedded), - p50/p95 time-to-terminal-event,
- 4xx vs 5xx rate on the launch endpoint.
- Alerting. Tune alert thresholds per mode — embedded and hosted have different baseline error/abandon rates and should not share a single SLO.
- Reconciliation. Always reconcile by
asset_transfer_instruction_id; the same instruction is referenced by bothembed_url/redirect_urlpayloads and downstream provider events.
Local testing checklist
- Start Vault API and Vault UI, plus the lender backend and frontend.
- Embedded: set
DEFAULT_ASSET_TRANSFER_DELIVERY_MODE=embeddedon the Vault API, launch a transfer, confirm the response hasdelivery_mode = "embedded"and the iframe loads/asset_transfer/embed/{token}, and observe theready→completed→closedevent sequence. - Hosted: set
DEFAULT_ASSET_TRANSFER_DELIVERY_MODE=hostedon the Vault API, launch with areturn_url, confirm the response hasdelivery_mode = "hosted"and the borrower lands on/asset_transfer/hosted/{token}, runs the flow, and is redirected back to{return_url}?status=...&transfer_id=.... - Confirm one-time token behavior: re-loading the embed/hosted URL should not re-run the flow.
- Confirm idempotency: replaying the launch with the same
Idempotency-Keyand identical body returns the original launch artifacts; with a different body it returns 409. - Confirm the lender frontend handles both modes by reading
delivery_modefrom the response.
Demo environment variables
Backend:VAULT_API_BASE_URL=http://localhost:8000VAULT_BEARER_TOKEN=<service-token-with-collateral-write-and-loans-read>LENDER_UI_ORIGIN=http://localhost:3000
VAULT_UI_ORIGIN=http://localhost:5173
Production hardening
- Add correlation IDs from the lender request through Vault APIs for traceability.
- Add bounded retry/backoff only for transient session-bootstrap network failures, not for provider-side failures.
- Provide a graceful fallback UX if launch fails (for example, fall back to hosted if embedded refuses to load behind a strict CSP).
- Instrument completion/error metrics by lender tenant and integration partner, split by
delivery_mode.