Browse docs
previewThese docs describe OpenIntents as it is being built. Endpoints and payloads may change before launch.

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
}
FieldTypeDescription
idstringUnique ID, prefixed int_.
statusstringOne of the statuses below.
intentstringThe natural-language request, as sent.
max_amountintegerSpending cap in cents, including tax and delivery. Optional.
contextobjectStructured details the agent may use (addresses, names, preferences).
metadataobjectYour own key/value pairs, returned untouched.
paymentobjectSet 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_requestobjectSet 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_inobjectSet when requires_login: the url to send your customer, the shop and expires_at.
resultobjectSet when completed: merchant, items, total, fee, confirmation, receipt URL.
errorobjectSet when failed: code and a human-readable message.
testbooleantrue 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
StatusMeaning
queuedAccepted, waiting for a browser. Usually under a second.
runningThe API is doing the agentic work in a real browser.
requires_paymentAt 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_inputThe 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_loginThe 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.
completedPaid from the payer's wallet and confirmed. result is set.
failedCould not be completed (out of stock, over max_amount, the wallet declined, merchant error). Free.
cancelledCancelled 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:

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:

reasonMeaning
connect_walletThe payer connects their wallet, or signs in to it again.
add_moneyThe wallet is short of total; the payer adds money to it.
identity_checkThe wallet needs the payer to confirm who they are first.
wallet_processingThe wallet is still taking the money (from a card the payer saved, say); the link shows how it is going.
over_limitThe purchase is over a spending limit; on a wallet, the payer raises the limit they set.
requestedYou 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 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.

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, create the intent with "sign_in_link": false: it then fails with login_required straight away.