> For the complete documentation index, see [llms.txt](https://docs.toolkit.equs.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.toolkit.equs.ai/docs/presentation/flows-1-and-2/build-your-own-ui.md).

# Method 2: Build Your Own UI

Own the Holder-facing surface and drive Presentation from your back end

[Method 1](/docs/presentation/flows-1-and-2/embed-ui-and-api.md) hands the Holder's part to Embed UI. This page is for teams that want that part themselves - because the experience has to match an existing product, because the journey is machine-to-machine with no browser at all, or because you need control Embed UI doesn't expose.

The platform's role doesn't change: it still builds signed presentation requests and still verifies responses. What changes is that everything between those two calls is yours.

**Before you start** you need [a Verifier and a VP Schema](/docs/presentation/configuration.md), and a decision about [which flow](/docs/presentation/choose-a-flow.md) you're running - it changes the request you create and where the response arrives.

### What you own

<table data-search="false"><thead><tr><th>Step</th><th>Whose code</th><th>Notes</th></tr></thead><tbody><tr><td>Create the request</td><td>Your back end</td><td>Protected call - carries your agent token, so it can't run in the browser</td></tr><tr><td>Get it to the Holder</td><td>You</td><td>QR code, deep link, or the DC API call - whatever suits the channel</td></tr><tr><td>Collect the response</td><td>Depends on the flow</td><td>Nothing to do on DC API; one fetch on the pull flow</td></tr><tr><td>Bridge a cross-device session</td><td>You, or EQUS</td><td>The <code>requests/multi</code> call hands this to an EQUS-hosted screen - see step 2</td></tr><tr><td>Verify</td><td>Your back end</td><td>Protected call</td></tr></tbody></table>

The two protected calls are the constraint worth designing around: whatever your front end does, request creation and verification have to happen server-side.

#### The screens you build

Everything the Holder sees is yours. Embed UI would have supplied these; on this path you write them. How many depends on which channels you offer.

<table data-search="false"><thead><tr><th width="300">Screen</th><th>When you need it</th></tr></thead><tbody><tr><td><strong>Wallet picker</strong></td><td>Only if you offer more than one channel - with a single channel there is nothing to choose</td></tr><tr><td><strong>What's being asked</strong></td><td>The claims you're requesting, and why. This is where the Holder actually consents</td></tr><tr><td><strong>QR code</strong></td><td>Cross-device - render the <code>requestUri</code> the platform returned</td></tr><tr><td><strong>Web wallet hand-off</strong></td><td>Same-device web wallet - open the <code>requestUri</code> in a frame or a new tab</td></tr><tr><td><strong>DC API call</strong></td><td>Browser-native - your page passes the request to the browser and awaits the result</td></tr><tr><td><strong>Cross-device bridge page</strong></td><td>Pull flow only, and only if you didn't use <code>requests/multi</code> - the phone-side screen that exchanges <code>responseCode</code> for a <code>txCode</code> and shows it</td></tr><tr><td><strong>Code entry</strong></td><td>Cross-device pull - the field on the main device where the Holder types the <code>txCode</code></td></tr><tr><td><strong>Waiting, error and rejected states</strong></td><td>Always. A presentation can hang, fail, or be declined, and each needs somewhere to land</td></tr></tbody></table>

On a machine-to-machine journey none of this applies - no browser, no Holder at a screen, no UI to build. The two protected calls are all that remain.

## 1. Create the request

One call turns a VP Schema into a request a wallet can answer.

{% hint style="info" icon="rectangle-api" %}
All calls mentioned on this page are on the [Integration API](https://docs.toolkit.equs.ai/docs/presentation/flows-1-and-2/pages/MvAvsnLQU3ER2hOIIDU2#id-2.4.1-apis).
{% endhint %}

## Create Authorization Request

> Create Authorization Request by Verifier based on VP Schema

```json
{"openapi":"3.0.0","info":{"title":"EqusDTK Integration API Documentation","version":"0.1.2"},"tags":[{"name":"Oid4Vp","description":""}],"security":[{"sp":[]},{"tenant":[]},{"sp":[]}],"components":{"securitySchemes":{"sp":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Service Provider. Token of the Service Provider client in the `agents` realm, with no `tenantId` claim. Authorize one bearer scheme at a time."},"tenant":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Tenant. Token of a Tenant machine client or of a Tenant portal user, carrying the `tenantId` claim. Authorize one bearer scheme at a time."}},"schemas":{"CreateAuthorizationRequestRequest":{"type":"object","properties":{"mode":{"type":"string","description":"Mechanism used by the wallet to deliver the authorization response. Only encrypted modes are supported.","enum":["direct_post.jwt","dc_api.jwt"]},"type":{"type":"string","description":"Type of authorization response requested from the wallet.","enum":["vp_token","vp_token id_token"]},"redirectUri":{"type":"string","description":"URL the wallet redirects the holder to after submitting the response."},"expectedOrigins":{"description":"Web origins (http or https) allowed to complete a DC API presentation for this request. Each value is reduced to its origin: scheme, host and non-default port.","type":"array","items":{"type":"string"}},"vpSchemaId":{"type":"string","description":"VP schema unique ID"},"verifierWebUrl":{"type":"string","description":"URL of the verifier web page that will display the presentation request."},"transactionData":{"description":"Transaction data items the wallet must sign over as part of the response.","type":"array","items":{"$ref":"#/components/schemas/DelegateTransactionDataItem"}},"intendedUseId":{"type":"string","description":"Intended use to create this request under. Required once the Verifier has registered one; its registration certificate is emitted to the Wallet as a `verifier_info` entry."}},"required":["mode","type","vpSchemaId"]},"DelegateTransactionDataItem":{"type":"object","properties":{"type":{"type":"string","description":"Type of transaction data item.","enum":["delegate"]},"credentialInputIndex":{"type":"number","description":"Index of the credential input this transaction data item applies to."},"format":{"type":"string","description":"Delegated SD-JWT format the disclosed payload claims are issued in.","enum":["dSD-JWT","dSD-JWT+KB"]},"delegateJwk":{"type":"object","description":"JWK of the delegate key that the disclosed payload claims are bound to."},"payloadClaims":{"type":"object","description":"Claims disclosed to the verifier via the delegated presentation."}},"required":["type","credentialInputIndex","format","payloadClaims"]},"CreateAuthorizationRequestResponse":{"type":"object","properties":{"requestUri":{"type":"string","description":"URI from which the wallet can fetch the full authorization request object."},"txId":{"type":"string","description":"Transaction unique ID"}},"required":["requestUri","txId"]}}},"paths":{"/api/v1/{verifierId}/oid4vp/requests":{"post":{"description":"Create Authorization Request by Verifier based on VP Schema","operationId":"Oid4VpController_createAuthorizationRequest_api/v1","parameters":[{"name":"verifierId","required":true,"in":"path","description":"Verifier unique ID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAuthorizationRequestRequest"}}}},"responses":{"200":{"description":"When Authorization Request is created successfully. The uri for the authorization request and corresponding txId is returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAuthorizationRequestResponse"}}}},"400":{"description":"When the Verifier uses x509_san_dns and the response URI FQDN does not match its Client Identifier."},"404":{"description":"- When no VP Schema exists with such id.\n- When no verifier found."},"422":{"description":"- When the Verifier uses an x509 Client Identifier Prefix and no access certificate is active.\n- When the active access certificate has been revoked, is expired or not yet valid, or carries no dNSName SAN naming the protocol host.\n- When the SDK refuses the x509 Client Identifier, because the certificate chain, the identifier and the signing key do not agree."}},"summary":"Create Authorization Request","tags":["Oid4Vp"]}}}}
```

`mode` selects the flow: `dc_api.jwt` for the browser-native exchange, `direct_post.jwt` for the two that go through the platform. **One request carries one mode.**

### Offering more than one channel

To support several channels, build them together rather than issuing a request each. One call returns a request per channel - `web` for a same-device hand-off, `mobile` for a QR code, `dc` for the browser-native exchange.

## Create multiple Authorization Requests (web and mobile)

> Create multiple Authorization Requests by Verifier based on VP Schema pointing to default Embed UI

```json
{"openapi":"3.0.0","info":{"title":"EqusDTK Integration API Documentation","version":"0.1.2"},"tags":[{"name":"Oid4Vp","description":""}],"security":[{"sp":[]},{"tenant":[]},{"sp":[]}],"components":{"securitySchemes":{"sp":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Service Provider. Token of the Service Provider client in the `agents` realm, with no `tenantId` claim. Authorize one bearer scheme at a time."},"tenant":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Tenant. Token of a Tenant machine client or of a Tenant portal user, carrying the `tenantId` claim. Authorize one bearer scheme at a time."}},"schemas":{"CreateMultiRequestRequest":{"type":"object","properties":{"vpSchemaId":{"type":"string","description":"VP schema unique ID"},"encrypted":{"type":"boolean","description":"Whether the generated presentation requests should be encrypted"},"modes":{"type":"array","description":"Presentation channels to generate an authorization request for.","items":{"type":"string","enum":["web","mobile","dcApi"]}},"verifierWebUrl":{"type":"string","description":"URL of the verifier web page that will display the presentation request."},"intendedUseId":{"type":"string","description":"Intended use to create this request under. Required once the Verifier has registered one; its registration certificate is emitted to the Wallet as a `verifier_info` entry."}},"required":["vpSchemaId","modes","verifierWebUrl"]},"CreateMultiRequestResponse":{"type":"object","properties":{"web":{"description":"Authorization request generated for the web presentation channel.","allOf":[{"$ref":"#/components/schemas/CreateAuthorizationRequestResponse"}]},"mobile":{"description":"Authorization request generated for the mobile presentation channel.","allOf":[{"$ref":"#/components/schemas/CreateAuthorizationRequestResponse"}]},"dc":{"description":"Authorization request generated for the Digital Credentials API channel.","allOf":[{"$ref":"#/components/schemas/CreateAuthorizationRequestResponse"}]}},"required":["web"]},"CreateAuthorizationRequestResponse":{"type":"object","properties":{"requestUri":{"type":"string","description":"URI from which the wallet can fetch the full authorization request object."},"txId":{"type":"string","description":"Transaction unique ID"}},"required":["requestUri","txId"]}}},"paths":{"/api/v1/{verifierId}/oid4vp/requests/multi":{"post":{"description":"Create multiple Authorization Requests by Verifier based on VP Schema pointing to default Embed UI","operationId":"Oid4VpController_createMultiRequest_api/v1","parameters":[{"name":"verifierId","required":true,"in":"path","description":"Verifier unique ID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMultiRequestRequest"}}}},"responses":{"200":{"description":"When multiple Authorization Requests are created successfully, the web, mobile and dc request uris and corresponding txIds are returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMultiRequestResponse"}}}},"400":{"description":"When the Verifier uses x509_san_dns and the response URI FQDN does not match its Client Identifier."},"404":{"description":"- When no VP Schema exists with such id.\n- When no verifier found."},"422":{"description":"- When the Verifier uses an x509 Client Identifier Prefix and no access certificate is active.\n- When the active access certificate has been revoked, is expired or not yet valid, or carries no dNSName SAN naming the protocol host.\n- When the SDK refuses the x509 Client Identifier, because the certificate chain, the identifier and the signing key do not agree."}},"summary":"Create multiple Authorization Requests (web and mobile)","tags":["Oid4Vp"]}}}}
```

This is what makes the fallback decision yours rather than the Holder's: create the channels you're willing to support, and let your UI pick among them. Creating a channel commits you to nothing - an unused request simply goes unanswered.

Using `multi` also saves you building a screen later, on the cross-device path - see step 2.

## 2. Collect the response

Where the response comes from, and whether you fetch it at all, depends on the flow.

```mermaid
flowchart LR
    W["Wallet"] -->|dc_api| FE1["Your page<br/>nothing to fetch"]
    W -->|direct_post| D["Platform"]
    D -->|"push"| BE1["Your endpoint<br/>nothing to fetch"]
    D -->|"pull, same device"| FE2["Your page fetches it<br/>responseCode + txId"]
    D -->|"pull, cross device"| X["On the phone: the code is exchanged<br/>for a txCode and displayed"]
    X --> H["Holder reads the code and types it<br/>into your page on the main device"]
    H --> G["Your page fetches it<br/>txCode + txId"]
    style FE1 fill:#e8f0fe,stroke:#4285f4
    style FE2 fill:#e8f0fe,stroke:#4285f4
    style G fill:#e8f0fe,stroke:#4285f4
    style BE1 fill:#e8f0fe,stroke:#4285f4
    style D fill:#fff4e5,stroke:#f9ab00
    style X fill:#fff4e5,stroke:#f9ab00
    style H fill:#e6f4ea,stroke:#34a853
```

On the **DC API** flow the response arrives in your page from the browser, and on the **push** flow the platform posts it to your endpoint. Only the **pull** flow has a call to make: the wallet redirects to your `redirectUri` with a `responseCode`, and what you do next depends on where that redirect landed.

**Same-device** is the easy case - the redirect lands in the same browser, so your page already has the `responseCode`. Fetch with that and the `txId`.

**Cross-device** is the case worth reading carefully. The redirect happens on the phone, so your main browser never sees it. The `responseCode` is exchanged there for a one-time `txCode`, the Holder carries that across by hand, and you fetch with the `txCode` and the `txId`. Who provides the screen that does the exchange depends on how you created the request:

<table data-search="false"><thead><tr><th width="200">Created with</th><th>The phone lands on</th></tr></thead><tbody><tr><td><code>requests/multi</code></td><td>An EQUS-hosted screen that exchanges the code and displays it</td></tr><tr><td><code>requests</code></td><td>Your own <code>redirectUri</code>. The exchange and the display are yours to build, using the same public API</td></tr></tbody></table>

This bridge is an **EQUS extension, not part of OID4VP**. A cross-device redirect has nowhere useful to land, and the standard offers no way to carry the response back to the device that started the flow.

Building your own is three steps and needs no back end: read `responseCode` from the redirect's query string, exchange it for a `txCode`, then show the code and tell the Holder to type it on the device they started on.

{% hint style="warning" %}
The exchange call is **unauthenticated**, so the `responseCode` is the only thing standing between the redirect and the code. Treat it as a one-time secret: don't log it, and don't leave it retrievable after the exchange.
{% endhint %}

## Get one-time txCode for Authorization Response

> Exchange responseCode on OTP txCode

```json
{"openapi":"3.0.0","info":{"title":"EqusDTK Integration API Documentation","version":"0.1.2"},"tags":[{"name":"Oid4Vp","description":""}],"paths":{"/api/v1/oid4vp/tx-codes":{"get":{"description":"Exchange responseCode on OTP txCode","operationId":"Oid4VpController_getTxCode_api/v1","parameters":[{"name":"responseCode","required":true,"in":"query","description":"Code returned by the wallet to fetch the pending tx-code.","schema":{"type":"string"}}],"responses":{"200":{"description":"When txCode was retrieved successfully.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetTxCodeResponse"}}}}},"summary":"Get one-time txCode for Authorization Response","tags":["Oid4Vp"]}}},"components":{"schemas":{"GetTxCodeResponse":{"type":"object","properties":{"txCode":{"type":"string","description":"Transaction code the holder must present to redeem the offer. Exchanged for txId for real Cross-device flow."},"txCodeExpireAt":{"format":"date-time","type":"string","description":"Date and time the transaction code expires at."},"verifierWebUrl":{"type":"object","description":"URL of the verifier web page that displayed the presentation request."}},"required":["txCode","txCodeExpireAt","verifierWebUrl"]}}}}
```

## Get Authorization Response

> Retrieve Authorization Response by response code and txId

```json
{"openapi":"3.0.0","info":{"title":"EqusDTK Integration API Documentation","version":"0.1.2"},"tags":[{"name":"Oid4Vp","description":""}],"paths":{"/api/v1/oid4vp/responses":{"get":{"description":"Retrieve Authorization Response by response code and txId","operationId":"Oid4VpController_getAuthorizationResponse_api/v1","parameters":[{"name":"responseCode","required":false,"in":"query","description":"Code returned by the wallet to fetch the authorization response.","schema":{"type":"string"}},{"name":"txCode","required":false,"in":"query","description":"Transaction code used to authorize retrieval of the response.","schema":{"type":"string"}},{"name":"txId","required":true,"in":"query","description":"Transaction unique ID","schema":{"type":"string"}}],"responses":{"200":{"description":"When Authorization Response is resolved correctly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAuthorizationResponseResponse"}}}}},"summary":"Get Authorization Response","tags":["Oid4Vp"]}}},"components":{"schemas":{"GetAuthorizationResponseResponse":{"type":"object","properties":{"vpToken":{"type":"object","description":"Plain verifiable presentation token. Only set when the response is not encrypted."},"jwe":{"type":"string","description":"Encrypted JWE containing the authorization response. Only set when the response is encrypted."},"type":{"type":"number","description":"Indicates whether the authorization response is plain or encrypted."},"transactionDataResponse":{"type":"object","description":"Hashes of the transaction data items the wallet signed over."}},"required":["type"]}}}}
```

{% hint style="info" %}
The presentation `txCode` is not the issuance `tx_code`. This one is transient and exists only to bridge the cross-device flow; the issuance one authorizes collecting a credential.
{% endhint %}

## 3. Verify

Send the response and the `txId`, and get back the disclosed claims. Where your page ends up holding the response, pass it to your back end and verify there - this call is protected.

## Verify Presentation

> Verify submitted Authorization Response and return verification result

```json
{"openapi":"3.0.0","info":{"title":"EqusDTK Integration API Documentation","version":"0.1.2"},"tags":[{"name":"Oid4Vp","description":""}],"security":[{"sp":[]},{"tenant":[]},{"sp":[]}],"components":{"securitySchemes":{"sp":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Service Provider. Token of the Service Provider client in the `agents` realm, with no `tenantId` claim. Authorize one bearer scheme at a time."},"tenant":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Tenant. Token of a Tenant machine client or of a Tenant portal user, carrying the `tenantId` claim. Authorize one bearer scheme at a time."}},"schemas":{"VerifyPresentationRequest":{"type":"object","properties":{"authResponse":{"description":"Authorization response returned by the wallet containing the presentation.","allOf":[{"$ref":"#/components/schemas/AuthResponse"}]},"txId":{"type":"string","description":"Transaction unique ID"},"origin":{"type":"string","description":"Web origin (http or https) the presentation response was submitted from."}},"required":["authResponse","txId"]},"AuthResponse":{"type":"object","properties":{"vpToken":{"type":"object","description":"Plain verifiable presentation token. Mutually exclusive with jwe."},"jwe":{"type":"string","description":"Encrypted JWE containing the authorization response. Mutually exclusive with vpToken."},"transactionDataResponse":{"type":"object","description":"Hashes of the transaction data items the wallet signed over."},"type":{"type":"number","description":"Indicates whether the authorization response is plain or encrypted."}},"required":["type"]},"VerifyPresentationResponse":{"type":"object","properties":{"verifiedClaims":{"type":"object","description":"Claims disclosed and verified from the holder-presented credential."},"presentations":{"type":"object","description":"Raw verifiable presentations submitted by the holder, keyed by credential."}},"required":["verifiedClaims"]}}},"paths":{"/api/v1/{verifierId}/oid4vp/verify":{"post":{"description":"Verify submitted Authorization Response and return verification result","operationId":"Oid4VpController_verifyPresentation_api/v1","parameters":[{"name":"verifierId","required":true,"in":"path","description":"Verifier unique ID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyPresentationRequest"}}}},"responses":{"200":{"description":"When Presentation is verified successfully.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyPresentationResponse"}}}},"400":{"description":"- When the session advertised a response encryption key but the Authorization Response was sent in plain, encrypted under a foreign key, or carried a malformed JWE.\n- When the origin supplied for a DC API verification is not one the Authorization Request declared in its expected origins."},"409":{"description":"When the verification session was verified, failed, deleted or expired by another request while this presentation was being verified."},"422":{"description":"When the access certificate the verification session was created under can no longer be resolved, or has since been revoked."},"500":{"description":"When a DC API verification session was stored without the expected origins its Authorization Request declared."}},"summary":"Verify Presentation","tags":["Oid4Vp"]}}}}
```

The platform checks the Issuer's signature, the credential's revocation status, and that what came back actually answers what you asked. You get `verifiedClaims` - the disclosed values - and optionally `presentations`, the raw verifiable presentations keyed by credential, if you need to keep evidence rather than just the values.

## 4. Track the result

Every request leaves a record: what was asked, whether the Holder answered, and how verification went. See [Verification Result](/docs/presentation/verification-result.md).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.toolkit.equs.ai/docs/presentation/flows-1-and-2/build-your-own-ui.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
