Albert One API · v1
The concierge and booking API
Albert One is a concierge and booking API for any product whose users need something arranged: a company booking for its teams and clients, an assistant or productivity app, a travel or hospitality app, a bank or card program, a member club, a hotel group. Three modules sit behind one key, and you switch each of them on or off in the console.
Ask Albert is the AI concierge, by conversation: it recommends from Albert's curated places and files a mission when your user wants something arranged. The Voice Agent phones the venue and books the table itself. Albert Connect hands what the AI cannot close to a vetted human concierge of the Albert network, who talks to your client in a thread you show in your own product.
The API is organized around REST. It accepts JSON bodies, returns JSON, and uses standard HTTP status codes.
https://albertone.albert-connect.com/v1How it works
From sign-up to production, an integration follows six steps.
- 1Create your account in the console with your work email. The Sandbox is free for every account and fully simulated.
- 2Create a sandbox key (ak_test_) in the console and keep it on your server. Build the whole integration with it: every answer, call and concierge is simulated in the exact format of live, and nothing is billed, booked, called or dispatched.
- 3Open a conversation for your client with
POST /v1/one/conversationsand show Albert One's reply and its place cards. Post each next message withPOST /v1/one/conversations/{id}/messages. - 4When done is
true, the mission is filed. A table at a place Albert knows is booked by the Voice Agent when it is on; anything else goes to Albert Connect when it is on, or comes back to you asmission.handoff. - 5Set your webhook address in the console and follow the mission with the events:
request.updated,request.assigned,message.created,mission.handoff. - 6Choose a plan in the console, create a live key (ak_live_) and swap it in. The same code now books real tables and, after review, reaches the Connect network.
Authentication
Authenticate every call with a secret key in the Authorization header, as a Bearer token. You create, name, roll and revoke keys in the console, as many as you need.
Keys come in two modes. A sandbox key (ak_test_) is free for every account and fully simulated: Ask Albert answers from a script with fictional places, a filed mission gets a simulated Voice Agent call and a simulated concierge, with the same objects and webhooks as live, and request objects carry livemode: false. Sandbox keys get 1,000 turns a month, never billed (429 test_quota past that). A live key (ak_live_) acts for real and needs an active plan: creating one, or calling with one, without a plan answers 402 live_requires_subscription.
Rolling a key gives you a new one at once and keeps the old one working for the overlap you choose (none, an hour, a day or a week), so a deploy can follow. A rolled or revoked key answers 401 key_expired.
Your keys carry real permissions: keep them on your server. Never put one in a mobile app, a web page or a public repository. A missing or wrong key answers 401.
Every response carries an X-Request-Id header (req_...). Quote it to us, or paste it in the console's request log to see the call, its body and what it triggered.
GET /v1/status tells you where your key stands: its mode, the plan in force, whether it is live, and which modules are on right now.
curl https://albertone.albert-connect.com/v1/status \
-H "Authorization: Bearer ak_test_..."{
"partner": "Your company",
"plan": "sandbox",
"live": false,
"key_mode": "test",
"modules": {
"ask": true,
"voice": true,
"connect": false
},
"sandbox": false,
"webhook_url": "https://api.yourcompany.com/albert/webhook",
"payments_enabled": false,
"albert_one": {
"enabled": true,
"monthly_turns": 1000,
"used_this_month": 12
}
}Modules and plans
Your plan decides how you are billed and which modules you may use. Inside that, each module has its own switch in the console, so you can turn one off without touching your code.
Pay as you go has no monthly fee and graduated tiers, like tax bands: each month, your first 2,000 turns are priced at the first rate, the next ones at the second, and so on. Voice Agent minutes work the same way, billed to the second with one minute minimum per call placed, whatever its outcome. A spending cap, $500 a month by default, stops the calls with 402 spend_cap_reached once reached.
The four subscriptions (Starter, Essential, Growth, Scale) each come in three bundles: Ask Albert alone, the Voice Agent alone, or both. Each carries a monthly quota of turns and minutes; past the quota, usage is billed at the overage rate and never cut. You pay by card in the console, and change plan, bundle or Connect level whenever you like, prorated to the day.
Albert Connect is available on every live plan, Pay as you go included, once our team has reviewed your integration, at one of three levels: Access, Plus or Premium (see Albert Connect). A bundle without Ask Albert answers 403 plan_lacks_ask on /v1/one; one without the Voice Agent simply skips the call.
A module that is off answers with a stable error, so your app can fall back: 403 ask_not_enabled for /v1/one, 403 connect_not_enabled for POST /v1/requests. With the Voice Agent off, a mission simply skips the call.
| Plan | A month (Ask / Voice / Both) | Ask Albert | Voice Agent | Beyond the quota |
|---|---|---|---|---|
Sandbox | Free, every account | Simulated: scripted answers, fictional places, 1,000 turns a month | Simulated calls, no phone rings | Never billed |
Pay as you go | No fee | First 2,000 $0.08, to 10,000 $0.06, to 50,000 $0.05, beyond $0.045 | Per minute: first 1,000 $0.45, to 10,000 $0.38, beyond $0.30 | No quota; $500 spending cap by default |
Starter | $49 / $69 / $109 | 1,000 turns | 150 minutes | $0.07 a turn, $0.50 a minute |
Essential | $149 / $179 / $299 | 3,000 turns | 450 minutes | $0.06 a turn, $0.45 a minute |
Growth | $390 / $449 / $790 | 8,000 turns | 1,200 minutes | $0.05 a turn, $0.40 a minute |
Scale | $690 / $1,090 / $1,690 | 15,000 turns | 3,000 minutes | $0.05 a turn, $0.37 a minute |
Enterprise | On quote | Per member or per seat | Local numbers, a voice in your brand | SLA, named contact |
Errors
Albert uses conventional HTTP status codes. 2xx means it worked, 4xx means the call cannot succeed as sent, 5xx means something failed on our side.
Every error carries a stable code to branch on, and a message written for a developer to read.
| Status | Codes | Meaning |
|---|---|---|
401 | missing_key, invalid_key | No key, or a key that is not valid |
401 | key_expired | The key was rolled or revoked. Use the current key from the console |
403 | ask_not_enabled | Ask Albert is switched off for this key. Switch it on in the console |
403 | connect_not_enabled | A request to a human concierge needs Albert Connect, which opens after a review by our team |
403 | plan_lacks_ask, plan_lacks_voice | Your subscription's bundle does not include this module. Add it in Plan & billing. |
404 | not_found | Unknown path, or a request or conversation that is not yours |
409 | already_closed, thread_closed, thread_not_ready | The request or its thread is closed, or not open yet |
409 | mission_filed, conversation_closed | The Albert One conversation filed its mission (write in the request's thread instead), or it is closed |
402 | live_requires_subscription | A live key (ak_live_) needs an active plan. Use a sandbox key, or choose a plan in the console. |
429 | test_quota | Sandbox keys get 1,000 simulated turns a month. Wait for the 1st (UTC), or go live. |
403 | role_forbidden | Console actions only: your team role cannot do this. Ask an owner of the account. |
402 | spend_cap_reached | Pay as you go: this month's usage reached your spending cap ($500 by default). Contact Albert to raise it, or move to a subscription in the console. |
404 / 409 | no_quote, no_escrow, escrow_not_held, escrow_disputed | Nothing to pay, confirm or dispute yet, or the escrow is not in a state that allows it |
503 | payments_disabled | Live payments are paused on Albert's side. Test-mode quotes are not affected. |
503 | albert_one_unavailable | Albert One could not answer this time. Retry with the same message. |
413 | file_too_large | A file over 25 MB |
415 | unsupported_type | A file type that is not accepted |
422 | user_id_required, description_required, invalid_category, invalid_date, invalid_party_size, invalid_file, empty_message, request_refused | The body is not acceptable; the message says why |
500 | server_error | On our side. Safe to retry with the same reference. |
{
"error": {
"code": "invalid_category",
"message": "Unknown category. One of: restaurant, hotel, ..."
}
}Idempotent requests
A network can fail after Albert received your call and before you received the answer. To retry safely, send your own reference with the request, or an Idempotency-Key header.
A second POST /v1/requests with a reference already used returns the first request, with duplicate set to true, instead of ringing a second concierge.
curl https://albertone.albert-connect.com/v1/requests \
-H "Authorization: Bearer ak_live_..." \
-H "Idempotency-Key: order-8841" \
-H "Content-Type: application/json" \
-d '{ ... }'Sandbox and going live
The Sandbox is free for every account, and everything in it is simulated: no AI model answers, no phone rings, no concierge is contacted. What you get is the exact format of live, the same objects, statuses and webhooks, so you can build your whole integration against it. Use a sandbox key (ak_test_). Request objects carry livemode: false.
Ask Albert answers from a script. It understands a kind of place (restaurant, bar, club, hotel, spa, beach club) or a service (jet, yacht, car, villa, tickets, chef), a city, a time (tonight, tomorrow, at 9pm, 21:00), a party size (for 4, 4 people) and the wish to book (book, reserve, yes, the first one). Places are fictional and clearly marked: ids start with sbx_, names like Sandbox Trattoria, photo null. Albert's real curated collection only answers in Live.
A filed mission then lives a scripted life. A restaurant mission gets a simulated Voice Agent call (GET /v1/requests/{id}/calls, webhooks voice.call.started and voice.call.completed). Any other mission, or a call that does not book, goes to a simulated concierge named Sandbox, who takes it (in_progress), writes in the thread (message.created), confirms (confirmed) and completes it (completed). You can write in the Sandbox thread too: the reply is simulated.
Magic values choose the story, like test cards: see the table. Timings: the call starts about 3 s after the mission and ends 12 s later; the concierge takes the mission about 5 s after, writes at +15 s, confirms at +35 s and completes at +60 s. Unanswered calls are retried 30 s apart in the Sandbox (5 minutes in Live).
Live is for accounts with a plan: choose Pay as you go or a subscription in the console and pay by card, then create a live key (ak_live_). Nothing changes in your code: the same calls now answer from the full curated collection, call real restaurants (United States for now) and reach real concierges once Albert Connect is approved.
Payments for Albert Connect missions have their own test mode: every quote carries mode test until Albert enables live payments for your account. See Test mode under Checkout and escrow.
| Magic value | Module | What happens |
|---|---|---|
party_size 13 | Voice Agent | The restaurant is full (outcome full). The mission goes to the Sandbox concierge. |
A time ending in :13 (21:13, 9:13pm) | Voice Agent | Nobody answers (no_answer): three calls, 30 s apart, then the Sandbox concierge. |
party_size 7 | Voice Agent | Only another time is offered (alternative, 22:30). The mission goes to the Sandbox concierge. |
The word decline in the request | Albert Connect | No concierge takes it: the request ends declined. |
Anything else | Both | A restaurant mission is confirmed by the call; any other mission is taken, confirmed and completed by the Sandbox concierge. |
{
"calls": [
{
"id": "6f0c2c1e-...",
"attempt": 1,
"status": "completed",
"outcome": "confirmed",
"venue": "Sandbox Trattoria",
"summary": "Table confirmed for 2 tonight at 9 pm under the client's name.",
"duration_seconds": 74,
"transcript": [
{ "role": "agent", "text": "Hi, I'm calling from Albert One, the concierge service..." },
{ "role": "venue", "text": "Yes, that works. What name should I put it under?" }
],
"confirmed": { "party_size": 2, "when": "tonight at 9 pm", "reference": "SBX-7C9C1A" },
"livemode": false,
"started_at": "2026-10-17T07:53:24Z",
"ended_at": "2026-10-17T07:53:36Z"
}
]
}AI agents (MCP)
Albert One is also an MCP server, so any agent that speaks the Model Context Protocol can use it directly: Claude (Claude Code, Claude Desktop and apps built on the Claude API), ChatGPT and the OpenAI Agents SDK, Cursor, or your own agent. The agent gets Albert's tools (ask, book, follow a mission, talk in its thread) without you writing the glue.
The server lives at https://albertone.albert-connect.com/mcp (Streamable HTTP, JSON responses, stateless). Authenticate with the same keys as the API, in an Authorization header: a sandbox key (ak_test_) keeps everything simulated and free, a live key (ak_live_) acts for real on your plan.
Every tool call is an ordinary API call: the same quotas and billing, the same request log in the console (with its request id), the same webhooks to your endpoint. With a sandbox key, missions live their simulated life exactly as they do through the REST API.
| Tool | What it does |
|---|---|
ask_albert | Talk to Albert One for a user: open a conversation or continue one. Returns the reply, place cards, and the mission once filed. |
get_conversation | A conversation so far, with its mission. |
create_request | Send a request to a human concierge (Albert Connect). |
get_request | One request, its status and concierge. |
list_requests | Your requests, newest first, filtered by user or status. |
get_calls | The Voice Agent's calls for a request, with outcome, summary and transcript. |
list_messages | The messages in a request's thread. |
send_message | Write in a request's thread as your user. |
cancel_request | Withdraw an open request. |
list_categories | What can be asked for. |
get_status | The key's account, plan, mode (sandbox or live) and modules. |
claude mcp add --transport http albert-one https://albertone.albert-connect.com/mcp \
--header "Authorization: Bearer ak_test_..."{
"mcpServers": {
"albert-one": {
"type": "http",
"url": "https://albertone.albert-connect.com/mcp",
"headers": { "Authorization": "Bearer ak_test_..." }
}
}
}from agents import Agent, Runner
from agents.mcp import MCPServerStreamableHttp
async with MCPServerStreamableHttp(
params={
"url": "https://albertone.albert-connect.com/mcp",
"headers": {"Authorization": "Bearer ak_test_..."},
},
) as albert:
agent = Agent(name="Concierge", mcp_servers=[albert])
result = await Runner.run(agent, "Find my client a table for 2 in Paris tonight")Ask Albert
Albert One is Albert's AI concierge, offered to your clients by conversation. In one conversation it does two things. It recommends places from Albert's curated list (restaurants, bars, clubs, hotels, spas, beach clubs, experiences) and returns them as place cards. And when your client wants something arranged, it gathers what, when and where, never more than it needs, and files a mission.
A mission is an ordinary request of this API, with the reference one:<conversation id>: read it with GET /v1/requests/{id} and follow it with the webhooks. What happens next depends on your modules: the Voice Agent books a table at a place Albert knows, Albert Connect hands anything else to a human concierge, and without either the mission comes back to you as mission.handoff.
While Ask Albert is switched off in the console, every call under /v1/one answers 403 ask_not_enabled.
A turn is one answer from Albert One. Sandbox keys get 1,000 simulated turns a month, free. Pay as you go bills each turn on its tiers, up to your spending cap. A subscription includes a quota of turns, then bills each turn past it at the overage rate. When the Sandbox allowance is used up, sandbox calls answer 429 test_quota until it renews on the 1st of the month, UTC. GET /v1/status tells you where you stand.
{
"partner": "Your company",
"plan": "sandbox",
"live": false,
"key_mode": "test",
"modules": {
"ask": true,
"voice": true,
"connect": false
},
"sandbox": false,
"webhook_url": "https://api.yourcompany.com/albert/webhook",
"payments_enabled": false,
"albert_one": {
"enabled": true,
"monthly_turns": 1000,
"used_this_month": 12
}
}Integrating Albert One
A complete integration follows six steps. Store conversation.id on your side, next to your client.
- 1When your client taps your concierge button, open a conversation with
POST /v1/one/conversations, with or without their first message. - 2Show reply as Albert One's message, and each entry of places as a card: the photo, the name, then subcategory ·
price_range. - 3Post each message your client writes with
POST /v1/one/conversations/{id}/messages, and show the reply and places that come back. - 4When done is
true, the mission is filed:conversation.missionis the request, in the statussearching. - 5Switch your interface to the mission's thread: read and write with
/v1/requests/{id}/messages, and follow the request with the webhooks, fromrequest.assignedon. - 6Keep showing Albert One's last reply above the thread, so your client sees what was understood and passed on.
The conversation object
One conversation between your client and Albert One. It holds at most one mission.
Attributes
idstringUnique identifier. Keep it to post the next messages.
statusenumopen while Albert One converses, filed once the mission is filed, or closed.
citystring | nullThe city hint you sent.
langstringThe language you sent, en by default. Albert One then mirrors the language your client writes in.
missionobject | nullnull until the mission is filed. Then the request object, whose reference is one:<conversation id>.
created_at, updated_attimestampISO 8601, UTC.
{
"id": "8d2e6f14-3b7a-4c91-a5e0-6f1d2c3b4a59",
"status": "open",
"city": "Cannes",
"lang": "fr",
"mission": null,
"created_at": "2026-10-16T09:12:04Z",
"updated_at": "2026-10-16T09:12:09Z"
}The place card
A place from Albert's curated list, recommended by Albert One. A reply carries zero, one or several of them in places.
Attributes
idstringUnique identifier.
namestringThe place's name.
categorystringWhat kind of place it is.
subcategorystring | nullA finer description, to show under the name.
addressstring | nullThe street address.
price_rangestring | nullHow expensive it is, to show next to the subcategory.
photostring | nullA photo URL, or null.
citystringThe city it is in.
{
"id": "e39c7c3e-9e6c-47c0-8bea-d2fe2ce03772",
"name": "La Môme Plage",
"category": "Beach club",
"subcategory": null,
"address": "Bd de la Croisette, 06400 Cannes, France",
"price_range": "€€€",
"photo": "https://...",
"city": "Cannes"
}Open a conversation
/v1/one/conversationsOpens a conversation with Albert One for one of your clients. With a first message, Albert One answers it; without one, it greets your client. Answers 201 with the conversation, Albert One's reply, the places it recommends, and done.
Body
user.idstringRequiredYour own identifier for this client.
user.namestringAlbert One greets a name, and the concierge sees it on the mission.
user.emailstringKept on file.
user.phonestringKept on file.
citystringA hint: the city your client is in or browsing.
langstringDefaults to en. Albert One then mirrors the language your client writes in.
messagestringYour client's first message. Without it, Albert One greets your client.
curl -X POST https://albertone.albert-connect.com/v1/one/conversations \
-H "Authorization: Bearer $ALBERT_KEY" \
-H "Content-Type: application/json" \
-d '{
"user": {
"id": "client-42",
"name": "Sophie Martin"
},
"city": "Cannes",
"lang": "fr",
"message": "Je cherche un beach club chic pour demain midi"
}'{
"conversation": {
"id": "8d2e6f14-3b7a-4c91-a5e0-6f1d2c3b4a59",
"status": "open",
"city": "Cannes",
"lang": "fr",
"mission": null,
"created_at": "2026-10-16T09:12:04Z",
"updated_at": "2026-10-16T09:12:09Z"
},
"reply": "Pour un déjeuner chic demain midi, je vous suggère La Môme Plage pour son ambiance glamour sur la Croisette, Annex Beach pour son décor signé Jacques Garcia, ou Plage du Festival pour la vue sur le Palais. Souhaitez-vous que je fasse réserver l'une de ces adresses pour vous ?",
"places": [
{
"id": "e39c7c3e-9e6c-47c0-8bea-d2fe2ce03772",
"name": "La Môme Plage",
"category": "Beach club",
"subcategory": null,
"address": "Bd de la Croisette, 06400 Cannes, France",
"price_range": "€€€",
"photo": "https://...",
"city": "Cannes"
},
{ "name": "Annex Beach", ... },
{ "name": "Plage du Festival", ... }
],
"done": false
}Send a message to Albert One
/v1/one/conversations/{id}/messagesPosts what your client wrote and returns Albert One's answer, in the same shape as opening the conversation. When done is true, the mission was filed: conversation.status is filed and conversation.mission is the request object, in the status searching at first.
Path parameters
idstringRequiredThe conversation id.
Body
textstringRequiredYour client's message.
Once the mission is filed, the conversation answers 409 mission_filed: the message tells you to write in /v1/requests/{request_id}/messages. A closed conversation answers 409 conversation_closed, an empty text 422 empty_message, and an unknown id 404 not_found.
503 albert_one_unavailable means Albert One could not answer this time: retry with the same message.
curl -X POST https://albertone.albert-connect.com/v1/one/conversations/8d2e6f14-3b7a-4c91-a5e0-6f1d2c3b4a59/messages \
-H "Authorization: Bearer $ALBERT_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Le premier, pour 4 personnes demain à 13h"
}'{
"conversation": {
"id": "8d2e6f14-3b7a-4c91-a5e0-6f1d2c3b4a59",
"status": "filed",
"city": "Cannes",
"lang": "fr",
"mission": {
"id": "1c7b9e20-4f3a-4d8e-b6a1-9e2f0d5c3b78",
"reference": "one:8d2e6f14-3b7a-4c91-a5e0-6f1d2c3b4a59",
"status": "searching",
"category": "restaurant",
"city": "Cannes",
"concierge": null,
...
},
"created_at": "2026-10-16T09:12:04Z",
"updated_at": "2026-10-16T09:13:21Z"
},
"reply": "Très bien Sophie, je transmets votre demande pour La Môme Plage demain à 13h pour 4 personnes. Votre concierge prend le relais et reviendra vers vous pour la confirmation.",
"places": [],
"done": true
}Retrieve a conversation
/v1/one/conversations/{id}Returns the conversation and every message in it, oldest first, with the places Albert One recommended in each of its answers.
Path parameters
idstringRequiredThe conversation id.
curl https://albertone.albert-connect.com/v1/one/conversations/8d2e6f14-3b7a-4c91-a5e0-6f1d2c3b4a59 \
-H "Authorization: Bearer $ALBERT_KEY"{
"conversation": {
"id": "8d2e6f14-3b7a-4c91-a5e0-6f1d2c3b4a59",
"status": "open",
"city": "Cannes",
"lang": "fr",
"mission": null,
"created_at": "2026-10-16T09:12:04Z",
"updated_at": "2026-10-16T09:12:09Z"
},
"messages": [
{
"from": "client",
"text": "Je cherche un beach club chic pour demain midi",
"places": [],
"created_at": "2026-10-16T09:12:04Z"
},
{
"from": "albert_one",
"text": "Pour un déjeuner chic demain à Cannes, je vous propose ...",
"places": [
{ "name": "La Môme Plage", ... },
{ "name": "Annex Beach", ... },
{ "name": "Plage du Festival", ... }
],
"created_at": "2026-10-16T09:12:09Z"
}
]
}Albert Voice Agent
The Voice Agent calls restaurants in the United States for now; elsewhere the mission moves straight on. When a mission is for a table, and the Voice Agent is on, Albert One phones the venue before anything else. The venue can be one of Albert's places or any restaurant your client names: Albert One finds it on Google Maps in the requested city (it never calls a namesake elsewhere) and takes its number and opening hours from there. A restaurant that lists no phone number, or books online only, goes straight to a concierge. It asks for the table in a natural voice, accepts any time within 30 minutes either side of what your client asked, and gives the name for the booking. There is nothing to call on your side: it starts by itself and reports through the request.
A booking it confirms turns the request to confirmed, and you receive request.updated. It is billed by the minute, to the second, with one minute minimum per call placed, whatever the outcome: a booking, a full house, no answer, and each retry. A call that cannot book (the venue is full, offers a time outside the window, books only online, or does not pick up after its retries) hands the mission on: to Albert Connect when it is on, otherwise to you as mission.handoff, with what the call learnt in the request.
- 1It only calls places from Albert's curated list that have a phone number: restaurants, bars and beach clubs.
- 2It checks the venue's opening hours in the venue's own time zone, and waits for them rather than ringing an empty room.
- 3It introduces itself as calling from Albert One, the concierge, for a client. Asked whether it is an AI, it says so plainly.
- 4Asked for a phone number, it gives Albert One's own line, never your client's. A venue that calls that line back reaches the agent, which matches the call to the booking.
- 5It never gives a card, an email or an address, and never accepts a deposit on your client's behalf.
- 6When the line drops, it calls back 30 seconds later and picks up where it stopped. When nobody answers, it tries again 5 minutes later, up to three attempts.
What a call can end with
Each call ends in one of these outcomes. Only the first books anything.
| Outcome | What happens next |
|---|---|
Confirmed | The table is booked at a time inside the window. The request becomes confirmed (request.updated). |
Alternative | The venue offered another time or day, outside the window. The mission moves on with the offer noted, so a person can ask your client. |
Full | No table. If the venue keeps a waiting list, the agent asked to be put on it. The mission moves on. |
No answer | Retried 5 minutes later, three attempts in all, then the mission moves on. |
Cut | Called back after 30 seconds, resuming the conversation where it stopped. |
Needs a card or online booking | The agent never pays or books online. The mission moves on. |
Calls in the Sandbox
In the Sandbox nothing is dialed. A restaurant mission filed with a sandbox key gets a simulated call about 3 seconds later, which ends about 12 seconds after: the same call object, the same voice.call.started and voice.call.completed webhooks, and the same request statuses as live, with livemode: false.
Magic values choose the outcome: party_size 13 gives full, a time ending in :13 gives no_answer (three tries, 30 s apart), party_size 7 gives alternative; anything else is confirmed. When the call does not book, the mission goes on to the simulated Sandbox concierge.
In Live, calls come from Albert One's US line, in English, to restaurants in the United States for now. Local numbers and other languages are part of Enterprise.
List a request's calls
/v1/requests/{id}/callsEvery call the Voice Agent placed for this request, oldest first: live, or simulated in the Sandbox. The same object travels in the voice.call.started and voice.call.completed webhooks as data.call.
Path parameters
idstringRequiredThe request id.
status is in_progress while the line is open, then completed.
outcome is confirmed, full, alternative, no_answer, cut or other. confirmed carries the party size, the time and the venue's reference when the table is booked.
transcript is filled in the Sandbox; in Live, the summary describes the call.
curl https://albertone.albert-connect.com/v1/requests/744aa039-2755-491c-8c4c-011ec93ba8a4/calls \
-H "Authorization: Bearer $ALBERT_KEY"{
"calls": [
{
"id": "6f0c2c1e-2b1a-4c55-9e8a-0c1d5a2e7f10",
"attempt": 1,
"status": "completed",
"outcome": "confirmed",
"venue": "Sandbox Trattoria",
"summary": "Table confirmed for 2 tonight at 9 pm under the client's name.",
"duration_seconds": 74,
"transcript": [
{ "role": "agent", "text": "Hi, I'm calling from Albert One, the concierge service..." },
{ "role": "venue", "text": "Yes, that works. What name should I put it under?" }
],
"confirmed": { "party_size": 2, "when": "tonight at 9 pm", "reference": "SBX-7C9C1A" },
"livemode": false,
"started_at": "2026-10-17T07:53:24Z",
"ended_at": "2026-10-17T07:53:36Z"
}
]
}Albert Connect
Albert Connect is the network of vetted human concierges behind Albert One. With it on, a mission the AI cannot close goes to one concierge, who talks to your client in a thread you show in your own product, prices the service and delivers it. You can also send a request straight to the network with POST /v1/requests, without a conversation first.
Because real people give their time to your clients, Albert Connect opens after a review by our team. Ask for it on the Modules page of the console, with a few lines on who your clients are and what they will ask for; once it is approved, switch it on there. Until then, POST /v1/requests answers 403 connect_not_enabled and missions come back to you as mission.handoff.
It comes in three monthly levels: Access ($79, 25 missions a month, then $3 each), Plus ($199, 150 missions, then $2 each, and 10% of Albert's transaction fees paid back to you) and Premium ($490, unlimited missions, 20% back, and a named contact). On each mission, Albert's transaction fees are 3% paid by your user on top of the price and 25% of the concierge's margin.
How missions reach a concierge
This is Albert Connect. Without it, nothing below happens: the mission comes back to you as mission.handoff, with the full request, and your own team takes it.
A mission is offered first to the concierges who joined Albert One and cover its city and speciality, for 10 minutes. After that, or at once when none of them fits, it is offered to the wider Albert Connect network.
It is offered to one concierge at a time, 10 minutes each, then to three at once, and the first to accept takes it. Your client only ever deals with one concierge: it is never an auction.
When a concierge accepts, you receive request.assigned and the concierge writes in the request's thread. If nobody has accepted after 45 minutes, Albert's team is alerted and you receive request.unassigned.
A concierge may hand a mission back. It is then offered again: you receive request.unassigned, then request.assigned when another concierge takes it.
In the Sandbox, nothing is dispatched for real: a simulated concierge named Sandbox takes the mission and runs it to completed (or declined with the magic word decline).
The request object
A request is one thing your client wants, handled by one concierge from acceptance to completion.
Attributes
idstringUnique identifier.
referencestring | nullYour own reference, when you sent one.
statusenumsearching,in_progress,waiting_for_client, quoted, confirmed, completed, declined or cancelled. See Statuses.categoryenumWhat kind of request it is. See Categories.
descriptionstringWhat the client wants, as you sent it.
citystring | nullWhere it happens.
whenstring | nullFree-text timing.
date, date_enddate | nullYYYY-MM-DD.
party_sizeinteger | nullNumber of people.
urgentbooleanWhether it was sent as urgent.
userobjectYour identifier and name for the client.
conciergeobject | nullnull until a concierge accepts. Then id,
first_nameandphoto_url. Never a surname, a phone or an email: the thread is how they talk.accepted_attimestamp | nullWhen the concierge accepted.
livemodebooleanfalsefor a Sandbox request: simulated, never booked, called or dispatched.created_at, updated_attimestampISO 8601, UTC.
{
"id": "744aa039-2755-491c-8c4c-011ec93ba8a4",
"reference": "order-8841",
"status": "in_progress",
"category": "restaurant",
"description": "A table for four on Friday evening, quiet.",
"city": "Paris",
"when": null,
"date": "2026-10-16",
"date_end": null,
"party_size": 4,
"urgent": false,
"user": { "id": "u_1001", "name": "Jane Doe" },
"concierge": {
"id": "4c8f86c4-3bdf-4ef8-8e91-da1698b5fc79",
"first_name": "Camille",
"photo_url": "https://..."
},
"accepted_at": "2026-10-12T05:00:10Z",
"livemode": true,
"created_at": "2026-10-12T04:58:50Z",
"updated_at": "2026-10-12T05:00:10Z"
}Create a request
/v1/requestsSends your client's request to Albert. Concierges are rung as soon as it is created. Answers 201 with the request object.
Body
user.idstringRequiredYour own identifier for this client. The same id always maps to the same person on our side.
user.namestringShown to the concierge. Send it: a concierge greets a name, not a number.
user.emailstringKept on file, never shown to the concierge.
user.phonestringKept on file, never shown to the concierge.
descriptionstringRequiredWhat the client wants, 10 characters at least. The more precise, the better the match.
categoryenumOne of the categories. Defaults to special.
citystringWhere it happens. Used to pick a concierge on the spot.
datedateYYYY-MM-DD.
date_enddateYYYY-MM-DD, for a stay or a rental.
whenstringFree text when a date is not enough: "Friday around 8 pm".
party_sizeintegerNumber of people.
venuestring | objectThe venue your client wants, by name ("Le Bernardin") or as { "name": "..." }. It need not be one of Albert's places: for a restaurant, the Voice Agent finds it on Google Maps in that city and calls it.
urgentbooleantruemoves it to the front.referencestringYour own reference. Makes the call safe to retry.
Needs Albert Connect. Without it, this answers 403 connect_not_enabled: use Ask Albert, or ask for Albert Connect in the console.
curl -X POST https://albertone.albert-connect.com/v1/requests \
-H "Authorization: Bearer $ALBERT_KEY" \
-H "Content-Type: application/json" \
-d '{
"user": {
"id": "u_1001",
"name": "Jane Doe"
},
"category": "restaurant",
"description": "A table for four on Friday evening, quiet.",
"city": "Paris",
"date": "2026-10-16",
"party_size": 4,
"reference": "order-8841"
}'{
"request": {
"id": "744aa039-2755-491c-8c4c-011ec93ba8a4",
"reference": "order-8841",
"status": "searching",
"category": "restaurant",
"description": "A table for four on Friday evening, quiet.",
"city": "Paris",
"when": null,
"date": "2026-10-16",
"date_end": null,
"party_size": 4,
"urgent": false,
"user": { "id": "u_1001", "name": "Jane Doe" },
"concierge": null,
"accepted_at": null,
"livemode": true,
"created_at": "2026-10-12T04:58:50Z",
"updated_at": "2026-10-12T05:00:10Z"
},
"sandbox": true
}Retrieve a request
/v1/requests/{id}Returns one of your requests, with its concierge once accepted.
Path parameters
idstringRequiredThe request id.
curl https://albertone.albert-connect.com/v1/requests/744aa039-2755-491c-8c4c-011ec93ba8a4 \
-H "Authorization: Bearer $ALBERT_KEY"{
"request": {
"id": "744aa039-2755-491c-8c4c-011ec93ba8a4",
"reference": "order-8841",
"status": "in_progress",
"category": "restaurant",
"description": "A table for four on Friday evening, quiet.",
"city": "Paris",
"when": null,
"date": "2026-10-16",
"date_end": null,
"party_size": 4,
"urgent": false,
"user": { "id": "u_1001", "name": "Jane Doe" },
"concierge": {
"id": "4c8f86c4-3bdf-4ef8-8e91-da1698b5fc79",
"first_name": "Camille",
"photo_url": "https://..."
},
"accepted_at": "2026-10-12T05:00:10Z",
"livemode": true,
"created_at": "2026-10-12T04:58:50Z",
"updated_at": "2026-10-12T05:00:10Z"
}
}List requests
/v1/requestsReturns your requests, newest first.
Query parameters
user_idstringOnly this client's requests (your identifier).
statusenumopen or closed.
limitinteger1 to
100. Defaults to 25.beforetimestampA
created_atvalue: returns what came before it, to page.
curl https://albertone.albert-connect.com/v1/requests?user_id=u_1001&status=open \
-H "Authorization: Bearer $ALBERT_KEY"{
"requests": [
{
"id": "744aa039-2755-491c-8c4c-011ec93ba8a4",
"reference": "order-8841",
"status": "in_progress",
"category": "restaurant",
"description": "A table for four on Friday evening, quiet.",
"city": "Paris",
"when": null,
"date": "2026-10-16",
"date_end": null,
"party_size": 4,
"urgent": false,
"user": { "id": "u_1001", "name": "Jane Doe" },
"concierge": {
"id": "4c8f86c4-3bdf-4ef8-8e91-da1698b5fc79",
"first_name": "Camille",
"photo_url": "https://..."
},
"accepted_at": "2026-10-12T05:00:10Z",
"livemode": true,
"created_at": "2026-10-12T04:58:50Z",
"updated_at": "2026-10-12T05:00:10Z"
}
]
}Cancel a request
/v1/requests/{id}/cancelWithdraws an open request. Its thread closes and the concierge is told.
Path parameters
idstringRequiredThe request id.
curl -X POST https://albertone.albert-connect.com/v1/requests/744aa039-2755-491c-8c4c-011ec93ba8a4/cancel \
-H "Authorization: Bearer $ALBERT_KEY"{
"request": {
"id": "744aa039-2755-491c-8c4c-011ec93ba8a4",
"status": "cancelled",
...
}
}The message object
Each request has one thread between your client and the concierge. It opens with the request and closes with it.
Attributes
idstringUnique identifier.
fromenumclient (written through this API) or concierge.
sender_namestring | nullThe concierge's first name. null for your client.
textstringThe message.
attachmentobject | nullkind is file, photo or quote. A file or photo carries url, name, mime, size. A quote carries title, description,
amount_minor, currency, status.created_attimestampISO 8601, UTC.
{
"id": "e39b9311-ce46-44ff-8ac1-f8f8c9a709e3",
"from": "concierge",
"sender_name": "Camille",
"text": "Hello, I am on it. Which arrondissement do you prefer?",
"attachment": null,
"created_at": "2026-10-12T04:59:02Z"
}List messages
/v1/requests/{id}/messagesReturns the thread, oldest first, 200 messages at most, and whether it is closed.
Path parameters
idstringRequiredThe request id.
Query parameters
afterstringA message id: returns only what came after it.
curl https://albertone.albert-connect.com/v1/requests/744aa039-2755-491c-8c4c-011ec93ba8a4/messages \
-H "Authorization: Bearer $ALBERT_KEY"{
"messages": [
{
"id": "e39b9311-ce46-44ff-8ac1-f8f8c9a709e3",
"from": "concierge",
"sender_name": "Camille",
"text": "Hello, I am on it. Which arrondissement do you prefer?",
"attachment": null,
"created_at": "2026-10-12T04:59:02Z"
}
],
"closed": false
}Send a message
/v1/requests/{id}/messagesYour client writes in the thread. The concierge's phone is notified. Your client can write before a concierge accepts: the concierge reads it when they take the request.
Path parameters
idstringRequiredThe request id.
Body
textstringThe message. Required unless
file_idis sent.file_idstringA file uploaded with
POST /v1/files.
Writing in a closed thread answers 409 thread_closed. Send a new request to talk again.
curl -X POST https://albertone.albert-connect.com/v1/requests/744aa039-2755-491c-8c4c-011ec93ba8a4/messages \
-H "Authorization: Bearer $ALBERT_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "The 8th or the 16th, thank you."
}'{
"message": {
"id": "b8faa09c-7574-47ed-a2d0-2650cec3f4d7",
"from": "client",
"sender_name": null,
"text": "The 8th or the 16th, thank you.",
"attachment": null,
"created_at": "2026-10-12T05:00:17Z"
}
}Upload a file
/v1/filesStores a file and returns a file_id to attach to a message. Send the raw bytes as the body with the right Content-Type, or a multipart/form-data body with a file field. 25 MB at most; HTML, SVG and script files are refused. Images reach the concierge as photos, anything else as a document.
Query parameters
filenamestringThe file's name, for a raw upload.
curl -X POST https://albertone.albert-connect.com/v1/files?filename=brief.pdf \
-H "Authorization: Bearer $ALBERT_KEY" \
-H "Content-Type: application/pdf" \
--data-binary @brief.pdf{
"file": {
"file_id": "partner/7d53.../24dc0779-...-brief.pdf",
"name": "brief.pdf",
"mime": "application/pdf",
"size": 182044,
"url": "https://..."
}
}Payment flow
The concierge who accepted the request sets the price: what the supplier costs plus their own fee. Albert adds its service fee of 3% on top, shown to your client as a separate line. You never compute a price: you receive it in quote.created.
Your client pays on a hosted checkout run by Tazapay, Albert's licensed payment provider. Tazapay has no native mobile SDK: open checkout_url in an in-app browser (SFSafariViewController on iOS, Chrome Custom Tabs on Android), never in a bare WebView.
The money is held by Albert through Tazapay until the service is done. It is released when you confirm delivery, or automatically 72 hours after the service date if no dispute is open. A dispute freezes it until Albert's team decides to release or to refund.
| Step | Your side | Webhook |
|---|---|---|
1 | POST /v1/requests | request.assigned or request.unassigned |
2 | Wait for the price | quote.created |
3 | Open quote.checkout_url in an in-app browser | payment.succeeded, payment.failed, payment.expired |
4 | After the service, POST /v1/requests/{id}/confirm-delivery | escrow.released |
4 bis | If something went wrong, POST /v1/requests/{id}/dispute | dispute.opened, then escrow.released or refund.created |
Return URL
When your client finishes or leaves the checkout, the page sends them back to your return URL with ?quote_id=...&status=success or status=cancelled. Albert sets this address for your account: ask your Albert contact. Prefer an https universal link (iOS) or app link (Android) that opens your app.
Never treat the return as proof of payment: a client can close the browser, or open the address by hand. Confirm with the payment.succeeded webhook, or read GET /v1/requests/{id}/quote, whose status is paid once the money is held.
https://app.yourcompany.com/albert/return?quote_id=5a1f3c2e-...&status=successOpening the checkout in your app
Read checkout_url from quote.created, or from GET /v1/requests/{id}/checkout when your client taps Pay. The address is stable for the life of the quote; Albert opens the payment session when it is visited.
Use the system in-app browser: it shares the payment methods and autofill the client already trusts, and the page is isolated from your app.
import * as WebBrowser from "expo-web-browser";
// Closes by itself when the checkout redirects to your return URL.
const result = await WebBrowser.openAuthSessionAsync(
checkoutUrl,
"https://app.yourcompany.com/albert/return"
);
// Or simply: await WebBrowser.openBrowserAsync(checkoutUrl);
// Bare React Native: InAppBrowser.open(checkoutUrl)
// from react-native-inappbrowser-reborn.
// Then confirm the payment with your server (webhook or GET /quote).import SafariServices
let safari = SFSafariViewController(url: URL(string: checkoutUrl)!)
safari.dismissButtonStyle = .close
present(safari, animated: true)
// Your universal link brings the client back to the app.import androidx.browser.customtabs.CustomTabsIntent
val tab = CustomTabsIntent.Builder()
.setShowTitle(true)
.build()
tab.launchUrl(context, Uri.parse(checkoutUrl))
// Your app link brings the client back to the app.Test mode
Every quote carries a mode. Until Albert enables live payments for your account, it is test: no money moves, and checkout_url opens a simulated checkout page where you choose the outcome (paid, failed or expired). It then sends the client to your return URL like the real one.
To test without a browser, call POST /v1/requests/{id}/quote/simulate on a test-mode quote. The webhooks, the escrow and the release behave exactly as in live mode, so the integration you test is the one you ship.
The quote object
The price of a request, set by the concierge. A new quote replaces the open one (the old one becomes superseded). One quote per request ends up paid.
Attributes
idstringUnique identifier.
request_idstringThe request it prices.
statusenumopen, paid, expired, superseded or cancelled.
modeenumtest (no money moves) or live.
amount_minorintegerWhat your client pays, in the smallest unit of the currency:
subtotal_minorplusfee_minor.currencystringISO 4217, upper case.
subtotal_minorintegerThe service as priced by the concierge.
fee_minorintegerAlbert's service fee, 3% of the subtotal.
descriptionstringWhat is being paid for.
service_date, service_date_enddate | nullWhen the service takes place. The automatic release counts from the last day.
expires_attimestampAfter this, the quote can no longer be paid (
payment.expired).checkout_urlstringThe page your client pays on. Open it in an in-app browser.
paid_attimestamp | nullWhen the payment succeeded.
created_attimestampISO 8601, UTC.
{
"id": "5a1f3c2e-8d7b-4e60-9a21-3f4b5c6d7e80",
"request_id": "744aa039-2755-491c-8c4c-011ec93ba8a4",
"status": "open",
"mode": "test",
"amount_minor": 1133000,
"currency": "USD",
"subtotal_minor": 1100000,
"fee_minor": 33000,
"description": "Private room for four, tasting menu and wine pairing",
"service_date": "2026-10-16",
"service_date_end": null,
"expires_at": "2026-10-14T09:14:00Z",
"checkout_url": "https://albertone.albert-connect.com/pay/5a1f3c2e-...?k=...",
"paid_at": null,
"created_at": "2026-10-12T09:14:00Z"
}The escrow object
The money your client paid, held by Albert through Tazapay until the service is delivered.
Attributes
idstringUnique identifier.
request_id, quote_idstringThe request and the quote that was paid.
statusenumheld, disputed, released or refunded.
modeenumtest or live, as the quote.
amount_minorintegerWhat was paid.
currencystringISO 4217, upper case.
held_attimestampWhen the payment succeeded.
release_aftertimestampWhen it is released automatically if you have not confirmed and no dispute is open: 72 hours after the service date.
released_at, release_reasontimestamp, enum | nullclient_confirmed(your call),auto_after_serviceor admin.disputed_at, dispute_reasontimestamp, string | nullSet by POST /dispute.
refunded_at, refund_minortimestamp, integer | nullSet when Albert refunds the client.
{
"id": "c2d9e4f1-0b3a-4c7d-8e65-1a2b3c4d5e6f",
"request_id": "744aa039-2755-491c-8c4c-011ec93ba8a4",
"quote_id": "5a1f3c2e-8d7b-4e60-9a21-3f4b5c6d7e80",
"status": "held",
"mode": "test",
"amount_minor": 1133000,
"currency": "USD",
"held_at": "2026-10-12T09:20:41Z",
"release_after": "2026-10-20T00:00:00Z",
"released_at": null,
"release_reason": null,
"disputed_at": null,
"dispute_reason": null,
"refunded_at": null,
"refund_minor": null
}Retrieve the quote
/v1/requests/{id}/quoteReturns the current quote of a request: the paid one once paid, otherwise the latest. 404 no_quote while the concierge has not priced it.
Path parameters
idstringRequiredThe request id.
curl https://albertone.albert-connect.com/v1/requests/744aa039-2755-491c-8c4c-011ec93ba8a4/quote \
-H "Authorization: Bearer $ALBERT_KEY"{
"quote": {
"id": "5a1f3c2e-8d7b-4e60-9a21-3f4b5c6d7e80",
"request_id": "744aa039-2755-491c-8c4c-011ec93ba8a4",
"status": "open",
"mode": "test",
"amount_minor": 1133000,
"currency": "USD",
"subtotal_minor": 1100000,
"fee_minor": 33000,
"description": "Private room for four, tasting menu and wine pairing",
"service_date": "2026-10-16",
"service_date_end": null,
"expires_at": "2026-10-14T09:14:00Z",
"checkout_url": "https://albertone.albert-connect.com/pay/5a1f3c2e-...?k=...",
"paid_at": null,
"created_at": "2026-10-12T09:14:00Z"
}
}Get the checkout URL
/v1/requests/{id}/checkoutReturns the address your client pays on for the open quote. Open it in an in-app browser.
Path parameters
idstringRequiredThe request id.
curl https://albertone.albert-connect.com/v1/requests/744aa039-2755-491c-8c4c-011ec93ba8a4/checkout \
-H "Authorization: Bearer $ALBERT_KEY"{
"checkout_url": "https://albertone.albert-connect.com/pay/5a1f3c2e-...?k=...",
"quote_id": "5a1f3c2e-8d7b-4e60-9a21-3f4b5c6d7e80",
"amount_minor": 1133000,
"currency": "USD",
"expires_at": "2026-10-14T09:14:00Z",
"mode": "test"
}Confirm delivery
/v1/requests/{id}/confirm-deliveryYour client confirms the service was delivered: the money is released to the concierge and the request is completed. Call it after the service, never before. Calling it twice is harmless.
Path parameters
idstringRequiredThe request id.
A disputed escrow answers 409 escrow_disputed: Albert's team settles it.
curl -X POST https://albertone.albert-connect.com/v1/requests/744aa039-2755-491c-8c4c-011ec93ba8a4/confirm-delivery \
-H "Authorization: Bearer $ALBERT_KEY"{
"escrow": {
"id": "c2d9e4f1-0b3a-4c7d-8e65-1a2b3c4d5e6f",
"request_id": "744aa039-2755-491c-8c4c-011ec93ba8a4",
"quote_id": "5a1f3c2e-8d7b-4e60-9a21-3f4b5c6d7e80",
"status": "released",
"mode": "test",
"amount_minor": 1133000,
"currency": "USD",
"held_at": "2026-10-12T09:20:41Z",
"release_after": "2026-10-20T00:00:00Z",
"released_at": "2026-10-17T10:02:11Z",
"release_reason": "client_confirmed",
"disputed_at": null,
"dispute_reason": null,
"refunded_at": null,
"refund_minor": null
}
}Open a dispute
/v1/requests/{id}/disputeYour client reports a problem with the service. The release is frozen and Albert's team is alerted; they release the money or refund your client, and you receive escrow.released or refund.created.
Path parameters
idstringRequiredThe request id.
Body
reasonstringRequiredWhat went wrong, in the client's words.
curl -X POST https://albertone.albert-connect.com/v1/requests/744aa039-2755-491c-8c4c-011ec93ba8a4/dispute \
-H "Authorization: Bearer $ALBERT_KEY" \
-H "Content-Type: application/json" \
-d '{
"reason": "The private room was not available on arrival."
}'{
"escrow": {
"id": "c2d9e4f1-0b3a-4c7d-8e65-1a2b3c4d5e6f",
"request_id": "744aa039-2755-491c-8c4c-011ec93ba8a4",
"quote_id": "5a1f3c2e-8d7b-4e60-9a21-3f4b5c6d7e80",
"status": "disputed",
"mode": "test",
"amount_minor": 1133000,
"currency": "USD",
"held_at": "2026-10-12T09:20:41Z",
"release_after": "2026-10-20T00:00:00Z",
"released_at": null,
"release_reason": null,
"disputed_at": "2026-10-16T21:40:00Z",
"dispute_reason": "The private room was not available on arrival.",
"refunded_at": null,
"refund_minor": null
}
}Retrieve the escrow
/v1/requests/{id}/escrowReturns where the money of a paid request stands. 404 no_escrow before payment.
Path parameters
idstringRequiredThe request id.
curl https://albertone.albert-connect.com/v1/requests/744aa039-2755-491c-8c4c-011ec93ba8a4/escrow \
-H "Authorization: Bearer $ALBERT_KEY"{
"escrow": {
"id": "c2d9e4f1-0b3a-4c7d-8e65-1a2b3c4d5e6f",
"request_id": "744aa039-2755-491c-8c4c-011ec93ba8a4",
"quote_id": "5a1f3c2e-8d7b-4e60-9a21-3f4b5c6d7e80",
"status": "held",
"mode": "test",
"amount_minor": 1133000,
"currency": "USD",
"held_at": "2026-10-12T09:20:41Z",
"release_after": "2026-10-20T00:00:00Z",
"released_at": null,
"release_reason": null,
"disputed_at": null,
"dispute_reason": null,
"refunded_at": null,
"refund_minor": null
}
}Simulate a payment
/v1/requests/{id}/quote/simulateTest mode only: settles the open test-mode quote as if your client had paid, failed or let it expire, and sends the same webhooks. A live quote answers 409.
Path parameters
idstringRequiredThe request id.
Body
outcomeenumRequiredsucceeded, failed or expired.
curl -X POST https://albertone.albert-connect.com/v1/requests/744aa039-2755-491c-8c4c-011ec93ba8a4/quote/simulate \
-H "Authorization: Bearer $ALBERT_KEY" \
-H "Content-Type: application/json" \
-d '{
"outcome": "succeeded"
}'{
"quote": { "id": "5a1f3c2e-...", "status": "paid", "mode": "test", ... },
"escrow": { "id": "c2d9e4f1-...", "status": "held", ... }
}The payment object
Older mechanism, kept for integrations built before the checkout: a payment link a concierge adds to the thread by hand. New integrations use Checkout and escrow above.
Attributes
idstringUnique identifier.
statusenumawaiting_paymentor paid.amount_minorintegerIn the smallest unit of the currency: 480000 is EUR 4,
800.00.currencystringISO 4217, upper case.
descriptionstringWhat is being paid.
urlstringThe page your client pays on.
paid_attimestamp | nullWhen it was paid.
created_attimestampISO 8601, UTC.
{
"id": "0b6f2c1e-1a4d-4c55-9f3e-7f1c2d9a8b10",
"status": "awaiting_payment",
"amount_minor": 480000,
"currency": "EUR",
"description": "Dinner for four, private room",
"url": "https://albert-connect.com/p/7KQ2",
"paid_at": null,
"created_at": "2026-10-12T09:14:00Z"
}List payments
/v1/requests/{id}/paymentsReturns every payment link raised on a request. While payments_enabled is false, no link is raised: the concierge agrees the payment with your client in the thread.
Path parameters
idstringRequiredThe request id.
curl https://albertone.albert-connect.com/v1/requests/744aa039-2755-491c-8c4c-011ec93ba8a4/payments \
-H "Authorization: Bearer $ALBERT_KEY"{
"payments": [
{
"id": "0b6f2c1e-1a4d-4c55-9f3e-7f1c2d9a8b10",
"status": "awaiting_payment",
"amount_minor": 480000,
"currency": "EUR",
"description": "Dinner for four, private room",
"url": "https://albert-connect.com/p/7KQ2",
"paid_at": null,
"created_at": "2026-10-12T09:14:00Z"
}
],
"payments_enabled": true
}Events
Albert calls your server when something happens, so you never have to poll. Set one HTTPS address in the console and answer 2xx within 8 seconds. Send yourself a test event from the console to check it.
The address must be public https on the standard port. Addresses that resolve to private, internal or cloud-metadata networks are refused, and redirects are not followed: a 3xx counts as a failed delivery.
A failed delivery is retried twice, a few seconds apart. The API stays the source of truth: on a missed event, read the request again. The same event can arrive twice; use its id to ignore a repeat. Messages your own client wrote through the API do not come back as webhooks.
| Event | Sent when | data |
|---|---|---|
mission.handoff | A mission needs a person and Albert Connect is off on Live: your own team takes it | request |
voice.call.started | The Voice Agent dialed the restaurant (simulated in the Sandbox) | request, call |
voice.call.completed | The call ended: outcome, summary, duration, and the confirmed booking if any | request, call |
request.assigned | A Connect member accepted the request | request |
request.accepted | Same moment as request.assigned; the earlier name, still sent | request |
request.unassigned | Nobody accepted within 45 minutes. Albert keeps looking | request |
request.updated | The status changed, or the request changed hands. A table the Voice Agent booked arrives here as confirmed | request |
message.created | The concierge wrote in the thread | request, message |
payment.created | A payment link was raised (payment links) | request, payment |
payment.paid | The client paid | request, payment |
quote.created | The concierge priced the request (amount, currency, description, expiry, checkout_url) | request, quote |
payment.succeeded | The client paid the quote; the money is held | request, quote |
payment.failed | A payment attempt failed; the quote stays payable | request, quote |
payment.expired | The quote expired unpaid | request, quote |
escrow.released | The money was released to the concierge | request, quote, escrow |
dispute.opened | Your dispute froze the release | request, quote, escrow |
refund.created | Albert refunded your client | request, quote, escrow |
webhook.test | You sent a test event from the console | message |
{
"id": "b1d0c7a2-5f0e-4c1a-9d33-0e8f2a7c6b41",
"type": "message.created",
"created_at": "2026-10-12T04:59:03.120Z",
"data": {
"request": { ... },
"message": { ... }
}
}Verifying signatures
Every webhook carries an X-Albert-Signature header: t=<unix seconds>,v1=<hex>. The v1 value is the HMAC-SHA256 of <t>.<raw body>, keyed with your signing secret from the console.
Compute it on the raw bytes, before parsing the JSON, compare in constant time, and reject a t older than five minutes.
import crypto from "node:crypto";
export function isFromAlbert(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(
header.split(",").map((part) => part.split("="))
);
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}import hmac, hashlib, time
def is_from_albert(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts["t"])) > 300:
return False
signed = parts["t"].encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])Statuses
A request moves through these states. The last three close the thread.
| Status | Meaning |
|---|---|
searching | Sent to concierges, nobody has accepted yet |
in_progress | A concierge accepted and is working on it |
waiting_for_client | The concierge asked something and waits for the answer |
quoted | A price was given (quote.created) |
confirmed | The client paid; the money is held until delivery |
completed | Done |
declined | Albert could not take it |
cancelled | Withdrawn by you or by the client |
Categories
The category tells Albert which concierges to ring. GET /v1/categories returns the same list.
| Category | For |
|---|---|
restaurant | A table, a private room, a chef's table |
hotel | A room, a suite, a late check-out |
villa | A villa or a house for a stay |
jet | A private flight |
yacht | A yacht, for a day or a charter |
car | A car with or without a chauffeur |
car_rent | A car to rent |
car_buy | A car to buy |
driver | A driver, a transfer |
chef | A private chef |
ticket | Seats: sport, concert, show, fashion |
event | An event to organize or to get into |
photographer | A photographer or a film crew |
watch | A watch to source |
object | A rare object to source |
property_buy | A property to buy |
property_rent | A property to rent long term |
logistics | Moving something or somebody |
special | Anything else |