Secrets
A secret is something about a payer that a purchase may need and nobody
else should see: a shipping address, a phone number, a shop password, a
loyalty number. You store it once, and an intent names it.
The agent that reads the shop's page is given only a handle (sec_…), its
label and a mask. The value is typed into the page by the worker, and it is
never returned by any endpoint, tool, event or webhook.
Secrets are write-only. You can create, replace, list and delete them. There is no way to read one back, so keep your own copy if you need it.
The values you send pass through your request, and through an MCP tool call they pass through the model's context. Where the payer can enter a value themselves, prefer that to sending it through an agent.
The secret object
{
"id": "sec_5a1c93d2e0b4",
"object": "secret",
"customer": "cus_42",
"kind": "address",
"label": "Home",
"mask": "London, GB",
"public": { "city": "London", "country": "GB" },
"shops": [],
"default": false,
"mode": "live",
"entered_by": "developer",
"version": 1,
"created_at": "2026-10-13T08:30:00Z",
"updated_at": "2026-10-13T08:30:00Z",
"expires_at": null,
"last_used_at": null
}| Field | Type | Description |
|---|---|---|
id | string | The handle, prefixed sec_. |
customer | string | The customer it belongs to; null for your account's own. |
kind | string | One of the kinds below. |
label | string | The name you gave it. Agents see it, so never put a value in it. |
mask | string | What a person may be shown to recognise it, such as London, GB. |
public | object | The few fields kept in the clear (a city, a loyalty program). |
shops | array | The shops (domains) it may be used on. Empty: any shop the purchase names. |
default | boolean | Offered first among the payer's secrets of its kind. |
mode | string | test or live, from the key that wrote it. A test key never sees a live secret. |
entered_by | string | developer (the API) or payer (their own page). |
version | integer | Counts replacements. |
Kinds and fields
Every fields value is text. Fields not listed for a kind are refused.
| Kind | Fields (sealed) | Public |
|---|---|---|
address | recipient_name, line1, line2, postal_code, phone | city, region, country |
contact | given_name, family_name, email, phone | none |
identity_document | number, full_name, date_of_birth, expiry | document_type, issuing_country |
shop_password | username, password | shop |
loyalty | member_number, pin | program |
A shop_password needs exactly one entry in shops, and is used nowhere
else. A payer may have up to 50 secrets in each mode, and one secret may hold
at most 4 KB.
Create a secret
curl https://api.openintents.io/v1/secrets \
-H "Authorization: Bearer $OPENINTENTS_API_KEY" \
-H "Idempotency-Key: addr-cus_42-1" \
-H "Content-Type: application/json" \
-d '{
"customer": "cus_42",
"kind": "address",
"label": "Home",
"fields": {
"recipient_name": "Ada Lovelace",
"line1": "12 Example Street",
"postal_code": "N1 9GU",
"city": "London",
"country": "GB"
}
}'| Parameter | Type | Description |
|---|---|---|
customer | string | The customer it is for. Leave out for your account's own. |
kind | string | Required. |
label | string | Required, at most 60 characters. |
fields | object | Required. The kind's fields, as text. |
shops | array | Domains it may be used on. |
default | boolean | Make it the payer's default for its kind. |
expires_at | string | An RFC 3339 time after which it is deleted. |
It answers 201 with the secret object. A request with an
Idempotency-Key that is sent again with the same body gets the same
answer. The same key with any change in the body, values included, is
409. The comparison is made on a keyed digest of the whole request, and the
stored record holds nothing from which a value could be guessed.
Replace a secret
Sends the whole secret again, in the same shape as a create. There is no
partial update. The old values are deleted and version goes up. A secret
cannot move to another customer.
List and get
Both return the object above and nothing more. customer and kind narrow a
list. Without customer, the list is your account's own.
Delete a secret
Answers 204. The values are gone for good.
See how a secret was used
Every use of a secret, newest first, paged with limit and cursor: when it
was created, replaced or deleted, when a worker typed a field into a shop, and
when a use was turned away. Each secret_use has action (create,
replace, delete, expire, fill or refuse), actor (worker, api,
payer, developer or system), outcome (ok or refused), the reason for a
refusal (shop_not_allowed, expired or wrong_use), and where they apply
the field, field_names, shop and intent. It never holds a value or a
mask. The history of a deleted secret stays readable. Your dashboard shows the
same under Secrets.
Let the payer enter it themselves
To keep a value out of your code and your agent altogether, ask the payer to type it on a page of ours:
curl https://api.openintents.io/v1/customers/cus_8f2a/secret_requests \
-H "Authorization: Bearer $OPENINTENTS_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "kinds": ["address", "contact"], "return_url": "https://app.example.com/done" }'{
"id": "sr_Q7mX2kLp0aVbN4cR8sTuYw1e",
"object": "secret_request",
"customer": "cus_8f2a",
"kinds": ["address", "contact"],
"url": "https://openintents.io/vault/sr_Q7mX2kLp0aVbN4cR8sTuYw1e",
"expires_at": "2026-01-02T10:00:00Z"
}Send the payer to url. It works once, and ttl_seconds (60 to 604800,
default one day) sets how long it lasts. The page sends what the payer types
straight to OpenIntents, sealed, with no referrer, and never shows it back.
The secrets it makes have entered_by: payer.
When the payer is done, an endpoint that names secret_request.completed
is sent the request with the handles it made (no values). You can also read
it with GET /v1/secret_requests/{id}: state is pending, done or
expired, and secrets lists the handles.
Use secrets in an intent
On POST /v1/intents, two optional parameters say what the
purchase may use:
| Parameter | Type | Description |
|---|---|---|
secrets | array | Handles of the payer's secrets the purchase may use. Left out: the payer's defaults. []: none. |
shops | array | Domains you let the purchase fill secrets on. ["*"] allows any shop, for contact and address secrets only. |
A handle that is not the payer's own, is from the other mode, or does not
exist is 404 with param: secrets. An expired one is 400.
Errors
| Status | Code | When |
|---|---|---|
400 | invalid_request | A field fails its rule. The error names the field and the rule, never the value. |
404 | not_found | The secret is not yours, is from the other mode, or is gone. |
409 | idempotency_key_reused | The Idempotency-Key was used with a different body. |
409 | too_many_secrets | The payer already has 50 in that mode. |
413 | invalid_request | The request is over 16 KB. |
429 | too_many_writes | A key may write 60 secrets a minute. Retry-After says when to try again. |
503 | secrets_unavailable | Secrets cannot be stored right now. |
MCP tools
The MCP server has create_secret, list_secrets and
delete_secret, with the same arguments and results. There is no tool that
reads a secret.