ALBERT

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.

Base URL
https://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/v1

How it works

A complete integration follows seven steps.

  1. 1Send the request with POST /v1/requests. It starts in the status searching.
  2. 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.assigned with their first name and photo, or request.unassigned if nobody has accepted after 45 minutes (Albert's team is alerted at the same moment and keeps looking).
  3. 3Your client writes with POST /v1/requests/{id}/messages. Every reply from the concierge reaches you as message.created. Files go both ways: upload with POST /v1/files, attach the file_id to a message.
  4. 4The concierge prices the request. You receive quote.created with the amount and a checkout_url.
  5. 5Open checkout_url in an in-app browser. Your client pays on the hosted page; you receive payment.succeeded, payment.failed or payment.expired.
  6. 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.
  7. 7Something went wrong? POST /v1/requests/{id}/dispute freezes 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.

Authenticated request
curl https://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/v1/status \
  -H "Authorization: Bearer ak_live_..."
Response
{
  "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.

StatusCodesMeaning
401missing_key, invalid_keyNo key, or a key that is not valid
403albert_one_not_enabledAlbert One is not part of your key's subscription
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
429albert_one_quotaThe monthly allowance of Albert One turns is used up. It renews on the 1st, UTC.
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://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

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

  • 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",
  "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.

  • urgentboolean

    true moves it to the front.

  • referencestring

    Your own reference. Makes the call safe to retry.

POST/v1/requests
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"
  }'
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,
    "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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/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",
    "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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/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",
      "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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/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://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."
  }'
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://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
Response · 201
{
  "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 }.

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

  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.

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

  • 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://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"
  }'
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://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"
  }'
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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/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"
    }
  ]
}

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.

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

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.

FeeWho paysHow much
Albert service feeYour client, on top of the price3% of the subtotal (fee_minor)
Payment processingYour client, added on the payment pagePer method, shown before paying (card about 4%)
Payment processing, all_in quoteThe concierge, out of their payout (never added for your client)Card about 4% of amount_minor, given in processing_fee_minor
RefundDeducted from the refundThe 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.

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

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

  • idstring

    Unique identifier.

  • request_idstring

    The request it prices.

  • statusenum

    open, 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.

  • modeenum

    test (no money moves) or live. Fixed when the quote is made. Sandbox-key requests are always test.

  • amount_minorinteger

    The price of the service, in the smallest unit of the currency: subtotal_minor plus fee_minor. The payment processing fee is added on the payment page, per method, before your client pays (unless all_in is true).

  • currencystring

    ISO 4217, upper case.

  • subtotal_minorinteger

    The service as priced by the concierge.

  • fee_minorinteger

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

  • all_inboolean

    true: amount_minor is 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 | null

    all_in quotes: the payment processing fee inside amount_minor (card, about 4%), paid out of the concierge's payout. null when all_in is false: the fee is added on the payment page instead.

  • processing_paid_byenum

    Who carries the payment processing fee: client (added on the payment page) or concierge (all_in, inside the price).

  • 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,
  "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

  • 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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/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,
    "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

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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/v1/requests/744aa039-2755-491c-8c4c-011ec93ba8a4/checkout \
  -H "Authorization: Bearer $ALBERT_KEY"
Response · 200
{
  "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

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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/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://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."
  }'
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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/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://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"
  }'
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://whdshixpoazdmitiatfo.supabase.co/functions/v1/partner-api/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.

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
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 handsrequest
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 organise 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