For partners and their developers
Partner guide
Put Talkspresso booking, payment and video inside your own platform, with your name on it.
This guide is for two people. If you run the business, read sections 1, 2, 5, 6 and 8. If you build the integration, read all of it. Sections 3, 4, 7 and 9 are written for you. Every call and answer shown was run against our staging system. Nothing here describes something we have not built.
Looking for the API for your own account or organization, which you use with a key? See the API reference.
1. What you get
You keep your platform and your members. Talkspresso handles booking, payment and video, which are hard to build well.
| You get | What that means |
|---|---|
| Your coaches, listed on your platform | Your members see your coaches and their sessions on your own platform, with your name and look. Buyers do not need a Talkspresso account. |
| Booking and payment handled | Talkspresso checks the coach’s calendar, takes the booking and collects the payment. Your server books free sessions directly. For paid sessions, Talkspresso opens its own payment page and sends the buyer back to you after they pay. |
| The video call | Each booking comes with a video room. Buyers join from a link you give them. You can also show the room inside your own page (see “Show the room in your page”). |
| Booking emails your way | You choose who sends the buyer’s booking emails: Talkspresso, Talkspresso with your name and logo, or your own platform (see “Booking emails”). |
| Updates when a booking changes | Webhooks (messages we send to your server) tell it when a booking is created, paid, moved, cancelled or completed, so your server does not have to keep asking. |
| How money works | Each coach is paid directly into the coach’s own PayPal account. Talkspresso’s fee on bookings from your platform is set in your agreement. Talkspresso does not pay you a share through the platform. Any arrangement between you and your coaches is yours. |
| A test system to build on first | You build and test on a staging copy (our test system) with a sandbox key and PayPal’s sandbox. No real money moves. |
2. How it starts
Six steps, in this order. Section 8 has the checklist that decides when you go live.
Together
We agree the terms. The agreement has a schedule that lists your settings and Talkspresso’s fee. Plans are named Community, Platform and Enterprise. Your agreement says which one applies and what it covers.
Talkspresso
We create your organization on Talkspresso with your name, logo, colors and contact email. Once the agreement is signed we set the settings that only Talkspresso can change (see “Settings you control and settings Talkspresso sets”).
You and your coaches
You invite your coaches. Each coach agrees to be listed on your platform and to let you cancel or move their sessions. A coach who sells paid sessions connects a PayPal account. A coach who is new to Talkspresso may have to wait a short time before buyers can book their paid sessions.
Talkspresso
We give you a sandbox key for staging. It works for 14 days and only on staging. We also register the addresses your build needs: the web address buyers return to after paying, the website that will show the video room, and your webhook address.
Your developers
You build and test against staging. The go-live checklist (section 8) shows what is done and what is left. GET /settings returns the current checklist at any time.
Talkspresso
We run one paid test booking, then refund it, using your final settings and fee. When every item on the checklist is done, we create a production key and give it to you by a method other than email.
3. Flows and calls
Each flow lists who does what, then shows the real call. Every call on this page runs against staging with a sandbox key. Run them in order. The examples use ids from our own test organization. The dates in them will be in the past when you read this: take a start_time from a fresh availability answer, within the next 14 days.
How every call works
| Topic | Rule |
|---|---|
| Base address | Staging: https://api-staging.talkspresso.com/partner/v1. Production: https://api.talkspresso.com/partner/v1 (production keys come after go-live). |
| Your key | Send Authorization: Bearer tsp_... on every call. Keep it on your server. One key belongs to one organization and cannot access any other organization’s data. It can read and write bookings; it cannot create keys or change settings. |
| Scopes | partner:read lets you read. partner:bookings lets you create, cancel and move bookings. partner:webhooks lets you ask for an event to be sent again. Your key has all three. |
| Idempotency-Key | Required on every POST except replay. Use a new value for each action. If you retry the same action, send the same value. 8 to 255 characters: letters, digits and _ . : -. The same key with the same body returns the first answer and adds the header Idempotent-Replayed: true. The same key with a different body is refused with 422. We remember each key for 24 hours. |
| Times | Start times are ISO 8601 with a zone, for example 2026-11-17T16:00:00Z. Answers are in UTC. A time with no zone is refused. |
| partner_ref | Your own order or user reference, up to 128 characters. It comes back on the booking, in webhooks and in GET /bookings?partner_ref=. |
| Money | Amounts are whole US cents (paid_cents) and the currency is always USD. |
export TSP_KEY="tsp_your_sandbox_key"
API=https://api-staging.talkspresso.com/partner/v1Errors
Every error has this shape: { "error": { "code", "message" } }. Some errors also include fields or retry_after_seconds. In your code, use code, not the message text.
| Status | code | What to do |
|---|---|---|
| 400 | validation_failed | A field is missing or wrong. fields lists them. |
| 400 | idempotency_key_required | Add an Idempotency-Key header (also idempotency_key_invalid for a bad value). |
| 401 | invalid_credentials | No key, a wrong key, an expired key or a revoked key. |
| 402 | payment_required | The session is paid. Use POST /checkouts instead of POST /bookings. |
| 403 | missing_scope | The key does not hold the scope this call needs. |
| 403 | org_not_ready | Your organization is not ready to use the partner API. Email support@talkspresso.com. |
| 404 | not_found | The id does not exist, belongs to another organization, or was not created by your platform. |
| 409 | slot_unavailable | Someone took that time or the coach no longer offers it. Ask for availability again. |
| 409 | coach_consent_required | This coach has not agreed that you may cancel or move their sessions. |
| 409 | notifications_mode_not_enabled | You asked for booking emails your organization is not set up for. See “Booking emails”. |
| 409 | webhook_endpoint_required | You asked for notifications “partner” but you have no webhook endpoint that receives booking.created. |
| 409 | request_in_progress | A call with the same Idempotency-Key is still being processed. Wait a moment and retry. |
| 422 | idempotency_key_reused | The same Idempotency-Key was used with a different body. |
| 422 | return_url_not_allowed | The return address must be https on a host we registered for you. |
| 422 | booking_terms_need_buyer_consent | This coach has booking terms the buyer must accept on the payment page. Use a checkout. |
| 429 | rate_limited | Too many calls. retry_after_seconds says how long to wait. |
{
"error": {
"code": "invalid_credentials",
"message": "Invalid or missing API key."
}
}Limits
If you go over a limit, you get 429 with retry_after_seconds. Wait that many seconds before you call again.
| What | Limit |
|---|---|
| Reads | 120 a minute per key |
| Writes | 30 a minute per key |
| Bookings and checkouts opened | 300 a day per key, for at most 150 different buyers a day |
| Checkouts for one buyer email | 5 a minute |
| Cancels and moves | 20 an hour each |
| Room links | 60 an hour |
| Replays of one event | 5 an hour |
| A new key | For its first 24 hours, every limit above is one quarter of the number shown. Plan your first load test for day two. |
Show your coaches and their times
Your server
GET /providers. You get your coaches and the sessions each one offers, with duration and price in cents. You can keep the answer for a few minutes, then ask again.
Your page
Show your coaches and their sessions. A coach with price_cents 0 offers a free session.
Your server
GET /services/:id/intake-questions. Use it when the coach asks the buyer questions before the session. Show the questions in your form and send the answers with the booking.
Your server
GET /services/:id/availability for the dates the buyer is looking at (up to 14 days at a time, in the buyer’s time zone). The times are always current, so you are never offered a time that was just taken.
curl -s "$API/providers" \
-H "Authorization: Bearer $TSP_KEY"{
"data": [
{
"id": "sn_andre",
"name": "Andre Williams",
"title": "Leadership Coach & Former Founder",
"handle": "andre-williams",
"photo_url": "/images/startupnash/andre.jpg",
"headline": "",
"services": [
{
"id": "svc_sn_andre_williams_mentor",
"title": "1:1 Mentorship Session",
"duration_minutes": 30,
"price_cents": 0,
"currency": "USD",
"free": true
}
]
}
]
}curl -s "$API/services/svc_sn_andre_williams_mentor/availability?from=2026-11-17&days=2&timezone=America/Chicago" \
-H "Authorization: Bearer $TSP_KEY"{
"data": {
"service_id": "svc_sn_andre_williams_mentor",
"provider_id": "sn_andre",
"timezone": "America/Chicago",
"duration_minutes": 30,
"slots": [
{
"start_time": "2026-11-17T15:00:00.000Z",
"end_time": "2026-11-17T15:30:00.000Z"
},
{
"start_time": "2026-11-17T15:30:00.000Z",
"end_time": "2026-11-17T16:00:00.000Z"
}
]
}
}Book a free session
Buyer
Picks a time and gives you a name and an email.
Your server
POST /bookings with a new Idempotency-Key. The answer is 201 with the booking and the buyer’s join link.
Talkspresso
Books the time, emails the buyer and the coach, and sends booking.created to your webhook.
Your page
Shows “you are booked”, and shows the join link when it is time to join. If a call fails or times out, send the same request again with the same Idempotency-Key. You will not get two bookings.
curl -s -X POST "$API/bookings" \
-H "Authorization: Bearer $TSP_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042-booking" \
-d '{
"provider_id": "sn_andre",
"service_id": "svc_sn_andre_williams_mentor",
"start_time": "2026-11-17T16:00:00Z",
"buyer": { "name": "Sam Rivera", "email": "sam@example.com", "timezone": "America/Chicago" },
"partner_ref": "order-1042"
}'{
"data": {
"id": "cmwecj3ax00dm1vjzdl3ceij7",
"status": "active",
"approval_status": "auto_approved",
"start_time": "2026-11-17T16:00:00.000Z",
"end_time": "2026-11-17T16:30:00.000Z",
"duration_minutes": 30,
"provider": {
"id": "sn_andre",
"name": "Andre Williams"
},
"service": {
"id": "svc_sn_andre_williams_mentor",
"title": "1:1 Mentorship Session"
},
"buyer": {
"name": "Sam Rivera",
"email": "sam@example.com"
},
"partner_ref": "order-s5-1794536217",
"paid_cents": 0,
"currency": "USD",
"created_at": "2026-11-13T02:16:57.466Z",
"join_url": "https://staging.talkspresso.com/meet/SESSION_ID?cid=BUYER_ID&ct=BUYER_TOKEN",
"notes_available": false
}
}join_url is the buyer’s link to the call. Treat it like a password: share it only with that buyer. A paid session answers this call with 402 payment_required. If a field is missing, you get 400, and fields lists the field names.
{
"error": {
"code": "idempotency_key_reused",
"message": "This Idempotency-Key was already used with a different request."
}
}{
"error": {
"code": "slot_unavailable",
"message": "That time is no longer available.",
"reason": "slot_taken"
}
}Book and pay
Buyer
Picks a time on a paid session and gives you a name and an email.
Your server
POST /checkouts with the same body as a booking, plus return_url (an https address on a host we registered). The answer is 201 with a checkout and a url. It is open for 30 minutes.
Your page
Send the buyer to url exactly as given. Do not log or store it. The part after # lets one buyer pay for this booking.
Buyer
Pays on the Talkspresso payment page, with PayPal or a card. The page shows the coach, the price, the cancellation rule and your organization’s name and logo.
Talkspresso
Sends the buyer back to your return_url with ?checkout_id=...&status=paid added. Talkspresso also sends checkout.paid and booking.paid to your webhook.
Your server
Do not trust status in the address, because the buyer can edit it. Confirm with GET /checkouts/:id (status paid, booking_id set) or wait for the webhook. The webhook arrives even if the buyer closes the tab before returning. A checkout that is not paid in 30 minutes becomes expired.
Your server
GET /bookings/:id for the booking and the join link.
curl -s -X POST "$API/checkouts" \
-H "Authorization: Bearer $TSP_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1043-checkout" \
-d '{
"provider_id": "cmlrh3wdm002l1qhg56q14ydj",
"service_id": "svc-ppcp-walkthrough-50",
"start_time": "2026-11-17T15:30:00Z",
"buyer": { "name": "Sam Rivera", "email": "sam@example.com" },
"partner_ref": "order-1043",
"return_url": "https://partner-e2e.example.com/done"
}'{
"data": {
"id": "cmweclzhd00gf1vjz7ucke94j",
"status": "open",
"url": "https://staging.talkspresso.com/pay/partner/cmweclzhd00gf1vjz7ucke94j#token=CHECKOUT_TOKEN",
"expires_at": "2026-11-13T02:49:12.480Z",
"partner_ref": "order-s5-1794536217-co",
"booking_id": null,
"provider_id": "cmlrh3wdm002l1qhg56q14ydj",
"service_id": "svc-ppcp-walkthrough-50",
"start_time": "2026-11-17T15:30:00.000Z"
}
}curl -s "$API/checkouts/CHECKOUT_ID" \
-H "Authorization: Bearer $TSP_KEY"{
"data": {
"id": "cmweclzhd00gf1vjz7ucke94j",
"status": "open",
"expires_at": "2026-11-13T02:49:12.480Z",
"partner_ref": "order-s5-1794536217-co",
"booking_id": null,
"provider_id": "cmlrh3wdm002l1qhg56q14ydj",
"service_id": "svc-ppcp-walkthrough-50",
"start_time": "2026-11-17T15:30:00.000Z"
}
}A checkout has one of four statuses: open, paid, expired or cancelled. Once it is paid, booking_id has a value. The return address must be https on a host we registered for you. Any other address is refused:
{
"error": {
"code": "return_url_not_allowed",
"message": "return_url must be https on a host registered for this organization.",
"fields": [
"return_url"
]
}
}{
"error": {
"code": "payment_required",
"message": "This service is paid. Open a checkout with POST /partner/v1/checkouts.",
"next": "POST /partner/v1/checkouts"
}
}Buyers pay on a Talkspresso payment page
Look up bookings
GET /bookings/:id returns one booking with its join link. GET /bookings lists your bookings, without join links. Filter with partner_ref, status (active, cancelled, awaiting_payment, completed), provider_id, from and to. Get more results with cursor and limit (1 to 100). GET /bookings/:id/intake returns the buyer’s answers to the coach’s questions.
curl -s "$API/bookings/BOOKING_ID" \
-H "Authorization: Bearer $TSP_KEY"Cancel and refund
Your server
POST /bookings/:id/cancel with a new Idempotency-Key and a reason_code. The reason_code is required.
Talkspresso
Cancels, refunds the buyer in full to the original payment method with no late fee, emails the buyer with your organization’s name and the reason in plain words, and tells the coach.
Your server
You get the cancelled booking back. booking.cancelled goes to your webhook with cancelled_by set to partner.
| reason_code | Use it when |
|---|---|
| buyer_requested | The buyer asked you to cancel. The buyer’s email says “at your request”. |
| partner_cause | A reason on your side. The email says “because of a reason on your side”. |
| duplicate | The session was booked twice. |
| fraud_suspected | You suspect a problem with the payment. |
curl -s -X POST "$API/bookings/BOOKING_ID/cancel" \
-H "Authorization: Bearer $TSP_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042-cancel" \
-d '{ "reason_code": "buyer_requested" }'{
"data": {
"id": "cmwecj3ax00dm1vjzdl3ceij7",
"status": "cancelled",
"approval_status": "auto_approved",
"start_time": "2026-11-17T19:00:00.000Z",
"end_time": "2026-11-17T19:30:00.000Z",
"duration_minutes": 30,
"provider": {
"id": "sn_andre",
"name": "Andre Williams"
},
"service": {
"id": "svc_sn_andre_williams_mentor",
"title": "1:1 Mentorship Session"
},
"buyer": {
"name": "Sam Rivera",
"email": "sam@example.com"
},
"partner_ref": "order-s5-1794536217",
"paid_cents": 0,
"currency": "USD",
"created_at": "2026-11-13T02:16:57.466Z",
"notes_available": false
}
}{
"error": {
"code": "validation_failed",
"message": "The request body is not valid.",
"fields": [
"reason_code"
]
}
}A coach must have agreed that you may cancel their sessions. If not, the call returns 409 coach_consent_required and nothing is refunded. The buyer can still cancel from the manage link Talkspresso emails them (in partner mode they get one only if Talkspresso sent a confirmation or they ask for a new link), or you can ask the coach.
Move a booking
Your server
GET /services/:id/availability for new times.
Your server
POST /bookings/:id/reschedule with a new Idempotency-Key and { "start_time": "..." }.
Talkspresso
Moves the booking if the coach allows it. A coach can limit how many times a buyer can move a booking and how close to the start time a move can happen. If the move has a reschedule fee, only the buyer can pay it, from the manage link Talkspresso emails them. Your call then gets 409 reschedule_fee_required.
Your server
You get the booking back with the new time. booking.rescheduled goes to your webhook.
curl -s -X POST "$API/bookings/BOOKING_ID/reschedule" \
-H "Authorization: Bearer $TSP_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042-move" \
-d '{ "start_time": "2026-11-17T19:00:00Z" }'{
"data": {
"id": "cmwecj3ax00dm1vjzdl3ceij7",
"status": "active",
"approval_status": "auto_approved",
"start_time": "2026-11-17T19:00:00.000Z",
"end_time": "2026-11-17T19:30:00.000Z",
"duration_minutes": 30,
"provider": {
"id": "sn_andre",
"name": "Andre Williams"
},
"service": {
"id": "svc_sn_andre_williams_mentor",
"title": "1:1 Mentorship Session"
},
"buyer": {
"name": "Sam Rivera",
"email": "sam@example.com"
},
"partner_ref": "order-s5-1794536217",
"paid_cents": 0,
"currency": "USD",
"created_at": "2026-11-13T02:16:57.466Z",
"notes_available": false
}
}Booking emails
Your organization has a setting called “Who sends booking emails”. An owner or admin changes it in Partner settings, or you email us. A key cannot change it. It has three values:
| Setting | What happens |
|---|---|
| talkspresso | Talkspresso sends every booking email, plain, with no organization name. |
| branded | Talkspresso sends every booking email. On bookings your platform creates, the emails carry your organization’s name and logo, and the sender reads “Your organization via Talkspresso” (the organization part is cut to 24 characters). Replies go to your organization’s support email when one is set. |
| partner | Your platform sends the buyer’s confirmation and reminders, and any thank-you it wants to send. Talkspresso sends only the emails listed under “Always sent by Talkspresso”. |
You can also ask for a value on one booking. Add notifications to POST /bookings or POST /checkouts, with talkspresso, branded or partner. If you leave it out, the organization’s setting applies. A request can ask for less than the setting, never more. The booking answer does not repeat the value.
curl -s -X POST "$API/bookings" \
-H "Authorization: Bearer $TSP_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1044-booking" \
-d '{
"provider_id": "sn_andre",
"service_id": "svc_sn_andre_williams_mentor",
"start_time": "2026-11-18T16:00:00Z",
"buyer": { "name": "Sam Rivera", "email": "sam@example.com" },
"partner_ref": "order-1044",
"notifications": "branded"
}'{
"error": {
"code": "notifications_mode_not_enabled",
"message": "Your organization is not set up for \"partner\" booking emails. Change \"Who sends booking emails\" in your organization's partner settings first.",
"fields": ["notifications"]
}
}{
"error": {
"code": "webhook_endpoint_required",
"message": "\"partner\" booking emails need an active webhook endpoint subscribed to booking.created, so your platform is told to send the buyer's email.",
"fields": ["notifications"]
}
}Any other value is refused with 400 validation_failed and fields set to ["notifications"].
What changes when your platform sends the emails:
- Partner mode needs a live webhook endpoint that receives booking.created. Without one, a request for partner is refused with 409 webhook_endpoint_required. If your endpoint is paused or removed later, Talkspresso goes back to sending the emails itself.
- Send the confirmation after you receive booking.created. “Delivered” means your endpoint answered with an HTTP 2xx. It does not mean your mailer sent anything.
- Talkspresso sends no reminders for these bookings, and there is no reminder event. Use the start_time in booking.created to schedule your own.
- The coach’s calendar invitation is not emailed to the buyer in partner mode either, while your endpoint is live.
- If booking.created is not delivered and answered with 2xx within 30 minutes (the check runs every 5 minutes, so allow up to 35), Talkspresso sends the buyer’s confirmation itself. If your endpoint answers late, the buyer can get both emails. The buyer always gets one confirmation.
- A confirmation is never held back when the session starts within 60 minutes. Talkspresso sends it at once. After Talkspresso has sent a booking’s confirmation in this way, it sends that booking’s reminders too.
- If your organization is set back to talkspresso or branded, Talkspresso sends the emails for every booking from then on, including bookings made earlier.
Always sent by Talkspresso
In every mode, Talkspresso never holds back an email that carries money, a cancellation, a link the buyer asked for or a notice about recording. In partner mode these emails also read “Your organization via Talkspresso”, with your organization’s name and logo, so the buyer knows who they are from.
| What it is | |
|---|---|
| Cancellations | The buyer is told when a booking is cancelled, by anyone. A cancel you make through the API sends one notice with your organization’s name, who cancelled, the reason in plain words and the refund. |
| Refunds | Any email about money going back to the buyer. |
| Moves and fees | A booking moved to a new time, and any reschedule fee charged, kept or returned. |
| Booking approval | A coach approved or declined a booking request. A decline carries the refund. |
| Links the buyer asked for | A new link to change or cancel, and a resent invitation with the join link. |
| Recording and notes notices | A notice that a session was recorded, or that notes are ready. |
| Messages from the coach | A coach’s own message to the buyer. |
| Emails to coaches and organization admins | Every email to a coach or an admin comes from Talkspresso in every mode. |
Talkspresso does not send thank-you, review request or book-again emails to buyers of bookings your platform created, in any mode. The coach can still write to the buyer, and those messages come from Talkspresso.
Coach consent
- A coach agrees to be listed on your platform and agrees that you may cancel or move the sessions your members book. A cancel through the API refunds the buyer in full with no late or reschedule fee, even if the coach’s own policy would keep one. Talkspresso emails the coach when you cancel, and shows each move in the coach’s Talkspresso notifications.
- If a coach’s consent is not in place when you cancel or move a session, the call is refused with 409 coach_consent_required, including for sessions already booked. Tell the buyer to contact the coach, or ask us for a new manage link for the buyer.
- Talkspresso can raise the consent text. A coach who has not agreed to the new text leaves GET /providers until they do, and is asked on their next sign-in. A coach on the older text stays listed until we raise the requirement.
- If your organization stops being ready (an agreement or fee needs renewing, for example), creating bookings and checkouts and moving bookings are refused with 403 org_not_ready. Reading bookings, cancelling and refunding, and replaying events keep working, so you can finish what is already in progress. Room links stop too, and rooms already shown in your page stop loading.
Show the room in your page
The room can open inside your own page, so the buyer never leaves it. What the buyer sees depends on the browser:
| Browser | What happens |
|---|---|
| Chrome and Edge | The buyer joins inside your page. |
| Safari and Firefox, including every browser on iPhone | The waiting room opens in its own window. Your frame shows “Open the call in its own window to join”, with no Join button, and the buyer joins there. Plan for this. |
Your server
GET /bookings/:id/room-links when the buyer is about to join. You get url, embed_url and parent_origins. The link is for the buyer only.
Your page
Put embed_url in an iframe on a website we registered for you, with allow="camera; microphone; autoplay; fullscreen". Also offer “Open in a new tab” with url.
Talkspresso
Shows the waiting room inside your page. The buyer waits there until the coach lets them in. The notices that a session is recorded or that AI notes are on appear inside your frame.
Your page
Listen for messages from the room (joined, left, ended) and update your screens.
curl -s "$API/bookings/BOOKING_ID/room-links" \
-H "Authorization: Bearer $TSP_KEY"{
"data": {
"booking_id": "cmwecj3ax00dm1vjzdl3ceij7",
"role": "buyer",
"url": "https://staging.talkspresso.com/meet/SESSION_ID?cid=BUYER_ID&ct=BUYER_TOKEN",
"embed_url": "https://staging.talkspresso.com/meet/SESSION_ID?cid=BUYER_ID&ct=BUYER_TOKEN&embed=1",
"parent_origins": [
"https://staging.talkspresso.com",
"https://partner-e2e.example.com"
],
"session_ends_at": "2026-11-17T16:30:00.000Z"
}
}parent_origins are the websites we registered to show this room. The room refuses to load inside any other page. session_ends_at is when the booked time ends.
<iframe
id="room"
src="EMBED_URL_FROM_YOUR_SERVER"
allow="camera; microphone; autoplay; fullscreen"
style="width:100%;height:640px;border:0"
></iframe>
<script>
// Messages come only from the room's own address, which is the host of the url we gave you.
const roomOrigin = new URL("EMBED_URL_FROM_YOUR_SERVER").origin;
window.addEventListener("message", (event) => {
if (event.origin !== roomOrigin) return;
const m = event.data;
if (!m || m.type !== "talkspresso-room-event") return;
if (m.event === "joined") console.log("buyer is in the room");
if (m.event === "left" || m.event === "ended") console.log("call over", m.session_id);
});
</script>What the room sends to your page, and only to a website on that list:
{ "type": "talkspresso-room-event", "version": 1, "event": "joined", "session_id": "..." }joined: the buyer is in the call.left: the buyer left.ended: the call is over.
The room only sends messages to your page. It does not receive any from your page, so your page cannot control the call. The buyer taps to join, so the browser can ask for permission. If there is a camera or microphone problem, the room shows a plain instruction.
Things to plan for:
- Talkspresso has to switch this on for your organization (the video setting is “inside your own page”) and register your website. Until then, GET /bookings/:id/room-links answers 409 video_mode_not_enabled. A change we make to your website list or this setting can take up to about 90 seconds to reach rooms.
- Use allow="camera; microphone; autoplay; fullscreen" on the iframe. Without it the browser blocks the camera and microphone and the buyer sees a plain instruction.
- The room shows the buyer the notices that the session is recorded or that AI notes are on, and offers Join only after the buyer has seen them in full. If something on your page covers the frame, even something see-through that fills the screen (a cookie banner layer or a chat widget container), Join is withheld and the buyer is offered the own-window path. The notices must also sit fully inside the visible part of your page, so the buyer may have to scroll your page until the whole frame is in view.
- A buyer can join only near the session time. The buyer’s call link stops working two hours after the session ends (this holds for every buyer call link, not only these), and room links stop 15 minutes after it ends. Until then, whoever holds the link can join as the buyer, so keep it on your server and give it only to that buyer.
- Make the frame at least 300 pixels wide and 380 pixels tall. In a smaller frame, the buyer is offered a button to open the call in a new window.
- A buyer’s link is tied to one browser at a time. If it is opened in a second browser while the first is in the room, the room asks for a name and asks the coach to let that person in.
- The coach hosts from Talkspresso. Host controls are not available inside your page. Only adults are supported, so a booking for a minor is never shown in a frame.
| Answer | Meaning |
|---|---|
| 403 org_not_ready | Your organization is not ready to use the partner API. Email support@talkspresso.com. |
| 409 video_mode_not_enabled | Showing the room inside your page is not switched on for your organization. Email support@talkspresso.com. |
| 409 session_over | The session ended more than 15 minutes ago, so there is no room link. |
| 409 no_allowed_origins | We have not registered the website that will show the room. Ask us to add it. |
| 409 booking_cancelled | The booking is cancelled. |
| 409 not_a_video_booking | The session is in person and has no video room. |
| 409 room_unavailable | The booking has no room link yet. |
| 429 room_link_rate_limited | More than 60 room links in an hour. |
4. Webhooks
A webhook is a message we send to a web address on your server when something happens, so you do not have to keep asking. Talkspresso registers your address and gives you a signing secret for it. A key cannot create or change endpoints. This is on purpose. Send us the https address (port 443, a public host). We set it up and send it a ping.
Events
| type | When it is sent |
|---|---|
| booking.created | A booking from your platform was created (free, or paid and confirmed). |
| booking.paid | Payment for the booking was collected. |
| booking.rescheduled | The start time changed, by you, the buyer or the coach. |
| booking.cancelled | The booking was cancelled. cancelled_by says buyer, coach, partner or system. |
| booking.completed | The session took place and finished. |
| checkout.paid | The buyer paid a checkout you opened. |
| checkout.expired | A checkout ended unpaid after 30 minutes. |
| ping | A test event we send when your endpoint is set up. It carries no buyer data. |
What a delivery looks like
Each delivery is a POST with a JSON body. The outer wrapper is small and has a version number. The booking is inside data.object. The body never contains a join link, notes, intake answers or fee detail. If you need the join link, fetch the booking with GET /bookings/:id.
{
"id": "evt_c4c240afd8fb3bf3119275fe",
"data": {
"object": {
"id": "cmwecn7jw00hw1vjz0y1ncjo4",
"buyer": {
"name": "Sam Rivera",
"email": "sam@example.com"
},
"object": "booking",
"status": "active",
"service": {
"id": "svc_sn_andre_williams_mentor",
"title": "1:1 Mentorship Session"
},
"currency": "USD",
"end_time": "2026-11-17T18:00:00.000Z",
"provider": {
"id": "sn_andre",
"name": "Andre Williams"
},
"paid_cents": 0,
"start_time": "2026-11-17T17:30:00.000Z",
"updated_at": "2026-11-13T02:20:09.596Z",
"partner_ref": "s5-wh-1794536409",
"cancelled_by": null,
"approval_status": "auto_approved",
"session_outcome": null,
"duration_minutes": 30
}
},
"type": "booking.created",
"created": "2026-11-13T02:20:09.633Z",
"api_version": "2026-11-12",
"organization_id": "org_startupnash_2024"
}{
"id": "evt_17c41458ff5377dacb31814d",
"data": {
"object": {
"id": "cmwecn7jw00hw1vjz0y1ncjo4",
"buyer": {
"name": "Sam Rivera",
"email": "sam@example.com"
},
"object": "booking",
"status": "cancelled",
"service": {
"id": "svc_sn_andre_williams_mentor",
"title": "1:1 Mentorship Session"
},
"currency": "USD",
"end_time": "2026-11-17T18:00:00.000Z",
"provider": {
"id": "sn_andre",
"name": "Andre Williams"
},
"paid_cents": 0,
"start_time": "2026-11-17T17:30:00.000Z",
"updated_at": "2026-11-13T02:21:02.114Z",
"partner_ref": "s5-wh-1794536409",
"cancelled_by": "partner",
"approval_status": "auto_approved",
"session_outcome": null,
"duration_minutes": 30
}
},
"type": "booking.cancelled",
"created": "2026-11-13T02:21:02.168Z",
"api_version": "2026-11-12",
"organization_id": "org_startupnash_2024"
}| Field | What it is |
|---|---|
| id, object | The booking id, and the value booking. |
| status, approval_status | active or cancelled (also awaiting_payment and completed when you read a booking), and the approval state. |
| start_time, end_time, duration_minutes | UTC times and length. |
| provider.id, provider.name | The coach. |
| service.id, service.title | The session. |
| buyer.name, buyer.email | The buyer you sent us. Only for bookings your platform created. |
| partner_ref | Your reference, or null. |
| paid_cents, currency | What the buyer paid, in cents, and USD. |
| session_outcome, cancelled_by | How the session ended, and who cancelled it, when they apply. |
| updated_at | When the booking last changed. Use it to ignore an older event that arrives late. |
A checkout event has object set to checkout, and carries id, status, partner_ref, booking_id, provider_id, service_id, start_time, expires_at and updated_at.
| Header | Value |
|---|---|
| Talkspresso-Signature | t=<unix seconds>,v1=<hex signature>. Two v1 values appear while we rotate your secret. |
| Talkspresso-Event-Id | The event id, the same on every delivery and replay of that event. |
| Talkspresso-Event-Type | For example booking.created. |
| Talkspresso-Delivery-Id | The id of this delivery. |
| Talkspresso-Delivery-Attempt | 1 for the first try, 2 for the next, and so on. |
Check the signature
Anyone can send a POST to your address. The signature proves a delivery is from us and was not changed. Do these in order:
- Read the raw body bytes exactly as they arrive, before any JSON parsing. Re-serialized JSON will not match.
- Read t and every v1 from the Talkspresso-Signature header.
- Reject the delivery if t is more than 5 minutes from your clock.
- Compute an HMAC-SHA256 of t, a dot and the raw body, using your signing secret. Write the result in lowercase hex.
- Compare it with each v1 in constant time. If any one matches, the delivery is real.
- Remember the event id and ignore an id you already handled: the same event can arrive more than once.
- Answer with any 2xx within 10 seconds. Do the slow work after you answer.
// Talkspresso webhook receiver: checks the signature, then the event.
// Run: TALKSPRESSO_WEBHOOK_SECRET=... node receiver.mjs
import crypto from "node:crypto";
import http from "node:http";
const SECRET = process.env.TALKSPRESSO_WEBHOOK_SECRET; // one secret per endpoint, from Talkspresso
const TOLERANCE_SECONDS = 300;
const seen = new Set(); // use your database in production
// header looks like: t=1794536400,v1=<hex>[,v1=<hex>] (two v1 values while a secret is being rotated)
const verify = (header, rawBody, secret) => {
if (!header) return false;
let t = null;
const sigs = [];
for (const part of header.split(",")) {
const i = part.indexOf("=");
if (i < 0) return false;
const key = part.slice(0, i).trim();
const value = part.slice(i + 1).trim();
if (key === "t") t = Number(value);
if (key === "v1") sigs.push(value);
}
if (!Number.isFinite(t) || sigs.length === 0) return false;
if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false; // too old or too new
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`, "utf8").digest();
return sigs.some((s) => /^[0-9a-f]{64}$/.test(s) && crypto.timingSafeEqual(Buffer.from(s, "hex"), expected));
};
http
.createServer((req, res) => {
const chunks = [];
req.on("data", (c) => chunks.push(c));
req.on("end", () => {
const rawBody = Buffer.concat(chunks).toString("utf8"); // the exact bytes, before any JSON parsing
if (!verify(req.headers["talkspresso-signature"], rawBody, SECRET)) {
res.writeHead(400).end("bad signature");
return;
}
const event = JSON.parse(rawBody);
if (!seen.has(event.id)) {
seen.add(event.id);
console.log(event.type, event.data.object.id, event.data.object.status ?? "");
}
res.writeHead(200).end("ok"); // any 2xx means delivered
});
})
.listen(Number(process.env.PORT || 8787));
# Check one Talkspresso webhook delivery. Pass the exact raw request body, not re-serialized JSON.
import hashlib
import hmac
import time
TOLERANCE_SECONDS = 300
def verify(signature_header: str, raw_body: bytes, secret: str) -> bool:
if not signature_header:
return False
timestamp = None
signatures = []
for part in signature_header.split(","):
key, _, value = part.strip().partition("=")
if key == "t" and value.isascii() and value.isdigit():
timestamp = int(value)
elif key == "v1":
signatures.append(value)
if timestamp is None or not signatures:
return False
if abs(time.time() - timestamp) > TOLERANCE_SECONDS:
return False
signed = str(timestamp).encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected.encode(), s.encode()) for s in signatures)
We sent a real ping and a real booking.created from staging to the Node receiver above, and the receiver accepted both.
Retries, replay and catching up
| Topic | What happens |
|---|---|
| Success | Any 2xx answer within 10 seconds. |
| Retries | If you answer with anything other than 2xx, or you time out, we try again after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours: that is seven retries, eight tries in all, over about 44 hours. Every retry sends the same body, unchanged, with a new signature. |
| After the last try | The delivery is marked abandoned. The event is still in GET /events and you can ask us to send it again. |
| Answer 410 Gone | We turn the endpoint off right away. Answer 410 only when you have stopped using that address. |
| A day of failures | If every delivery fails for 24 hours, we pause the endpoint and hold your events. Nothing is dropped. We contact you. When your receiver is working again, we resume and send the held events. |
| Order | Events can arrive out of order and more than once. Use the event id to ignore repeats and updated_at to ignore an older state. |
| Replay | POST /events/:id/replay sends one event again with the stored payload, up to 5 times an hour. Deliveries and their results are in GET /webhook-deliveries. |
| Catching up | GET /events?since=<last event id>&type=...&limit=... returns events oldest first. It is the same data as the webhooks. An event shows up here about 30 seconds after it happens. |
| How long we keep buyer details in events | We remove the buyer’s name and email from an event after 90 days. The id, type and time stay. |
curl -s "$API/events?limit=50" \
-H "Authorization: Bearer $TSP_KEY"
# next page: pass the last event id back
curl -s "$API/events?since=evt_8c8a5fba318e1a4daa0570b3&limit=50" \
-H "Authorization: Bearer $TSP_KEY"{
"data": [
"...events in order, oldest first..."
],
"has_more": true,
"next_cursor": "evt_992beaad97620c2cb562b057"
}curl -s -X POST "$API/events/evt_8c8a5fba318e1a4daa0570b3/replay" \
-H "Authorization: Bearer $TSP_KEY"{
"data": {
"event_id": "evt_92175b0a3de3dc94bc49d768",
"queued_deliveries": 0
}
}curl -s "$API/webhook-endpoints" -H "Authorization: Bearer $TSP_KEY"
curl -s "$API/webhook-deliveries?limit=20" -H "Authorization: Bearer $TSP_KEY"{
"data": [
{
"id": "whe_ba03b4efeda4fa78c4a5d910",
"url": "https://hooks.example.com/talkspresso",
"events": [],
"status": "active",
"disabled_reason": null,
"consecutive_failures": 0,
"last_success_at": null,
"created_at": "2026-11-13T02:22:55.776Z"
}
]
}{
"data": [
{
"id": "whd_fd4816cfe0914e269290cdd039312342",
"event_id": "evt_e910558c2b61b3cc03d4c1a0",
"endpoint_id": "whe_ba03b4efeda4fa78c4a5d910",
"status": "delivered",
"attempts": 1,
"next_attempt_at": null,
"last_status_code": 200,
"last_error_class": null,
"duration_ms": 140,
"delivered_at": "2026-11-13T02:23:05.071Z",
"replays": 0,
"created_at": "2026-11-13T02:23:05.071Z"
}
]
}5. Settings you control and settings Talkspresso sets
Your organization has a Partner settings screen for owners and admins. It shows each setting, who can change it, when it last changed and your go-live checklist. Your developers can read the same data with GET /settings.
You control
| Setting | Your options |
|---|---|
| Where your members book | Only inside your own platform, or also on a Talkspresso page for your organization. |
| Who sends booking emails | Talkspresso plain, Talkspresso with your name and logo, or your platform. See “Booking emails”. |
| Who can join your roster | Only people you invite, or people can apply and you approve each one. |
| Your name and look | Organization name, logo, colors, tagline and contact and support email. |
| Your coaches | Which coaches you invite, and each coach’s title on your platform. |
| What your server does with an event | Everything your server does after we deliver an event. |
Talkspresso sets, after your agreement
These can affect money, buyer data and compliance, so we set them with you. Email support@talkspresso.com to ask for a change. Every change is recorded with who made it and why.
| Setting | Today |
|---|---|
| How buyers pay | Each coach is paid directly through the coach’s own PayPal account. |
| Who the buyer is | A guest. You send a name and an email. No Talkspresso account is needed. |
| Which bookings you receive | The bookings your platform created, and no others. |
| Who may attend | Adults only. Health records (HIPAA) mode is off for partner platforms. |
| Where the video call opens | On a Talkspresso page, or inside your own page. Talkspresso sets this, and registers the website that will show the room. |
| Talkspresso’s fee | Set in your agreement for bookings from your platform, as a percentage. It cannot be set above 30 percent. |
| Which coaches may be listed | A coach is listed only after agreeing to the current consent text (see “Coach consent”). |
| Registered addresses | Where buyers return after paying, which websites may show the video room, and where webhooks go. |
| Keys and secrets | We create, rotate and revoke API keys and webhook secrets. Nobody with a key can change them. |
| Going live | We decide when the checklist is complete and turn on production. |
6. What we never share and never allow
What we never share
| Never shared | Details |
|---|---|
| Coach or host links | No API response, webhook or room link contains a link that lets someone host or control a call. You only ever receive the buyer’s link. |
| Raw video tokens | Video session tokens never appear in any response. You do not need them: the room link is enough. |
| What was said in a session | No recordings, transcripts, AI notes or summaries. Webhooks and API answers do not contain them. |
| Coach contact details | A coach’s email, phone and payout details are not shared. |
| Payout and fee detail | You see what the buyer paid. You do not see what the coach earned or what the fee was. |
| Other people’s bookings | You see only bookings your platform created. You cannot read a booking a buyer made on a coach’s own Talkspresso page, even if the coach is on your roster. |
| More about the buyer than you sent | Webhooks and bookings carry the buyer’s name and email. Intake answers are available only through an API call with your key, for your own bookings. They are never in a webhook. |
What we never allow, or do not support yet
| Not allowed | Today |
|---|---|
| Single sign-on | Not available yet. Buyers are guests identified by the name and email you send. |
| Under 18 | Adults only. Minors are not supported. |
| Buyers outside the United States | US buyers only for now. The pages and emails are in English and prices are in US dollars. |
| Health records | Coaches in health records (HIPAA) mode do not appear, and partner platforms cannot use that mode. |
| Changing money or settings with a key | A key cannot change settings, fees or webhook endpoints, and cannot create or revoke keys. |
| Hiding Talkspresso on the payment page | The payment page keeps the coach, the price, the cancellation rule and the line that Talkspresso collects the payment through PayPal. You cannot hide them. |
| Hiding recording and AI notes notices | The room tells buyers when a session is recorded or AI notes are on. A frame cannot hide that. |
| Host controls inside your page | The coach hosts from Talkspresso. A host who opens the room inside a frame is offered a button to open it in a new window. |
Your part
Your agreement sets out your obligations. In short:
- Keep your key on your server and out of your pages, your mobile apps and your logs. Tell us at once if it leaks and we will revoke it.
- Use the API for your own platform and your own members only. Do not reuse the coach list for anything else, and do not present yourself as Talkspresso.
- Cancel a booking only for a reason in the reason code list, and send the matching code. A cancel through the API refunds the buyer in full with no late fee. Do not use it to avoid a fee the buyer’s own cancellation would have caused. Keep a record of the buyer’s request.
- Do not promise buyers a different refund or cancellation rule than the one they accepted when they paid.
- Offer adults and US buyers only.
7. Sandbox
The sandbox is our staging system with a staging copy of your organization. Build here first.
| Topic | Detail |
|---|---|
| How to get one | Email support@talkspresso.com. We create a sandbox key for your staging organization and give it to you by a method other than email. |
| How long it lasts | 14 days. Ask for a new one when it ends. |
| Where it works | Only on https://api-staging.talkspresso.com/partner/v1. On production it is refused with 401. |
| What it is not | It is not a production key. Having one does not mean you are ready to go live. |
| Real people | Use made-up buyers with email addresses you control. Staging is a test system. |
| Limits | The same limits as a new production key (see “Limits” in section 3). |
What you can book against
| Test item | Detail |
|---|---|
| Organization | A staging copy of your organization. In this guide the examples use our test organization, StartupNash (id org_startupnash_2024). |
| Free sessions | Every test coach offers a free 30 minute “1:1 Mentorship Session”. Example: coach sn_andre, service svc_sn_andre_williams_mentor. GET /providers lists all of them. |
| A paid session | One test coach offers a paid session you can open a checkout for, and pay with PayPal sandbox. In our run: coach cmlrh3wdm002l1qhg56q14ydj, service svc-ppcp-walkthrough-50, $50. Ask us if your staging organization does not have one yet. |
| Paying | Use a PayPal sandbox buyer login. Ask us for one. No real money moves. |
| Return address | In our run the registered return host is partner-e2e.example.com. Yours is whatever you gave us. |
| Ids in this guide | They come from our own test run. Use the ids your own calls return. |
Staging and production use different base addresses: https://api-staging.talkspresso.com/partner/v1 and https://api.talkspresso.com/partner/v1. When you switch to production, change only the base address. We register your payment return address and webhook address separately for staging and for production.
8. Go-live checklist
This is what we check before we create your production key. You can see each item and its status in your Partner settings screen, or from GET /settings. An item says who has to act and what is still blocking it.
Yours
You can do these from your organization settings, or ask us.
| Item | What we check |
|---|---|
| At least one coach listed | At least one coach is on your roster and can be booked. |
| Seller name and contact email | Buyers can see your organization’s name and a contact email. |
| Roster is not open to anyone | People either apply and you approve each one, or you invite them. |
| Public page matches your setup | If your members book inside your own platform, your public Talkspresso page is switched off. |
Talkspresso
We do these. You can see each one in your settings.
| Item | What we check |
|---|---|
| Agreement signed | The agreement schedule is signed and matches your settings. |
| Adults only confirmation | We have your written confirmation that your members and buyers are adults. |
| Organization active | Your organization is active and is not in health records (HIPAA) mode. |
| Dedicated key owner | Your key belongs to an account that manages only your organization. |
| Return address registered | The web address buyers return to after paying is on file. |
| Allowed websites | Only if you show the video room inside your page: the website that will show it is on file. |
| Privacy Policy and Terms updated | Our public Privacy Policy and Terms mention partner platforms. |
| How buyers pay is ready | Each coach can be paid directly through PayPal. |
| Settings chosen on purpose | Each of these was set on purpose: how buyers pay, who buys, which bookings you receive, who may attend and where the call opens. |
| Fee on bookings set | Talkspresso’s fee on bookings from your platform is set in your agreement. |
| Fee applied to every booking | That fee is applied to every booking from your platform. |
| Coaches agreed to be listed | Every listed coach has agreed to be listed on your platform. |
| Booking emails | Who sends the buyer’s booking emails is chosen. If your platform sends them, you also have a webhook endpoint that receives booking.created. |
| Test payment run | A paid test booking and refund with your final settings and fee is on file. |
| Key expiry | Your key has not expired and does not expire soon. |
Your developers
You do this one.
| Item | What we check |
|---|---|
| Webhook endpoint | You gave us a web address that receives webhooks. If you would rather ask us for events yourself, use GET /events instead. |
Production keys last a year. You can hold two at once, so you can switch to the new one without a gap. We create the replacement before the old one ends and we revoke the old key when you tell us the new one works. If a key leaks, tell us and we revoke it at once.
Questions, a sandbox key or a change to a setting? Email support@talkspresso.com.
9. Developer kit
Everything a developer needs to start without reading this whole guide first: a machine-readable description of the API, a Postman collection, and a small working sample website.
| Part | What it is and where to get it |
|---|---|
| API description (OpenAPI 3.1) | Every call in this guide, with its fields, answers and error codes. The live copy needs no key. The saved copy is committed with our website and updated with each release. Load either one into a tool that reads OpenAPI to make a client library or a test page. |
| Postman collection | The same calls, ready to send. Download it, import it into Postman, and fill in the partner_api_key variable with your sandbox key. The base address is already the staging address, and every create call sends its own Idempotency-Key. |
| Sample teacher platform | A small working website that lists coaches, shows their times, books a session, sends the buyer to our hosted payment page, receives webhooks and shows the video room. It is being published as open source. Email support@talkspresso.com and we send you a copy today. |
| Sandbox key | Email support@talkspresso.com. Staging only, lasts 14 days, and we give it to you by a method other than email. See section 7. |
- Live API description:
https://api-staging.talkspresso.com/partner/v1/openapi.json - Saved copy:
https://talkspresso.com/partner/openapi.json - Postman collection:
https://talkspresso.com/partner/talkspresso-partner.postman_collection.json
Open the live API description, open the saved copy, or download the Postman collection.
Good to know
- The description follows the code. If this guide and the description ever differ, the description on staging is right, and we want to hear about it.
- The saved copy can be a few days behind the live copy on staging. Use the live copy when you need the newest fields.
- On the production address the live copy answers once the partner API is turned on for production. Until then, use the staging address.