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.

Base URL
https://albertone.albert-connect.com/v1

How it works

From sign-up to production, an integration follows six steps.

  1. 1Create your account in the console with your work email. The Sandbox is free for every account and fully simulated.
  2. 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.
  3. 3Open a conversation for your client with POST /v1/one/conversations and show Albert One's reply and its place cards. Post each next message with POST /v1/one/conversations/{id}/messages.
  4. 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 as mission.handoff.
  5. 5Set your webhook address in the console and follow the mission with the events: request.updated, request.assigned, message.created, mission.handoff.
  6. 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.

Authenticated request
curl https://albertone.albert-connect.com/v1/status \
  -H "Authorization: Bearer ak_test_..."
Response
{
  "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.

PlanA month (Ask / Voice / Both)Ask AlbertVoice AgentBeyond the quota
SandboxFree, every accountSimulated: scripted answers, fictional places, 1,000 turns a monthSimulated calls, no phone ringsNever billed
Pay as you goNo feeFirst 2,000 $0.08, to 10,000 $0.06, to 50,000 $0.05, beyond $0.045Per minute: first 1,000 $0.45, to 10,000 $0.38, beyond $0.30No quota; $500 spending cap by default
Starter$49 / $69 / $1091,000 turns150 minutes$0.07 a turn, $0.50 a minute
Essential$149 / $179 / $2993,000 turns450 minutes$0.06 a turn, $0.45 a minute
Growth$390 / $449 / $7908,000 turns1,200 minutes$0.05 a turn, $0.40 a minute
Scale$690 / $1,090 / $1,69015,000 turns3,000 minutes$0.05 a turn, $0.37 a minute
EnterpriseOn quotePer member or per seatLocal numbers, a voice in your brandSLA, 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.

StatusCodesMeaning
401missing_key, invalid_keyNo key, or a key that is not valid
401key_expiredThe key was rolled or revoked. Use the current key from the console
403ask_not_enabledAsk Albert is switched off for this key. Switch it on in the console
403connect_not_enabledA request to a human concierge needs Albert Connect, which opens after a review by our team
403plan_lacks_ask, plan_lacks_voiceYour subscription's bundle does not include this module. Add it in Plan & billing.
404not_foundUnknown path, or a request or conversation that is not yours
409already_closed, thread_closed, thread_not_readyThe request or its thread is closed, or not open yet
409mission_filed, conversation_closedThe Albert One conversation filed its mission (write in the request's thread instead), or it is closed
402live_requires_subscriptionA live key (ak_live_) needs an active plan. Use a sandbox key, or choose a plan in the console.
429test_quotaSandbox keys get 1,000 simulated turns a month. Wait for the 1st (UTC), or go live.
403role_forbiddenConsole actions only: your team role cannot do this. Ask an owner of the account.
402spend_cap_reachedPay 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 / 409no_quote, no_escrow, escrow_not_held, escrow_disputedNothing to pay, confirm or dispute yet, or the escrow is not in a state that allows it
503payments_disabledLive payments are paused on Albert's side. Test-mode quotes are not affected.
503albert_one_unavailableAlbert One could not answer this time. Retry with the same message.
413file_too_largeA file over 25 MB
415unsupported_typeA file type that is not accepted
422user_id_required, description_required, invalid_category, invalid_date, invalid_party_size, invalid_file, empty_message, request_refusedThe body is not acceptable; the message says why
500server_errorOn our side. Safe to retry with the same reference.
Error response
{
  "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.

Retry-safe request
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 valueModuleWhat happens
party_size 13Voice AgentThe restaurant is full (outcome full). The mission goes to the Sandbox concierge.
A time ending in :13 (21:13, 9:13pm)Voice AgentNobody answers (no_answer): three calls, 30 s apart, then the Sandbox concierge.
party_size 7Voice AgentOnly another time is offered (alternative, 22:30). The mission goes to the Sandbox concierge.
The word decline in the requestAlbert ConnectNo concierge takes it: the request ends declined.
Anything elseBothA restaurant mission is confirmed by the call; any other mission is taken, confirmed and completed by the Sandbox concierge.
A Sandbox call, GET /v1/requests/{id}/calls
{
  "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.

ToolWhat it does
ask_albertTalk to Albert One for a user: open a conversation or continue one. Returns the reply, place cards, and the mission once filed.
get_conversationA conversation so far, with its mission.
create_requestSend a request to a human concierge (Albert Connect).
get_requestOne request, its status and concierge.
list_requestsYour requests, newest first, filtered by user or status.
get_callsThe Voice Agent's calls for a request, with outcome, summary and transcript.
list_messagesThe messages in a request's thread.
send_messageWrite in a request's thread as your user.
cancel_requestWithdraw an open request.
list_categoriesWhat can be asked for.
get_statusThe key's account, plan, mode (sandbox or live) and modules.
Claude Code
claude mcp add --transport http albert-one https://albertone.albert-connect.com/mcp \
  --header "Authorization: Bearer ak_test_..."
Clients with a JSON config
{
  "mcpServers": {
    "albert-one": {
      "type": "http",
      "url": "https://albertone.albert-connect.com/mcp",
      "headers": { "Authorization": "Bearer ak_test_..." }
    }
  }
}
OpenAI Agents SDK (Python)
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.

GET /v1/status
{
  "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.

  1. 1When your client taps your concierge button, open a conversation with POST /v1/one/conversations, with or without their first message.
  2. 2Show reply as Albert One's message, and each entry of places as a card: the photo, the name, then subcategory · price_range.
  3. 3Post each message your client writes with POST /v1/one/conversations/{id}/messages, and show the reply and places that come back.
  4. 4When done is true, the mission is filed: conversation.mission is the request, in the status searching.
  5. 5Switch your interface to the mission's thread: read and write with /v1/requests/{id}/messages, and follow the request with the webhooks, from request.assigned on.
  6. 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

  • idstring

    Unique identifier. Keep it to post the next messages.

  • statusenum

    open while Albert One converses, filed once the mission is filed, or closed.

  • citystring | null

    The city hint you sent.

  • langstring

    The language you sent, en by default. Albert One then mirrors the language your client writes in.

  • missionobject | null

    null until the mission is filed. Then the request object, whose reference is one:<conversation id>.

  • created_at, updated_attimestamp

    ISO 8601, UTC.

The conversation object
{
  "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

  • idstring

    Unique identifier.

  • namestring

    The place's name.

  • categorystring

    What kind of place it is.

  • subcategorystring | null

    A finer description, to show under the name.

  • addressstring | null

    The street address.

  • price_rangestring | null

    How expensive it is, to show next to the subcategory.

  • photostring | null

    A photo URL, or null.

  • citystring

    The city it is in.

The place card
{
  "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

POST/v1/one/conversations

Opens 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.idstringRequired

    Your own identifier for this client.

  • user.namestring

    Albert One greets a name, and the concierge sees it on the mission.

  • user.emailstring

    Kept on file.

  • user.phonestring

    Kept on file.

  • citystring

    A hint: the city your client is in or browsing.

  • langstring

    Defaults to en. Albert One then mirrors the language your client writes in.

  • messagestring

    Your client's first message. Without it, Albert One greets your client.

POST/v1/one/conversations
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"
  }'
Response · 201
{
  "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

POST/v1/one/conversations/{id}/messages

Posts 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

  • idstringRequired

    The conversation id.

Body

  • textstringRequired

    Your 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.

POST/v1/one/conversations/{id}/messages
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"
  }'
Response · 200
{
  "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

GET/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

  • idstringRequired

    The conversation id.

GET/v1/one/conversations/{id}
curl https://albertone.albert-connect.com/v1/one/conversations/8d2e6f14-3b7a-4c91-a5e0-6f1d2c3b4a59 \
  -H "Authorization: Bearer $ALBERT_KEY"
Response · 200
{
  "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.

  1. 1It only calls places from Albert's curated list that have a phone number: restaurants, bars and beach clubs.
  2. 2It checks the venue's opening hours in the venue's own time zone, and waits for them rather than ringing an empty room.
  3. 3It introduces itself as calling from Albert One, the concierge, for a client. Asked whether it is an AI, it says so plainly.
  4. 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.
  5. 5It never gives a card, an email or an address, and never accepts a deposit on your client's behalf.
  6. 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.

OutcomeWhat happens next
ConfirmedThe table is booked at a time inside the window. The request becomes confirmed (request.updated).
AlternativeThe venue offered another time or day, outside the window. The mission moves on with the offer noted, so a person can ask your client.
FullNo table. If the venue keeps a waiting list, the agent asked to be put on it. The mission moves on.
No answerRetried 5 minutes later, three attempts in all, then the mission moves on.
CutCalled back after 30 seconds, resuming the conversation where it stopped.
Needs a card or online bookingThe 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

GET/v1/requests/{id}/calls

Every 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

  • idstringRequired

    The 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.

GET/v1/requests/{id}/calls
curl https://albertone.albert-connect.com/v1/requests/744aa039-2755-491c-8c4c-011ec93ba8a4/calls \
  -H "Authorization: Bearer $ALBERT_KEY"
Response · 200
{
  "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

  • idstring

    Unique identifier.

  • referencestring | null

    Your own reference, when you sent one.

  • statusenum

    searching, in_progress, waiting_for_client, quoted, confirmed, completed, declined or cancelled. See Statuses.

  • categoryenum

    What kind of request it is. See Categories.

  • descriptionstring

    What the client wants, as you sent it.

  • citystring | null

    Where it happens.

  • whenstring | null

    Free-text timing.

  • date, date_enddate | null

    YYYY-MM-DD.

  • party_sizeinteger | null

    Number of people.

  • urgentboolean

    Whether it was sent as urgent.

  • userobject

    Your identifier and name for the client.

  • conciergeobject | null

    null until a concierge accepts. Then id, first_name and photo_url. Never a surname, a phone or an email: the thread is how they talk.

  • accepted_attimestamp | null

    When the concierge accepted.

  • livemodeboolean

    false for a Sandbox request: simulated, never booked, called or dispatched.

  • created_at, updated_attimestamp

    ISO 8601, UTC.

The request object
{
  "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

POST/v1/requests

Sends your client's request to Albert. Concierges are rung as soon as it is created. Answers 201 with the request object.

Body

  • user.idstringRequired

    Your own identifier for this client. The same id always maps to the same person on our side.

  • user.namestring

    Shown to the concierge. Send it: a concierge greets a name, not a number.

  • user.emailstring

    Kept on file, never shown to the concierge.

  • user.phonestring

    Kept on file, never shown to the concierge.

  • descriptionstringRequired

    What the client wants, 10 characters at least. The more precise, the better the match.

  • categoryenum

    One of the categories. Defaults to special.

  • citystring

    Where it happens. Used to pick a concierge on the spot.

  • datedate

    YYYY-MM-DD.

  • date_enddate

    YYYY-MM-DD, for a stay or a rental.

  • whenstring

    Free text when a date is not enough: "Friday around 8 pm".

  • party_sizeinteger

    Number of people.

  • venuestring | object

    The 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.

  • urgentboolean

    true moves it to the front.

  • referencestring

    Your 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.

POST/v1/requests
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"
  }'
Response · 201
{
  "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

GET/v1/requests/{id}

Returns one of your requests, with its concierge once accepted.

Path parameters

  • idstringRequired

    The request id.

GET/v1/requests/{id}
curl https://albertone.albert-connect.com/v1/requests/744aa039-2755-491c-8c4c-011ec93ba8a4 \
  -H "Authorization: Bearer $ALBERT_KEY"
Response · 200
{
  "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

GET/v1/requests

Returns your requests, newest first.

Query parameters

  • user_idstring

    Only this client's requests (your identifier).

  • statusenum

    open or closed.

  • limitinteger

    1 to 100. Defaults to 25.

  • beforetimestamp

    A created_at value: returns what came before it, to page.

GET/v1/requests
curl https://albertone.albert-connect.com/v1/requests?user_id=u_1001&status=open \
  -H "Authorization: Bearer $ALBERT_KEY"
Response · 200
{
  "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

POST/v1/requests/{id}/cancel

Withdraws an open request. Its thread closes and the concierge is told.

Path parameters

  • idstringRequired

    The request id.

POST/v1/requests/{id}/cancel
curl -X POST https://albertone.albert-connect.com/v1/requests/744aa039-2755-491c-8c4c-011ec93ba8a4/cancel \
  -H "Authorization: Bearer $ALBERT_KEY"
Response · 200
{
  "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

  • idstring

    Unique identifier.

  • fromenum

    client (written through this API) or concierge.

  • sender_namestring | null

    The concierge's first name. null for your client.

  • textstring

    The message.

  • attachmentobject | null

    kind is file, photo or quote. A file or photo carries url, name, mime, size. A quote carries title, description, amount_minor, currency, status.

  • created_attimestamp

    ISO 8601, UTC.

The message object
{
  "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

GET/v1/requests/{id}/messages

Returns the thread, oldest first, 200 messages at most, and whether it is closed.

Path parameters

  • idstringRequired

    The request id.

Query parameters

  • afterstring

    A message id: returns only what came after it.

GET/v1/requests/{id}/messages
curl https://albertone.albert-connect.com/v1/requests/744aa039-2755-491c-8c4c-011ec93ba8a4/messages \
  -H "Authorization: Bearer $ALBERT_KEY"
Response · 200
{
  "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

POST/v1/requests/{id}/messages

Your 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

  • idstringRequired

    The request id.

Body

  • textstring

    The message. Required unless file_id is sent.

  • file_idstring

    A file uploaded with POST /v1/files.

Writing in a closed thread answers 409 thread_closed. Send a new request to talk again.

POST/v1/requests/{id}/messages
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."
  }'
Response · 201
{
  "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

POST/v1/files

Stores 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

  • filenamestring

    The file's name, for a raw upload.

POST/v1/files
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
Response · 201
{
  "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.

StepYour sideWebhook
1POST /v1/requestsrequest.assigned or request.unassigned
2Wait for the pricequote.created
3Open quote.checkout_url in an in-app browserpayment.succeeded, payment.failed, payment.expired
4After the service, POST /v1/requests/{id}/confirm-deliveryescrow.released
4 bisIf something went wrong, POST /v1/requests/{id}/disputedispute.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.

Return
https://app.yourcompany.com/albert/return?quote_id=5a1f3c2e-...&status=success

Opening 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.

React Native (Expo)
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).
Swift (iOS)
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.
Kotlin (Android)
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

  • idstring

    Unique identifier.

  • request_idstring

    The request it prices.

  • statusenum

    open, paid, expired, superseded or cancelled.

  • modeenum

    test (no money moves) or live.

  • amount_minorinteger

    What your client pays, in the smallest unit of the currency: subtotal_minor plus fee_minor.

  • currencystring

    ISO 4217, upper case.

  • subtotal_minorinteger

    The service as priced by the concierge.

  • fee_minorinteger

    Albert's service fee, 3% of the subtotal.

  • descriptionstring

    What is being paid for.

  • service_date, service_date_enddate | null

    When the service takes place. The automatic release counts from the last day.

  • expires_attimestamp

    After this, the quote can no longer be paid (payment.expired).

  • checkout_urlstring

    The page your client pays on. Open it in an in-app browser.

  • paid_attimestamp | null

    When the payment succeeded.

  • created_attimestamp

    ISO 8601, UTC.

The quote object
{
  "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

  • idstring

    Unique identifier.

  • request_id, quote_idstring

    The request and the quote that was paid.

  • statusenum

    held, disputed, released or refunded.

  • modeenum

    test or live, as the quote.

  • amount_minorinteger

    What was paid.

  • currencystring

    ISO 4217, upper case.

  • held_attimestamp

    When the payment succeeded.

  • release_aftertimestamp

    When 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 | null

    client_confirmed (your call), auto_after_service or admin.

  • disputed_at, dispute_reasontimestamp, string | null

    Set by POST /dispute.

  • refunded_at, refund_minortimestamp, integer | null

    Set when Albert refunds the client.

The escrow object
{
  "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

GET/v1/requests/{id}/quote

Returns 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

  • idstringRequired

    The request id.

GET/v1/requests/{id}/quote
curl https://albertone.albert-connect.com/v1/requests/744aa039-2755-491c-8c4c-011ec93ba8a4/quote \
  -H "Authorization: Bearer $ALBERT_KEY"
Response · 200
{
  "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

GET/v1/requests/{id}/checkout

Returns the address your client pays on for the open quote. Open it in an in-app browser.

Path parameters

  • idstringRequired

    The request id.

GET/v1/requests/{id}/checkout
curl https://albertone.albert-connect.com/v1/requests/744aa039-2755-491c-8c4c-011ec93ba8a4/checkout \
  -H "Authorization: Bearer $ALBERT_KEY"
Response · 200
{
  "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

POST/v1/requests/{id}/confirm-delivery

Your 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

  • idstringRequired

    The request id.

A disputed escrow answers 409 escrow_disputed: Albert's team settles it.

POST/v1/requests/{id}/confirm-delivery
curl -X POST https://albertone.albert-connect.com/v1/requests/744aa039-2755-491c-8c4c-011ec93ba8a4/confirm-delivery \
  -H "Authorization: Bearer $ALBERT_KEY"
Response · 200
{
  "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

POST/v1/requests/{id}/dispute

Your 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

  • idstringRequired

    The request id.

Body

  • reasonstringRequired

    What went wrong, in the client's words.

POST/v1/requests/{id}/dispute
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."
  }'
Response · 200
{
  "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

GET/v1/requests/{id}/escrow

Returns where the money of a paid request stands. 404 no_escrow before payment.

Path parameters

  • idstringRequired

    The request id.

GET/v1/requests/{id}/escrow
curl https://albertone.albert-connect.com/v1/requests/744aa039-2755-491c-8c4c-011ec93ba8a4/escrow \
  -H "Authorization: Bearer $ALBERT_KEY"
Response · 200
{
  "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

POST/v1/requests/{id}/quote/simulate

Test 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

  • idstringRequired

    The request id.

Body

  • outcomeenumRequired

    succeeded, failed or expired.

POST/v1/requests/{id}/quote/simulate
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"
  }'
Response · 200
{
  "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

  • idstring

    Unique identifier.

  • statusenum

    awaiting_payment or paid.

  • amount_minorinteger

    In the smallest unit of the currency: 480000 is EUR 4,800.00.

  • currencystring

    ISO 4217, upper case.

  • descriptionstring

    What is being paid.

  • urlstring

    The page your client pays on.

  • paid_attimestamp | null

    When it was paid.

  • created_attimestamp

    ISO 8601, UTC.

The payment object
{
  "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

GET/v1/requests/{id}/payments

Returns 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

  • idstringRequired

    The request id.

GET/v1/requests/{id}/payments
curl https://albertone.albert-connect.com/v1/requests/744aa039-2755-491c-8c4c-011ec93ba8a4/payments \
  -H "Authorization: Bearer $ALBERT_KEY"
Response · 200
{
  "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.

EventSent whendata
mission.handoffA mission needs a person and Albert Connect is off on Live: your own team takes itrequest
voice.call.startedThe Voice Agent dialed the restaurant (simulated in the Sandbox)request, call
voice.call.completedThe call ended: outcome, summary, duration, and the confirmed booking if anyrequest, call
request.assignedA Connect member accepted the requestrequest
request.acceptedSame moment as request.assigned; the earlier name, still sentrequest
request.unassignedNobody accepted within 45 minutes. Albert keeps lookingrequest
request.updatedThe status changed, or the request changed hands. A table the Voice Agent booked arrives here as confirmedrequest
message.createdThe concierge wrote in the threadrequest, message
payment.createdA payment link was raised (payment links)request, payment
payment.paidThe client paidrequest, payment
quote.createdThe concierge priced the request (amount, currency, description, expiry, checkout_url)request, quote
payment.succeededThe client paid the quote; the money is heldrequest, quote
payment.failedA payment attempt failed; the quote stays payablerequest, quote
payment.expiredThe quote expired unpaidrequest, quote
escrow.releasedThe money was released to the conciergerequest, quote, escrow
dispute.openedYour dispute froze the releaserequest, quote, escrow
refund.createdAlbert refunded your clientrequest, quote, escrow
webhook.testYou sent a test event from the consolemessage
Event
{
  "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.

Node.js
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));
}
Python
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.

StatusMeaning
searchingSent to concierges, nobody has accepted yet
in_progressA concierge accepted and is working on it
waiting_for_clientThe concierge asked something and waits for the answer
quotedA price was given (quote.created)
confirmedThe client paid; the money is held until delivery
completedDone
declinedAlbert could not take it
cancelledWithdrawn by you or by the client

Categories

The category tells Albert which concierges to ring. GET /v1/categories returns the same list.

CategoryFor
restaurantA table, a private room, a chef's table
hotelA room, a suite, a late check-out
villaA villa or a house for a stay
jetA private flight
yachtA yacht, for a day or a charter
carA car with or without a chauffeur
car_rentA car to rent
car_buyA car to buy
driverA driver, a transfer
chefA private chef
ticketSeats: sport, concert, show, fashion
eventAn event to organize or to get into
photographerA photographer or a film crew
watchA watch to source
objectA rare object to source
property_buyA property to buy
property_rentA property to rent long term
logisticsMoving something or somebody
specialAnything else