curl --request POST \
--url https://laso.finance/buy-with-saved-card \
--header 'Authorization: <api-key>' \
--header 'Content-Type: application/json' \
--data '
{
"ask": "a 16 oz bag of Colombian ground coffee from Amazon",
"delivery_address": {
"street": "1900 Jefferson St",
"city": "San Francisco",
"state": "CA",
"zip": "94123",
"phone": "+14155550100",
"name": "Ada Lovelace"
}
}
'import requests
url = "https://laso.finance/buy-with-saved-card"
payload = {
"ask": "a 16 oz bag of Colombian ground coffee from Amazon",
"delivery_address": {
"street": "1900 Jefferson St",
"city": "San Francisco",
"state": "CA",
"zip": "94123",
"phone": "+14155550100",
"name": "Ada Lovelace"
}
}
headers = {
"Authorization": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
ask: 'a 16 oz bag of Colombian ground coffee from Amazon',
delivery_address: {
street: '1900 Jefferson St',
city: 'San Francisco',
state: 'CA',
zip: '94123',
phone: '+14155550100',
name: 'Ada Lovelace'
}
})
};
fetch('https://laso.finance/buy-with-saved-card', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://laso.finance/buy-with-saved-card",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'ask' => 'a 16 oz bag of Colombian ground coffee from Amazon',
'delivery_address' => [
'street' => '1900 Jefferson St',
'city' => 'San Francisco',
'state' => 'CA',
'zip' => '94123',
'phone' => '+14155550100',
'name' => 'Ada Lovelace'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: <api-key>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://laso.finance/buy-with-saved-card"
payload := strings.NewReader("{\n \"ask\": \"a 16 oz bag of Colombian ground coffee from Amazon\",\n \"delivery_address\": {\n \"street\": \"1900 Jefferson St\",\n \"city\": \"San Francisco\",\n \"state\": \"CA\",\n \"zip\": \"94123\",\n \"phone\": \"+14155550100\",\n \"name\": \"Ada Lovelace\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://laso.finance/buy-with-saved-card")
.header("Authorization", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"ask\": \"a 16 oz bag of Colombian ground coffee from Amazon\",\n \"delivery_address\": {\n \"street\": \"1900 Jefferson St\",\n \"city\": \"San Francisco\",\n \"state\": \"CA\",\n \"zip\": \"94123\",\n \"phone\": \"+14155550100\",\n \"name\": \"Ada Lovelace\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://laso.finance/buy-with-saved-card")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"ask\": \"a 16 oz bag of Colombian ground coffee from Amazon\",\n \"delivery_address\": {\n \"street\": \"1900 Jefferson St\",\n \"city\": \"San Francisco\",\n \"state\": \"CA\",\n \"zip\": \"94123\",\n \"phone\": \"+14155550100\",\n \"name\": \"Ada Lovelace\"\n }\n}"
response = http.request(request)
puts response.read_body{
"turn_id": "q8FvT2kLmN3pR5sX7yZa",
"state": "done",
"created_at": 1790366020000,
"conversation_id": "conv_49ec10bf959bba5559e67bac",
"status": "needs_input",
"reply": "Added it to your cart from Amazon. Total: $19.46. Let me know if you're ready to place it.",
"cart": {
"merchant": "retail",
"merchant_name": "Amazon",
"items": [
{
"name": "Cafe Quindio Medium Roast 100% Colombian Ground Coffee, 16 oz",
"quantity": 1,
"price_cents": 1899,
"product_id": "https://www.amazon.com/dp/B0C91LZ8PK"
}
],
"fees_cents": 47,
"tip_cents": 0,
"total_cents": 1946,
"approved_ceiling_cents": 3073,
"hash": "5783c0c78f1c16a2"
},
"order_id": null,
"decline_code": null,
"approval_url": null,
"charge_status": null,
"card_last4": null,
"error_code": null,
"note": "Show the account holder the cart (items, total_cents, approved_ceiling_cents). Once they agree, send its hash as confirm with this conversation_id. To change it, send another ask instead."
}{
"error": "A request with this Idempotency-Key is still being processed",
"code": "idempotency_key_in_progress"
}{
"error": "Idempotency-Key reused with a different request",
"code": "idempotency_key_reused"
}Shop and pay with the holder's saved card
Buys from online merchants (Amazon, Walmart, Target, Best Buy, DoorDash and more) and pays with the card the account holder saved themselves (see GET /get-saved-cards). One conversation per purchase:
- Send what to buy as
ask, in plain language. Passdelivery_addresson this first call when you know it. - Relay
replyto the holder and send their answers back asaskwith the sameconversation_id, until acartcomes back. - Show the holder the cart. Once they agree, send its
hashasconfirmwith theconversation_id. Nothing can be charged before this. - The confirm answers
awaiting_approvalwith anapproval_url. Send it to your human; never open it yourself. They approve on their own device with their passkey. Then send the same confirm again to getorder_placed.
A turn runs against a live merchant and can take up to two minutes. This call waits about 45 seconds: a turn that finishes in time comes back complete (state: done); a slower one comes back as state: running with a turn_id to poll with GET /get-purchase-turn.
This route is free. The holder’s own card pays the merchant, and no identity verification is needed.
curl --request POST \
--url https://laso.finance/buy-with-saved-card \
--header 'Authorization: <api-key>' \
--header 'Content-Type: application/json' \
--data '
{
"ask": "a 16 oz bag of Colombian ground coffee from Amazon",
"delivery_address": {
"street": "1900 Jefferson St",
"city": "San Francisco",
"state": "CA",
"zip": "94123",
"phone": "+14155550100",
"name": "Ada Lovelace"
}
}
'import requests
url = "https://laso.finance/buy-with-saved-card"
payload = {
"ask": "a 16 oz bag of Colombian ground coffee from Amazon",
"delivery_address": {
"street": "1900 Jefferson St",
"city": "San Francisco",
"state": "CA",
"zip": "94123",
"phone": "+14155550100",
"name": "Ada Lovelace"
}
}
headers = {
"Authorization": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
ask: 'a 16 oz bag of Colombian ground coffee from Amazon',
delivery_address: {
street: '1900 Jefferson St',
city: 'San Francisco',
state: 'CA',
zip: '94123',
phone: '+14155550100',
name: 'Ada Lovelace'
}
})
};
fetch('https://laso.finance/buy-with-saved-card', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://laso.finance/buy-with-saved-card",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'ask' => 'a 16 oz bag of Colombian ground coffee from Amazon',
'delivery_address' => [
'street' => '1900 Jefferson St',
'city' => 'San Francisco',
'state' => 'CA',
'zip' => '94123',
'phone' => '+14155550100',
'name' => 'Ada Lovelace'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: <api-key>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://laso.finance/buy-with-saved-card"
payload := strings.NewReader("{\n \"ask\": \"a 16 oz bag of Colombian ground coffee from Amazon\",\n \"delivery_address\": {\n \"street\": \"1900 Jefferson St\",\n \"city\": \"San Francisco\",\n \"state\": \"CA\",\n \"zip\": \"94123\",\n \"phone\": \"+14155550100\",\n \"name\": \"Ada Lovelace\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://laso.finance/buy-with-saved-card")
.header("Authorization", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"ask\": \"a 16 oz bag of Colombian ground coffee from Amazon\",\n \"delivery_address\": {\n \"street\": \"1900 Jefferson St\",\n \"city\": \"San Francisco\",\n \"state\": \"CA\",\n \"zip\": \"94123\",\n \"phone\": \"+14155550100\",\n \"name\": \"Ada Lovelace\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://laso.finance/buy-with-saved-card")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"ask\": \"a 16 oz bag of Colombian ground coffee from Amazon\",\n \"delivery_address\": {\n \"street\": \"1900 Jefferson St\",\n \"city\": \"San Francisco\",\n \"state\": \"CA\",\n \"zip\": \"94123\",\n \"phone\": \"+14155550100\",\n \"name\": \"Ada Lovelace\"\n }\n}"
response = http.request(request)
puts response.read_body{
"turn_id": "q8FvT2kLmN3pR5sX7yZa",
"state": "done",
"created_at": 1790366020000,
"conversation_id": "conv_49ec10bf959bba5559e67bac",
"status": "needs_input",
"reply": "Added it to your cart from Amazon. Total: $19.46. Let me know if you're ready to place it.",
"cart": {
"merchant": "retail",
"merchant_name": "Amazon",
"items": [
{
"name": "Cafe Quindio Medium Roast 100% Colombian Ground Coffee, 16 oz",
"quantity": 1,
"price_cents": 1899,
"product_id": "https://www.amazon.com/dp/B0C91LZ8PK"
}
],
"fees_cents": 47,
"tip_cents": 0,
"total_cents": 1946,
"approved_ceiling_cents": 3073,
"hash": "5783c0c78f1c16a2"
},
"order_id": null,
"decline_code": null,
"approval_url": null,
"charge_status": null,
"card_last4": null,
"error_code": null,
"note": "Show the account holder the cart (items, total_cents, approved_ceiling_cents). Once they agree, send its hash as confirm with this conversation_id. To change it, send another ask instead."
}{
"error": "A request with this Idempotency-Key is still being processed",
"code": "idempotency_key_in_progress"
}{
"error": "Idempotency-Key reused with a different request",
"code": "idempotency_key_reused"
}Authorizations
Firebase ID token from /auth or any paid route, sent as a Bearer token: Authorization: Bearer <id_token> (the Bearer prefix is required).
Headers
Optional client-generated unique key (a UUID works) that makes this write safe to retry. A retry with the same key within 24 hours returns the recorded response of the original request, marked with an Idempotency-Replayed: true header, instead of performing the write again. Reusing a key with a different body or route is rejected with 422 (code: idempotency_key_reused); a retry that arrives while the original is still running gets 409 (code: idempotency_key_in_progress), so wait a moment and retry with the same key. Keys are scoped to the authenticated account.
255Body
What the holder wants, in plain language. Required unless confirming.
2000The conversation to continue. Required with confirm.
A cart's hash from an earlier turn, to place exactly that cart.
^[0-9a-f]{16}$Where to ship. Send it on the call that asks for the cart (a cart is bound to the address it was shown for) and on the confirm.
Show child attributes
Show child attributes
Response
The turn, complete or still running.
This turn's id. Poll GET /get-purchase-turn with it while state is running.
running means the turn is still talking to the merchant; the fields below appear once it is done.
running, done, failed Milliseconds since the epoch.
Send it back on every follow-up and on the confirm.
What to do next. needs_input: a question or a cart waiting on a confirm. awaiting_approval: send approval_url to the holder, then repeat the same confirm. in_progress: the order is already being placed; read GET /get-purchase-conversation instead of confirming again. order_placed. declined: see decline_code. conflict: nothing ran, see error_code and confirm the fresh cart's hash. Never branch on reply.
needs_input, awaiting_approval, in_progress, order_placed, declined, conflict The shopping assistant's turn as prose, ready to show the holder.
The most recently shown cart.
Show child attributes
Show child attributes
Every open cart in the conversation, oldest first.
Show child attributes
Show child attributes
The last product search. Empty on a turn that did not search again, so keep the previous one.
Show child attributes
Show child attributes
Things asked for that did not make it into a cart, with a reason. Show these rather than guessing from reply.
Show child attributes
Show child attributes
Set when this turn placed an order.
The machine reason behind declined, e.g. items_unavailable.
Send to the holder when status is awaiting_approval. Never open it yourself.
Whether money moved: none, confirming, settled, or unknown (do not retry; read the conversation).
none, confirming, settled, unknown, null The saved card that paid, for naming it to the holder.
On conflict: cart_changed, turn_in_progress, delivery_address_changed, or delivery_address_bound_after_cart.
Present when state is failed.
The next step.
Was this page helpful?