> ## Documentation Index
> Fetch the complete documentation index at: https://docs.asteroid.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Collect credentials in your own app

> Render a vault template as a form on your own page. Values go straight from the browser to Asteroid.

A **credential request** asks someone to fill in a vault item. The platform can send the request as a
link to a hosted page. You can also render the same request on your own page, with your own
components. This page covers the second option.

Your backend creates the request with your API key. Your page reads the form shape and submits the
values to Asteroid. The values never pass through your servers.

<Tip>
  Start from the [example app](https://github.com/asteroidai/vault-form-example). It is a small
  Next.js app that implements every step on this page. Copy it into your own codebase.
</Tip>

## The contract

| Step                  | Call                                                                             | Where it runs | Auth            |
| --------------------- | -------------------------------------------------------------------------------- | ------------- | --------------- |
| 1. Create the request | `POST https://odyssey.asteroid.ai/agents/v2/vault/share-links`                   | Your backend  | `X-Api-Key`     |
| 2. Read the form      | `GET https://odyssey.asteroid.ai/agents/public_v2/vault/share-link`              | The browser   | `X-Share-Token` |
| 3. Submit the values  | `POST https://odyssey.asteroid.ai/agents/public_v2/vault/share-link/submit`      | The browser   | `X-Share-Token` |
| 4. Check the status   | `GET https://odyssey.asteroid.ai/agents/v2/vault/share-links?organizationId=...` | Your backend  | `X-Api-Key`     |

The `public_v2` calls accept requests from any origin. They carry no session and send no cookies.
The share token is the only credential.

<Warning>
  Never send your API key to the browser. Only your backend calls the `v2` endpoints.
</Warning>

## 1. Create the request

Design the form as a vault template on **Vault** in the platform. Copy its ID. Then create one
request for each person you collect from:

```bash theme={null}
curl -X POST https://odyssey.asteroid.ai/agents/v2/vault/share-links \
  -H "X-Api-Key: $ASTEROID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "organizationId": "<your organization ID>",
    "templateId": "<vault template ID>",
    "title": "Connect your EHR",
    "name": "EHR login for clinician 4821"
  }'
```

Get the organization ID from `GET /context`. The response holds the request and a `token`:

```json theme={null}
{
  "link": { "id": "0b7c…", "status": "pending", "expiresAt": "2026-09-30T12:00:00Z", "...": "..." },
  "token": "q3J9…"
}
```

The API returns the token once. Asteroid stores only its hash. Store the token server-side, next to
your user, until the request completes. Hand it to your page when that user opens the form.

`name` becomes the vault item's name. The item key is derived from it, and it must be unique in
your organization. A pending request reserves the key. Put your own user ID in the name, so two
requests never clash. A clash returns `409`.

To skip the template, send `kind`, `fields` and optional `steps` inline instead of `templateId`.

## 2. Read the form

```ts theme={null}
const res = await fetch('https://odyssey.asteroid.ai/agents/public_v2/vault/share-link', {
  headers: { 'X-Share-Token': token },
});
const link = await res.json();
```

```json theme={null}
{
  "title": "Connect your EHR",
  "requesterOrgName": "Acme Health",
  "kind": "login",
  "status": "pending",
  "expiresAt": "2026-09-30T12:00:00Z",
  "fields": [
    { "key": "USERNAME", "label": "Username", "type": "username", "required": true },
    { "key": "PASSWORD", "label": "Password", "type": "password", "required": true },
    { "key": "TOTP_SEED", "label": "Authenticator key", "type": "totp_seed", "required": false,
      "hint": "The setup key behind 'Can't scan the QR code?'" }
  ],
  "steps": [
    { "key": "sign-in", "title": "Your login", "instructions": "Use the account you sign in with each day.",
      "fieldKeys": ["USERNAME", "PASSWORD"] },
    { "key": "two-factor", "title": "Two-factor", "fieldKeys": ["TOTP_SEED"] }
  ]
}
```

Render `fields` in array order. When `steps` is not empty, show one step per page. Each step lists
its fields by key. `instructions` is Markdown. A step can have instructions and no fields.

Map each field type to an input:

| Type                             | Input                                    | Stored as  |
| -------------------------------- | ---------------------------------------- | ---------- |
| `username`, `text`               | `type="text"`                            | Readable   |
| `email`                          | `type="email"`                           | Readable   |
| `url`                            | `type="url"`                             | Readable   |
| `phone`                          | `type="tel"`                             | Readable   |
| `cardholder_name`, `card_expiry` | `type="text"`                            | Readable   |
| `password`, `hidden`, `api_key`  | `type="password"`                        | Write-only |
| `card_number`, `card_cvv`        | `type="password"`, `inputmode="numeric"` | Write-only |
| `totp_seed`                      | `type="password"`                        | Write-only |

Write-only values never come back out of the API. Asteroid decrypts them only when a workflow uses
them.

## 3. Submit the values

```ts theme={null}
const res = await fetch('https://odyssey.asteroid.ai/agents/public_v2/vault/share-link/submit', {
  method: 'POST',
  headers: { 'X-Share-Token': token, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    values: [
      { key: 'USERNAME', value: 'dr.lee' },
      { key: 'PASSWORD', value: '…' },
    ],
  }),
});
```

Send one entry per filled field. Leave out empty optional fields. An unknown key or a missing
required value returns `400`.

Clean the values first, the way the hosted page does:

* **TOTP seed:** accept a Base32 key or an `otpauth://totp/` link. Send the Base32 secret only,
  uppercase, with spaces, dashes and `=` padding removed.
* **Card number and CVV:** remove spaces and dashes.
* **URL:** add `https://` when the scheme is missing.
* **Readable fields:** trim whitespace. Send secrets exactly as typed.

A `200` with `"status": "completed"` means the vault item exists. Attach it to an
[agent profile](/concepts/profiles) to use it in workflows.

## Responses

| Status | Meaning                                      | What to do                             |
| ------ | -------------------------------------------- | -------------------------------------- |
| `400`  | A value is missing or invalid                | Show the error. The request stays open |
| `404`  | The token is wrong                           | Create a new request                   |
| `409`  | The item key was taken meanwhile             | Create a new request with another name |
| `410`  | The request is completed, expired or revoked | Stop showing the form                  |
| `429`  | Too many calls from this address             | Wait `Retry-After` seconds             |

## Limits

| Limit                                                      | Value                        |
| ---------------------------------------------------------- | ---------------------------- |
| Request lifetime                                           | 7 days                       |
| Pending requests per organization                          | 200                          |
| Request creation per user (an API key counts as its owner) | 1,000 per hour, burst of 100 |
| Form reads per client address                              | 30 per minute                |
| Submits per client address                                 | 10 per minute                |

Create a request when a user needs one, not on every page load. Reuse the stored token until the
request completes or returns `410`. Revoke unused requests with
`POST /vault/share-links/{linkId}/revoke`.

## Or send the hosted link

The **Vault** page in the platform creates the same request and gives you a link to
`vault.asteroid.ai`. Send that link when you do not need the form inside your own app.
