# Intents

An **intent** is a request, in natural language, for something to be bought or
booked. OpenIntents turns it into a completed transaction or a clear failure.

## Writing a good intent

Be as specific as a person would need to be. The agent asks as little as it
can mid-run, so include what matters:

- **What:** _"large oat flat white"_, _"UberX, not Comfort"_
- **Where or from whom:** a merchant, a location, or _"cheapest option"_
- **When:** _"pickup 8:45"_, _"leaving at 7am"_, _"arrive by Friday"_
- **Constraints:** _"aisle seat"_, _"under $400"_, _"refundable only"_

Anything structured (addresses, names, loyalty numbers) is better passed in
`context` than in the sentence.

## The intent object

```json
{
  "id": "int_7f3a2c",
  "object": "intent",
  "status": "running",
  "intent": "one-way flight JFK to Lisbon on the 14th, aisle seat, under $400",
  "max_amount": 40000,
  "currency": "usd",
  "context": { "traveler": { "name": "Ada Lovelace", "dob": "1990-12-10" } },
  "metadata": { "trip_id": "tr_881" },
  "result": null,
  "error": null,
  "test": false,
  "created_at": "2026-10-01T08:30:00Z",
  "completed_at": null
}
```

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | Unique ID, prefixed `int_`. |
| `status` | string | One of the statuses below. |
| `intent` | string | The natural-language request, as sent. |
| `max_amount` | integer | Spending cap in cents, including tax and delivery. Optional. |
| `context` | object | Structured details the agent may use (addresses, names, preferences). |
| `metadata` | object | Your own key/value pairs, returned untouched. |
| `payment` | object | Set when `requires_payment`: the `payment_url` to send the payer, the `total`, the `fee` and the `reason` it waits. See [when a purchase waits](#when-a-purchase-waits). |
| `input_request` | object | Set when `requires_input`: what the agent needs from your customer to sign them in to the shop, such as the code it sent. See [signing in by message](#signing-in-by-message). |
| `sign_in` | object | Set when `requires_login`: the `url` to send your customer, the `shop` and `expires_at`. |
| `result` | object | Set when `completed`: merchant, items, total, fee, confirmation, receipt URL. |
| `error` | object | Set when `failed`: `code` and a human-readable `message`. |
| `test` | boolean | `true` for intents created with a test key. |

## Lifecycle

```text
queued ──► running ──► (paid from the wallet) ───────► completed
              │                                           ▲
              ├──► requires_payment ──► (step done) ──► queued, then on as above
              │           │
              │           └──► cancelled (link expired or cancelled)
              ├──► requires_input ──► (answered) ──► running, then on as above
              │           │
              │           └──► (no answer, or a password) ──► requires_login
              ├──► requires_login ──► (signed in) ──► queued, then on as above
              │           │
              │           └──► failed (login_required: declined or expired)
              └──► failed
```

| Status | Meaning |
| --- | --- |
| `queued` | Accepted, waiting for a browser. Usually under a second. |
| `running` | The API is doing the agentic work in a real browser. |
| `requires_payment` | At checkout, and the payer has a step to take in their [wallet](/docs/wallets) first (connect it, add money, raise a limit, confirm who they are), or you asked for their approval. `payment.payment_url` is set; once the step is done the intent goes back to `queued` and carries on. |
| `requires_input` | The agent is signing your customer in to the shop and needs something only they know: their phone number or email, or the code the shop just sent. `input_request` says what; [answer it](#signing-in-by-message) and the intent carries on. |
| `requires_login` | The shop wants your customer signed in. `sign_in.url` is set: send it to them; once they sign in, the intent carries on by itself. See [sign-in links](#sign-in-links). |
| `completed` | Paid from the payer's wallet and confirmed. `result` is set. |
| `failed` | Could not be completed (out of stock, over `max_amount`, the wallet declined, merchant error). Free. |
| `cancelled` | Cancelled by you before payment. Free. |

`completed`, `failed` and `cancelled` are final. Every transition emits a
[webhook](/docs/webhooks) event, and every step lands on the intent's
[timeline](/docs/events), which you can follow live.

## When a purchase waits

Live purchases are paid from the payer's own [wallet](/docs/wallets) at
Perflo. The payer meets their wallet only when a purchase needs it, so the
first live purchase usually waits once:

```json
{
  "id": "int_7f3a2c",
  "status": "requires_payment",
  "payment": {
    "reason": "connect_wallet",
    "payment_url": "https://openintents.io/api/pay/pay_7f3a2c",
    "merchant": "Blue Bottle Coffee",
    "total": 550,
    "fee": 0,
    "currency": "usd",
    "expires_at": "2026-10-01T09:00:00Z"
  }
}
```

Send `payment_url` to the payer. It opens one page that runs only the steps
left for this purchase, and skips the ones already done. `reason` says why it
waits:

| `reason` | Meaning |
| --- | --- |
| `connect_wallet` | The payer connects their wallet, or signs in to it again. |
| `add_money` | The wallet is short of `total`; the payer adds money to it. |
| `identity_check` | The wallet needs the payer to confirm who they are first. |
| `wallet_processing` | The wallet is still taking the money (from a card the payer saved, say); the link shows how it is going. |
| `over_limit` | The purchase is over a spending limit; on a wallet, the payer raises the limit they set. |
| `requested` | You asked for the payer to see a link first (`require_payment_link`). |

Others may appear: send the link whatever the reason. Once the step is done,
the intent goes back to `queued` by itself, with the same cart, and on to
`completed`. Later purchases go straight through while the wallet's money,
limits and identity check hold. The link expires after 30 minutes, and an
intent whose link expired is cancelled, free. Test keys never pay, so test
intents never wait here.

## Signing in by message

Some shops won't sell without an account. Many sign people in with a phone
number or an email and a one-time code. There, the agent signs your customer
in itself, and asks you for each thing only they know. The intent waits in
`requires_input`:

```json
{
  "id": "int_7f3a2c",
  "status": "requires_input",
  "input_request": {
    "id": "inp_8f2c41d0a9e7b3f15c62d1aa",
    "kind": "code",
    "shop": "blinkit.com",
    "prompt": "Enter the 4-digit code blinkit.com just sent to your phone.",
    "channel": "sms",
    "length": 4,
    "url": "https://openintents.io/input/inp_8f2c41d0a9e7b3f15c62d1aa",
    "expires_at": "2026-10-14T10:05:00Z",
    "attempts_left": 2
  }
}
```

1. **Ask your customer** exactly `input_request.prompt`, the way you reach
   them: in a chat, say. `kind` is `phone`, `email`, `code` or `approval`
   (approve the sign-in on their phone; nothing to type).
2. **Send their answer** within 5 minutes:

   ```bash
   curl https://api.openintents.io/v1/intents/int_7f3a2c/input \
     -H "Authorization: Bearer $OPENINTENTS_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{ "input": "inp_8f2c41d0a9e7b3f15c62d1aa", "value": "4829" }'
   ```

   Send `"decline": true` if they'd rather not, or `"done": true` once they
   approved an `approval`.
3. **The intent carries on.** It is `running` again at once. The agent types
   the answer into the shop's page for you, and may ask once more (the code,
   after the phone number). Their next intents at that shop find them signed
   in already.

The answer is typed only into the shop's own page, and only where it was
asked. It is sealed while it waits, never shown back, and deleted once
typed: the agent's model never sees it. A password is never asked for this
way, nor anything of a payment. Prefer that the code not pass through your
chat? Send your customer `input_request.url` instead: they type it on our
page, where their phone can fill in the code from the message.

Know your customer's phone number or email already? Pass it as
`sign_in_hint` when you create the intent (`{ "phone": "+919812345678" }`),
and the agent won't ask for it. It is kept sealed, used once, and deleted
when the intent ends.

When the shop asks for a password, shows a captcha, or your customer doesn't
answer in time, the intent moves on to a [sign-in link](#sign-in-links)
instead (if you allow links), where your customer signs in themselves. To
never be asked, create the intent with `"sign_in_agent": false`.

### When something is missing

The same status covers anything else the purchase needs that your request
didn't give: the email the shop wants for the order, a size, a colour. The
agent asks instead of failing, with `kind` `detail`: `prompt` is then its own
question, and `options` lists the choices when the page offers them (answer
with one of them). Answer as above, with `value`. An intent asks at most 3
such questions, and never for a password, a code, a PIN, card or bank
details, or an ID number. If your customer declines or doesn't answer, the
intent fails with `needs_information`.

## Paying at the shop

With `pay_at_shop: true` your customer pays the shop themselves, in their own
UPI app, instead of the purchase being paid from their wallet. At the payment
step the agent brings up the shop's UPI QR code and the intent waits in
`requires_input` with a `payment` request:

```json
{
  "id": "int_7f3a2c",
  "status": "requires_input",
  "input_request": {
    "id": "inp_3b9d02e6c1f84a7d5e20b9cc",
    "kind": "payment",
    "shop": "swiggy.com",
    "prompt": "Scan the QR code with your UPI app to pay swiggy.com 149.00 INR, then confirm here that you paid. Do not pay a different amount.",
    "url": "https://openintents.io/input/inp_3b9d02e6c1f84a7d5e20b9cc",
    "image_url": "https://openintents.io/api/input/inp_3b9d02e6c1f84a7d5e20b9cc/image",
    "expires_at": "2026-10-14T10:05:00Z",
    "attempts_left": 0
  }
}
```

Show your customer `image_url` (or send them `url`, which shows the code and
a button), and answer `done` once they paid, or `decline`. The agent then
checks the shop's confirmation and the intent completes with its receipt.
No wallet is needed, and the fee is waived for now. The code expires after 5
minutes. A payment not confirmed by then is never asked for again, since the
payer may have paid all the same: the intent completes if the shop shows the
order, and otherwise fails with `needs_review` for us to check. Live keys
only: a test key's intent stops at the payment step as always.

## Sign-in links

When the shop wants your customer signed in in a way the agent can't do by
message (a password, say), the intent doesn't fail: it waits in
`requires_login` with a link.

```json
{
  "id": "int_7f3a2c",
  "status": "requires_login",
  "sign_in": {
    "url": "https://openintents.io/signin/sgn_4fQ9…",
    "shop": "amazon.com",
    "expires_at": "2026-10-01T09:00:00Z"
  }
}
```

1. **Send the link to your customer only**, the way you reach them (a
   message, an email, your app), and don't open it yourself. It works for 30
   minutes, on the first device that opens it: any other device, yours
   included, sees only that it is in use, unless the first one leaves it
   alone for a few minutes.
2. **They open it and sign in.** They see the shop's real sign-in page, live,
   in their own browser that OpenIntents runs: the one their purchases use.
   They tap, type and drag as on any page, codes by SMS, captchas and
   sliders included. That browser never pays, places orders or enters card
   details, and stays on the shop's site.
3. **The intent carries on.** Once they're signed in it goes back to
   `queued` (reason `signed_in`) and on to `completed` as usual. Their next
   intents at that shop find them signed in already.

If they decline, or the link expires, the intent fails with `login_required`.
Nothing is charged either way. To sign customers in yourself instead, with
[browser sessions](/docs/sessions), create the intent with
`"sign_in_link": false`: it then fails with `login_required` straight away.
