> ## 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 secrets in your own app

> Send a hosted vault.asteroid.ai link, or render the same form on your own page. Values go straight from the browser to Asteroid.

A **request** asks someone to fill in a secret. There are two ways to send one.

## Which option do you need?

**Most teams need the hosted link.** No code is required. Open **Logins**, then **Requests**, and
click **Ask someone for a login**. Pick a template, and the platform gives you a link to
`vault.asteroid.ai`. Send that link to the person however you like. They fill in the form on
Asteroid's page. The secret appears in your organization when they submit.

You can also create the same link from the API. `POST /agents/v2/secret-requests` returns a `token`.
The hosted page is `https://vault.asteroid.ai/vault-request#<token>`. Send that URL to the recipient.

**Use the embedded form below only when you must hide Asteroid.** The rest of this page shows how
to render the request on your own page, with your own components and branding. The recipient never
sees `vault.asteroid.ai`. Pick this when you white-label the flow or keep the form inside your own
product.

Both options share the same request, the same token, the same seven-day expiry and the same
security model. The recipient submits straight to Asteroid. The values never pass through your
servers.

## How the embedded form works

Your backend creates the request with your API key. Your page reads the form shape and submits the
values to Asteroid.

<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/secret-requests` | 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/secret-requests?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 template in the platform: open **Logins**, then **Secrets**, then **Templates**.
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/secret-requests \
  -H "X-Api-Key: $ASTEROID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "organizationId": "<your organization ID>",
    "templateId": "<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 secret's name. Omit `itemKey` and the server derives the key from that
name. Send `itemKey` when you want a specific UPPER\_SNAKE key. Many secrets may share a key. A later
rename leaves the key unchanged. A profile cannot attach two secrets with the same key.

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 secret exists. Attach it to a
[login profile](/concepts/profiles) to use it in workflows. In the platform, click **Add to a profile**
on the completed request in **Logins**, **Requests**.

## 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 |
| `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 /secret-requests/{requestId}/revoke`.

## Or send the hosted link

The **Requests** tab on the **Logins** page 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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.