Albert Partner API · v1
A concierge for your clients, by API
The Albert Partner API lets your product hand a client's request to Albert. Albert offers it to the Connect member best placed to deliver it, first in the client's city, then in the same country, then in the same region. Once a member accepts, the two of them talk, exchange files and agree a price through this API, inside your own product. Your client pays on a hosted checkout and Albert holds the money until the service is delivered.
The API is organised around REST. It accepts JSON bodies, returns JSON, and uses standard HTTP status codes. Every request belongs to one of your users and is handled by one concierge.
https://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/v1How it works
A complete integration follows seven steps.
- 1Send the request with
POST /v1/requests. It starts in the statussearching. - 2Albert offers it to one Connect member at a time, 10 minutes each: members in the requested city first, then the same country, then the same region. After three members without an answer it goes to three at once and the first to accept takes it. You receive
request.assignedwith their first name and photo, orrequest.unassignedif nobody has accepted after 45 minutes (Albert's team is alerted at the same moment and keeps looking). - 3Your client writes with
POST /v1/requests/{id}/messages. Every reply from the concierge reaches you asmessage.created. Files go both ways: upload withPOST /v1/files, attach thefile_idto a message. - 4The concierge prices the request. You receive
quote.createdwith the amount and acheckout_url. - 5Open
checkout_urlin an in-app browser. Your client pays on the hosted page; you receivepayment.succeeded,payment.failedorpayment.expired. - 6Albert holds the money until the service is delivered. After the service, call
POST /v1/requests/{id}/confirm-delivery to release it. Without your call it is released 72 hours after the service date, unless a dispute is open. - 7Something went wrong?
POST /v1/requests/{id}/disputefreezes the release until Albert's team settles it.
Authentication
Authenticate every call with your secret key in the Authorization header, as a Bearer token. You create and replace your key in the developer console.
Your key carries real permissions: keep it on your server. Never put it in a mobile app, a web page or a public repository. A missing or wrong key answers 401.
curl https://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/v1/status \
-H "Authorization: Bearer ak_live_..."{
"partner": "Your company",
"sandbox": true,
"webhook_url": "https://api.yourcompany.com/albert/webhook",
"payments_enabled": false,
"albert_one": {
"enabled": true,
"monthly_turns": 2000,
"used_this_month": 12
}
}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 |
403 | albert_one_not_enabled | Albert One is not part of your key's subscription |
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 |
429 | albert_one_quota | The monthly allowance of Albert One turns is used up. It renews on the 1st, UTC. |
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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/v1/requests \
-H "Authorization: Bearer ak_live_..." \
-H "Idempotency-Key: order-8841" \
-H "Content-Type: application/json" \
-d '{ ... }'Sandbox and going live
A new key starts in sandbox. Your requests go to one named person at Albert who plays the concierge, and nobody else is rung. Everything else is real: the thread, the files, the webhooks. GET /v1/status answers sandbox: true.
Payments have their own test mode, independent of the sandbox: every quote carries mode test until Albert enables live payments for your account. See Test mode under Checkout and escrow.
When your integration works end to end, Albert switches your key to live. Nothing changes in your code: the same calls now reach the Connect network.
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.
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",
"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.
urgentbooleantruemoves it to the front.referencestringYour own reference. Makes the call safe to retry.
curl -X POST https://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/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,
"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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/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",
"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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/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",
"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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/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://..."
}
}Albert One
Albert One now has its own developer platform: self-serve sign-up, test and live keys, the console, plans and the full, current documentation live at albertone.albert-connect.com/docs. This page is kept for partners integrated before October 2026.
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: the same object POST /v1/requests creates, with the reference one:<conversation id>. Everything documented under Requests, Messages, Files and Webhooks applies to it: GET /v1/requests/{id}, the thread in /v1/requests/{id}/messages, files, request.assigned and request.unassigned, message events, and quote events when payments are on. Albert One never confirms a booking itself: a concierge does.
The base URL and the authentication are those of the rest of the API. Albert One is an add-on of your key's subscription: until it is enabled, every call under /v1/one answers 403 albert_one_not_enabled.
Each subscription carries a monthly allowance of turns, 2000 by default. A turn is one answer from Albert One. Once the allowance is used up, calls answer 429 albert_one_quota until it renews on the 1st of the month, UTC. GET /v1/status tells you where you stand; without the add-on, albert_one is { "enabled": false }.
{
"partner": "Your company",
"sandbox": true,
"webhook_url": "https://api.yourcompany.com/albert/webhook",
"payments_enabled": false,
"albert_one": {
"enabled": true,
"monthly_turns": 2000,
"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.
How missions reach a concierge
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.
With a sandbox key, the mission only ever goes to the sandbox person at Albert. Nobody real is contacted.
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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/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"
}
]
}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.
checkout_url first opens Albert's payment page. It shows each payment method with its processing fee and the total, and your client ticks one line before paying: "Payment fees are not refunded, including on deposits, cancellations or disputes." Then the page hands over to Tazapay. The payment is collected by Albert Connect Company for the concierge who quoted and recorded against their mission; the concierge is paid out once the service is delivered and their company and bank details are on file.
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 |
Fees
Your client sees every fee before paying. Nothing is taken from you, the partner.
A refund returns what was paid minus the payment processing fee, which the provider does not give back. Your client ticks this line on the payment page before paying.
Currencies with three decimals (BHD, KWD, OMR, JOD, TND, LYD, IQD) cannot be quoted.
| Fee | Who pays | How much |
|---|---|---|
Albert service fee | Your client, on top of the price | 3% of the subtotal (fee_minor) |
Payment processing | Your client, added on the payment page | Per method, shown before paying (card about 4%) |
Payment processing, all_in quote | The concierge, out of their payout (never added for your client) | Card about 4% of amount_minor, given in processing_fee_minor |
Refund | Deducted from the refund | The processing fee is not refunded |
Return URL
When your client finishes or leaves the checkout, the page sends them back to your return URL with ?quote_id=...&request_id=...&status=... 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.
status is one of: success (paid, or already paid when the page was opened), cancelled (the client left the hosted checkout), failed (test mode: a failed payment was simulated), expired (the quote can no longer be paid). Without a return URL, the client lands on an Albert page that says the same thing.
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 your client confirms on the payment page. Opening it twice, or in two tabs, never creates two payments.
Behind checkout_url, the payment page calls POST /pay/{quote_id}/start?k=... with { "method": "card", "accept_fees": true } and answers JSON: { "url": ... } to open, or { "error": { "code", "message" } }. You do not call it yourself; the codes are listed below so your support team can read them: fees_not_accepted (the line was not ticked), method_unavailable, quote_not_open (superseded or expired), payments_paused, currency_not_supported, checkout_failed (try again).
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 quote.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. A request created with a sandbox key (ak_test_) only ever gets test quotes, even on a live account.
quote.mode is decided when the quote is made and never changes: a test quote stays simulated, a live quote is always charged on Tazapay's live account.
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, cancelled or refunded. Only an open quote can be paid; a payment that still reaches a superseded or expired quote is held and refunded, and you receive
dispute.opened.modeenumtest (no money moves) or live. Fixed when the quote is made. Sandbox-key requests are always test.
amount_minorintegerThe price of the service, in the smallest unit of the currency:
subtotal_minorplusfee_minor. The payment processing fee is added on the payment page, per method, before your client pays (unlessall_inistrue).currencystringISO 4217, upper case.
subtotal_minorintegerThe service as priced by the concierge.
fee_minorintegerAlbert's service fee, 3% of the subtotal.
all_inbooleantrue:amount_minoris the whole price, every fee is inside it and nothing is added on the payment page; the card processing fee is taken from the concierge's payout, never from Albert's fee.false: the processing fee of the method your client picks (card about 4%) is shown and added before paying.processing_fee_minorinteger | nullall_inquotes: the payment processing fee insideamount_minor(card, about 4%), paid out of the concierge's payout. null whenall_inisfalse: the fee is added on the payment page instead.processing_paid_byenumWho carries the payment processing fee: client (added on the payment page) or concierge (
all_in, inside the price).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,
"all_in": false,
"processing_fee_minor": null,
"processing_paid_by": "client",
"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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/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,
"all_in": false,
"processing_fee_minor": null,
"processing_paid_by": "client",
"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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/v1/requests/744aa039-2755-491c-8c4c-011ec93ba8a4/checkout \
-H "Authorization: Bearer $ALBERT_KEY"{
"checkout_url": "https://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/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.
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 |
|---|---|---|
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 | 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 organise 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 |