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
{
"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. |
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. |
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
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 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 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. |
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 event, and every step lands on the intent's
timeline, which you can follow live.
When a purchase waits
Live purchases are paid from the payer's own wallet at Perflo. The payer meets their wallet only when a purchase needs it, so the first live purchase usually waits once:
{
"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:
{
"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
}
}-
Ask your customer exactly
input_request.prompt, the way you reach them: in a chat, say.kindisphone,email,codeorapproval(approve the sign-in on their phone; nothing to type). -
Send their answer within 5 minutes:
bashcurl 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": trueif they'd rather not, or"done": trueonce they approved anapproval. -
The intent carries on. It is
runningagain 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
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:
{
"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.
{
"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"
}
}- 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.
- 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.
- The intent carries on. Once they're signed in it goes back to
queued(reasonsigned_in) and on tocompletedas 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, create the intent with
"sign_in_link": false: it then fails with login_required straight away.