Developers · API v1

Your calls, in your own software.

The Riff API gives your software the calls Riff answers for your business: who called, their number, what they needed, how the call ended, the booked job, every event and the transcript. It is read-only and answers JSON.

View as Markdown

Quickstart

You need a key and one command. Your first request takes about a minute.

  1. Make a key. Sign in on whappai.com, open the gear in the header, open the API tab and choose Create key. Copy the key.
  2. On the web page, paste the key in the box below. Every example on the page then holds your key. This step is optional.
  3. Run the command. You should see your two newest calls, newest first. If your business has no calls yet, you see {"data": [], "next_cursor": null}: make a demo call on whappai.com, claim it in the gear, and run the command again.
curl -H "Authorization: Bearer rk_live_YOUR_KEY" \
  "https://api.chattychapters.com/api/v1/calls?limit=2"

The answer is JSON. data holds the calls. next_cursor points to the next page. List calls shows every field.

Want every call in a spreadsheet? Build your call log in about 10 minutes.

Authentication

Send your key in the Authorization header on every request: Authorization: Bearer rk_live_YOUR_KEY. Keys start with rk_live_. Every example on this page uses rk_live_YOUR_KEY in its place.

The base URL is https://api.chattychapters.com/api/v1.

Never put the key in the URL. URLs end up in logs, so the API refuses a key in the URL with 400 key_in_url. If a key was ever in a link, treat it as leaked and revoke it.

Make a key

Only an admin of your organization can make keys.

  1. Sign in on whappai.com and open the gear in the header.
  2. Open the API tab and choose Create key.
  3. Name the key, like "Zapier", so you can tell your apps apart.
  4. Copy the key from the panel. You will not see it again. Riff keeps only a fingerprint of it, so we cannot show it to you later. If you lose it, revoke it and make a new one.

To stop an app, choose Revoke beside its key. The key stops working on the very next request.

Rotating a key

Rotation needs no special step. Do these three things in this order:

  1. Create a new key, with a name that says it is the new one.
  2. Switch your integration to the new key, and check that it works.
  3. Revoke the old key.

A revoke takes effect on the next request, so switch first. If you revoke first, your integration stops until you give it the new key.

Warning: Treat a key like a password. Keep it on your server, in an environment variable or in a file only you can read. Never put it in a web page you publish, an app you ship, a link or a chat. Every key sees all of your organization's calls.

Build your call log

Do you want every call in a spreadsheet that you keep? The Build your call log tutorial gives you one Python script. Run it with your key, and you get calls.csv and a transcript file for each call. Run it again, and only new and changed calls arrive. It takes about 10 minutes, and you need no other software.

Calls

A call is one ended call to your Riff line, or a demo call that you claimed in the gear. The API lists the same calls, in the same order, as the Calls tab in the gear. It never lists a call that is still in progress.

List calls

GET/calls

Your organization's ended calls, newest first, from the last 30 days. Filters combine (AND). To get every call, follow next_cursor: send it as cursor with the same filters until next_cursor is null. Add expand=transcript to get each call's transcript in the same answer (then at most 20 calls a page). A call still in progress is not listed.

Parameters

NameTypeRequiredDescription
limitintegernoHow many calls in one page: 1 to 100, default 50. With expand=transcript the most is 20. A larger number is treated as the most.
cursorstringnoThe next_cursor from the page before. It means "everything older than the last call of that page": send the same filters with it.
sincestring (date-time)noOnly calls that started at or after this time (inclusive). ISO 8601 with a UTC offset; a time with no offset is read as UTC.
untilstring (date-time)noOnly calls that started before this time (exclusive). ISO 8601 with a UTC offset; a time with no offset is read as UTC.
outcomestringnoOnly calls with this outcome. One of: booked, emergency, no_booking.
kindstringnoOnly calls to your line (line), or only demo calls you claimed (demo).
phonestringnoOnly calls from this caller. Any format with at least 10 digits: (512) 555-0147, 5125550147 and +15125550147 find the same calls (the last 10 digits are compared). Fewer than 10 digits is a 400 bad_request.
expandstringnotranscript adds each call's transcript to the answer, so you need no extra request per call.

Errors

curl -H "Authorization: Bearer rk_live_YOUR_KEY" \
  "https://api.chattychapters.com/api/v1/calls?limit=2"
Sample answerGET /calls?limit=2 — a first page with more to come
{
  "data": [
    {
      "id": "web-trial-862861dca69e41079dd1bcd6b864992d",
      "started_at": "2026-10-05T14:02:11.482913+00:00",
      "ended_at": "2026-10-05T14:05:23.907114+00:00",
      "duration_seconds": 192,
      "line": "+15126411499",
      "kind": "line",
      "caller": {
        "name": "Dana Ortiz",
        "phone": "+15125550147"
      },
      "need": "Kitchen sink — leaking under the cabinet",
      "outcome": "booked",
      "booking": {
        "start": "2026-10-07T09:00:00-05:00",
        "end": "2026-10-07T10:00:00-05:00"
      },
      "details": {
        "address": "603 W Cesar Chavez St, Austin, TX 78701",
        "name": "Dana Ortiz",
        "phone": "+15125550147",
        "problem": "Kitchen sink — leaking under the cabinet",
        "urgency": "this week"
      },
      "deleted_after": "2026-11-04T14:05:23.907114+00:00",
      "recording": null
    },
    {
      "id": "web-trial-8af9e2f2ea4f43c3adee12a5f86cc661",
      "started_at": "2026-10-05T11:40:02.118305+00:00",
      "ended_at": "2026-10-05T11:42:47.520991+00:00",
      "duration_seconds": 165,
      "line": "+15126411499",
      "kind": "demo",
      "caller": {
        "name": "Sam Patel",
        "phone": "+15125550183"
      },
      "need": "Water heater — leaking from the bottom",
      "outcome": "emergency",
      "booking": null,
      "details": {
        "address": "1100 Congress Ave, Austin, TX 78701",
        "name": "Sam Patel",
        "phone": "+15125550183",
        "problem": "Water heater — leaking from the bottom",
        "urgency": "emergency — water on the floor"
      },
      "deleted_after": "2026-11-04T11:42:47.520991+00:00",
      "recording": null
    }
  ],
  "next_cursor": "eyJpZCI6IndlYi10cmlhbC04YWY5ZTJmMmVhNGY0M2MzYWRlZTEyYTVmODZjYzY2MSIsImsiOiJjYWxscyIsInQiOiIyMDI2LTEwLTA1VDExOjQwOjAyLjExODMwNSswMDowMCJ9"
}

Get one call

GET/calls/{id}

One call, with the same fields as in the list. Add expand=transcript to get its transcript too. A call that is not yours, or older than 30 days, answers 404 not_found.

Parameters

NameTypeRequiredDescription
idstringyes (path)The call's id from GET /calls.
expandstringnotranscript adds each call's transcript to the answer, so you need no extra request per call.

Errors

curl -H "Authorization: Bearer rk_live_YOUR_KEY" \
  "https://api.chattychapters.com/api/v1/calls/web-trial-862861dca69e41079dd1bcd6b864992d"
Sample answerGET /calls/web-trial-862861dca69e41079dd1bcd6b864992d
{
  "id": "web-trial-862861dca69e41079dd1bcd6b864992d",
  "started_at": "2026-10-05T14:02:11.482913+00:00",
  "ended_at": "2026-10-05T14:05:23.907114+00:00",
  "duration_seconds": 192,
  "line": "+15126411499",
  "kind": "line",
  "caller": {
    "name": "Dana Ortiz",
    "phone": "+15125550147"
  },
  "need": "Kitchen sink — leaking under the cabinet",
  "outcome": "booked",
  "booking": {
    "start": "2026-10-07T09:00:00-05:00",
    "end": "2026-10-07T10:00:00-05:00"
  },
  "details": {
    "address": "603 W Cesar Chavez St, Austin, TX 78701",
    "name": "Dana Ortiz",
    "phone": "+15125550147",
    "problem": "Kitchen sink — leaking under the cabinet",
    "urgency": "this week"
  },
  "deleted_after": "2026-11-04T14:05:23.907114+00:00",
  "recording": null
}

What a call carries

FieldMeaning
idThe call's id. Web and demo calls look like web-trial- plus 32 hex digits. Treat it as an opaque string.
started_at, ended_at, duration_secondsWhen the call began and ended (UTC, ISO 8601), and how long it ran in whole seconds. started_at and duration_seconds can be null; ended_at is never null.
lineThe number the caller dialed (your Riff line), in E.164 form, like +15126411499. Can be null.
kindline for a call to your business line. demo for a demo call you claimed.
callername as the caller gave it, and phone as +1 and 10 digits. Each is null when Riff did not get it.
needWhat the caller needed, in a few words, like Water heater — not heating. null when Riff did not learn it.
outcomebooked when a job was booked. Else emergency when the caller said it was an emergency. Else no_booking. Never null.
bookingThe booked job's start and end (end can be null), or null when nothing was booked.
detailsEvery detail Riff captured, by name, like name, phone, address, urgency, problem. Read the names you know and keep the rest.
deleted_afterWhen Riff deletes the call. Can be null. See the 30-day rule.
recordingAlways null in this release.
transcriptOnly when you ask for expand=transcript. See Transcripts.

Filters

GET /calls takes five filters. They combine: a call must match all of them.

FilterOnly calls that…Example
sincestarted at or after this time2026-10-01T00:00:00+00:00
untilstarted before this time2026-10-06T00:00:00+00:00
outcomeended this way: booked, emergency or no_bookingbooked
kindcame to your line (line), or are demo calls you claimed (demo)line
phonecame from this caller. Any format with at least 10 digits works. The last 10 digits are compared.(512) 555-0147

since and until make the range [since, until): a call that started at since is in it, and a call that started at until is not. So one day ends exactly where the next day starts, and no call is counted twice. Write times in ISO 8601 with a UTC offset. A time with no offset is read as UTC.

A cursor means everything older than this call. It does not remember your filters. So send the same filters with the cursor on every page. If you change a filter, start again with no cursor.

curl -G -H "Authorization: Bearer rk_live_YOUR_KEY" \
  --data-urlencode "outcome=booked" \
  --data-urlencode "phone=(512) 555-0147" \
  https://api.chattychapters.com/api/v1/calls

A value that is not allowed answers 400 bad_request. The message names the parameter and the allowed values, like outcome must be one of: booked, emergency, no_booking. A phone with fewer than 10 digits answers phone must have at least 10 digits.

Transcripts

Get one call's transcript

GET/calls/{id}/transcript

What was said on one call, in order. speaker is agent (Riff) or caller. The answer can be {"turns": []} for a few seconds after call.ended: if it is empty, fetch it again. To get many transcripts in one request, use GET /calls?expand=transcript.

Parameters

NameTypeRequiredDescription
idstringyes (path)The call's id from GET /calls.

Errors

curl -H "Authorization: Bearer rk_live_YOUR_KEY" \
  "https://api.chattychapters.com/api/v1/calls/web-trial-862861dca69e41079dd1bcd6b864992d/transcript"
Sample answerGET /calls/web-trial-862861dca69e41079dd1bcd6b864992d/transcript
{
  "turns": [
    {
      "speaker": "agent",
      "text": "Thanks for calling Example Plumbing Business. This is Riff. How can I help?"
    },
    {
      "speaker": "caller",
      "text": "Hi, my kitchen sink is leaking under the cabinet."
    },
    {
      "speaker": "agent",
      "text": "I'm sorry to hear that. Can I get your first and last name?"
    },
    {
      "speaker": "caller",
      "text": "Dana Ortiz."
    },
    {
      "speaker": "agent",
      "text": "Thanks, Dana. What is the address for the job?"
    },
    {
      "speaker": "caller",
      "text": "603 West Cesar Chavez Street."
    },
    {
      "speaker": "agent",
      "text": "I have Wednesday, October 7th at 9 AM or 11 AM. Which works better?"
    },
    {
      "speaker": "caller",
      "text": "9 works."
    },
    {
      "speaker": "agent",
      "text": "You're booked for Wednesday, October 7th at 9 AM. We'll see you then."
    }
  ]
}

Many transcripts in one request

Add expand=transcript to GET /calls or GET /calls/{id}. Each call then carries its transcript, so you need no extra request per call. With expand=transcript, a page holds at most 20 calls: a limit above 20 is treated as 20. So 100 calls cost 5 requests, not 101.

A transcript can be {"turns": []} for a few seconds after call.ended. If it is empty, fetch it again.

Events

An event is one thing that happened on a call. There are five types:

TypeWhendata
call.startedThe call began.dialed: the number the caller dialed.
call.detail_capturedRiff captured one detail. There is one event per detail.field and value, like address.
call.appointment_bookedRiff booked a job on your calendar.booking and calendar.
call.endedThe call ended.turns: how many turns the transcript has.
call.claimedA demo call was claimed into your organization.Always {}.

Riff records details when the call ends. So call.detail_captured and call.appointment_booked carry the end time, the same time as call.ended.

Events keep raw values. A phone in call.detail_captured is as the caller gave it, like 5125550147. Calls carry normalized phones: caller.phone and details.phone are +1 and 10 digits, or null.

Stay up to date

To keep your own copy up to date, read GET /events and store its next_cursor. Next time, send it as after. You get only new events, and none twice.

Re-fetch a call on call.ended or call.claimed: fetch it with GET /calls/{id}. The feed trails real time by about 5 seconds, so a call can appear in the feed a few seconds after it ends.

New event types are announced in the Changelog first. Skip types you do not know.

List one call's events

GET/calls/{id}/events

What happened on one call, oldest first: call.started, one call.detail_captured per detail, call.appointment_booked when a job was booked, call.ended, and call.claimed when a demo call was claimed. Riff records details when the call ends, so call.detail_captured and call.appointment_booked carry the end time. Follow next_cursor as cursor until it is null.

Parameters

NameTypeRequiredDescription
idstringyes (path)The call's id from GET /calls.
limitintegernoHow many events in one page: 1 to 100, default 50. A larger number is treated as 100.
cursorstringnoThe next_cursor from the page before.

Errors

curl -H "Authorization: Bearer rk_live_YOUR_KEY" \
  "https://api.chattychapters.com/api/v1/calls/web-trial-862861dca69e41079dd1bcd6b864992d/events"
Sample answerGET /calls/web-trial-862861dca69e41079dd1bcd6b864992d/events
{
  "data": [
    {
      "id": "1041",
      "type": "call.started",
      "version": 1,
      "occurred_at": "2026-10-05T14:02:11.482913+00:00",
      "call_id": "web-trial-862861dca69e41079dd1bcd6b864992d",
      "data": {
        "dialed": "+15126411499"
      }
    },
    {
      "id": "1042",
      "type": "call.detail_captured",
      "version": 1,
      "occurred_at": "2026-10-05T14:05:23.907114+00:00",
      "call_id": "web-trial-862861dca69e41079dd1bcd6b864992d",
      "data": {
        "field": "address",
        "value": "603 W Cesar Chavez St, Austin, TX 78701"
      }
    },
    {
      "id": "1043",
      "type": "call.detail_captured",
      "version": 1,
      "occurred_at": "2026-10-05T14:05:23.907114+00:00",
      "call_id": "web-trial-862861dca69e41079dd1bcd6b864992d",
      "data": {
        "field": "name",
        "value": "Dana Ortiz"
      }
    },
    {
      "id": "1044",
      "type": "call.detail_captured",
      "version": 1,
      "occurred_at": "2026-10-05T14:05:23.907114+00:00",
      "call_id": "web-trial-862861dca69e41079dd1bcd6b864992d",
      "data": {
        "field": "phone",
        "value": "5125550147"
      }
    },
    {
      "id": "1045",
      "type": "call.detail_captured",
      "version": 1,
      "occurred_at": "2026-10-05T14:05:23.907114+00:00",
      "call_id": "web-trial-862861dca69e41079dd1bcd6b864992d",
      "data": {
        "field": "problem",
        "value": "Kitchen sink — leaking under the cabinet"
      }
    },
    {
      "id": "1046",
      "type": "call.detail_captured",
      "version": 1,
      "occurred_at": "2026-10-05T14:05:23.907114+00:00",
      "call_id": "web-trial-862861dca69e41079dd1bcd6b864992d",
      "data": {
        "field": "urgency",
        "value": "this week"
      }
    },
    {
      "id": "1047",
      "type": "call.appointment_booked",
      "version": 1,
      "occurred_at": "2026-10-05T14:05:23.907114+00:00",
      "call_id": "web-trial-862861dca69e41079dd1bcd6b864992d",
      "data": {
        "booking": {
          "event_id": "5k2v9q1d8h7m3n4p6r0s",
          "slot_id": "slot-0900",
          "offer_set_id": "ofs-7c41e2",
          "start": "2026-10-07T09:00:00-05:00",
          "end": "2026-10-07T10:00:00-05:00",
          "status": "confirmed"
        },
        "calendar": {
          "kind": "live",
          "label": null
        }
      }
    },
    {
      "id": "1048",
      "type": "call.ended",
      "version": 1,
      "occurred_at": "2026-10-05T14:05:23.907114+00:00",
      "call_id": "web-trial-862861dca69e41079dd1bcd6b864992d",
      "data": {
        "turns": 9
      }
    }
  ],
  "next_cursor": null
}

Follow the event feed

GET/events

Every event of every call in your organization, oldest first, to keep your own system up to date. Store the next_cursor you get and send it as after next time: you get only new events, none twice. When there is nothing new, data is empty and next_cursor is the cursor you sent. The feed trails real time by about 5 seconds. On call.ended or call.claimed, fetch the call with GET /calls/{id}. Event data keeps raw values (a phone is not normalized). The feed keeps events for 30 days, like the calls.

Parameters

NameTypeRequiredDescription
limitintegernoHow many events in one page: 1 to 100, default 50. A larger number is treated as 100.
afterstringnoThe next_cursor from your last /events answer. You get only events after it. Leave it out to start from the oldest event.

Errors

curl -H "Authorization: Bearer rk_live_YOUR_KEY" \
  "https://api.chattychapters.com/api/v1/events?after=eyJpZCI6MTA0NiwiayI6ImV2ZW50cyJ9"
Sample answerGET /events?after=eyJpZCI6MTA0NiwiayI6ImV2ZW50cyJ9
{
  "data": [
    {
      "id": "1047",
      "type": "call.appointment_booked",
      "version": 1,
      "occurred_at": "2026-10-05T14:05:23.907114+00:00",
      "call_id": "web-trial-862861dca69e41079dd1bcd6b864992d",
      "data": {
        "booking": {
          "event_id": "5k2v9q1d8h7m3n4p6r0s",
          "slot_id": "slot-0900",
          "offer_set_id": "ofs-7c41e2",
          "start": "2026-10-07T09:00:00-05:00",
          "end": "2026-10-07T10:00:00-05:00",
          "status": "confirmed"
        },
        "calendar": {
          "kind": "live",
          "label": null
        }
      }
    },
    {
      "id": "1048",
      "type": "call.ended",
      "version": 1,
      "occurred_at": "2026-10-05T14:05:23.907114+00:00",
      "call_id": "web-trial-862861dca69e41079dd1bcd6b864992d",
      "data": {
        "turns": 9
      }
    },
    {
      "id": "1049",
      "type": "call.claimed",
      "version": 1,
      "occurred_at": "2026-10-05T14:20:09.311458+00:00",
      "call_id": "web-trial-8af9e2f2ea4f43c3adee12a5f86cc661",
      "data": {}
    }
  ],
  "next_cursor": "eyJpZCI6MTA0OSwiayI6ImV2ZW50cyJ9"
}

Paging

A list answers {"data": [...], "next_cursor": ...}. limit sets the page size: 1 to 100, default 50. With expand=transcript the most is 20.

Two routes page with cursor. One route pages with after:

RouteOrderSend next_cursor back asYou have them all when
GET /callsnewest firstcursornext_cursor is null
GET /calls/{id}/eventsoldest firstcursornext_cursor is null
GET /eventsoldest firstafterdata is empty. next_cursor is then the cursor you sent: keep it for next time.

If you mix them up, the API answers 400 unknown_parameter and says which one to use:

  • GET /calls?after=… answers Unknown parameter 'after'. /calls pages with 'cursor'.
  • GET /events?cursor=… answers Unknown parameter 'cursor'. /events pages with 'after'.

Errors

Every error uses the HTTP status and this shape. Act on code. The message is for people and can change.

JSON
{"error": {"code": "invalid_key", "message": "The API key is missing or not valid. Send it as 'Authorization: Bearer <key>'."}}
StatusCodeMeaningExample message
400bad_requestA parameter value is not valid; the message names the parameter and the allowed values.outcome must be one of: booked, emergency, no_booking.
400unknown_parameterThe message is Unknown parameter '<name>'.; when you send after to /calls or /calls/{id}/events it adds /calls pages with 'cursor'., and when you send cursor to /events it adds /events pages with 'after'.Unknown parameter 'after'. /calls pages with 'cursor'.
400key_in_urlA key was sent in the URL.Send the key in the Authorization header, never in the URL (URLs are logged).
401invalid_keyThe Authorization header is missing, or the key is malformed, unknown or revoked. The answer is the same in every case.The API key is missing or not valid. Send it as 'Authorization: Bearer <key>'.
404not_foundNo such call, or the call is another organization's, or it is older than 30 days. The answer is the same in every case.No such resource.
405method_not_allowedThe API is read-only; use GET.This API is read-only: use GET.
429rate_limitedMore than 60 requests in a minute with this key, or more than 30 requests in a minute with invalid keys from your address. Wait Retry-After seconds.Rate limit exceeded: 60 requests per minute per key.
500internal_errorSomething failed on our side. Try again; if it keeps failing, send us the X-Request-Id.Something went wrong on our side.
503unavailableThe service is busy for a moment. Wait Retry-After seconds and try again.The service is busy. Try again in a few seconds.

Every answer carries an X-Request-Id header. Quote X-Request-Id when you ask for help: it lets us find your request, and you never need to send us your key.

A 403 with the text error code: 1010 does not come from the API. A network filter between you and the API refused your HTTP client's User-Agent. Set your own User-Agent, as the Python examples on this page do.

Limits and headers

  • Each key may make 60 requests a minute. Past that you get 429 rate_limited with a Retry-After header. Wait that many seconds, then try again.
  • Requests with invalid keys are limited by address: 30 requests in a minute.
  • On 503 unavailable, also wait Retry-After seconds.
  • A page holds at most 100 items, or 20 calls with expand=transcript.
  • Answers are never cached (Cache-Control: no-store). Ask again for fresh data.
  • The API is read-only. Any method but GET answers 405 method_not_allowed.
HeaderTypeSent withDescription
X-Request-Idstringevery answerA random id for this request. Quote it when you ask for help.
Cache-Controlstringevery answerAnswers are never cached. Ask again for fresh data.
X-RateLimit-Limitinteger200; 400, 404, 429, 503 when the key is validRequests allowed per minute for this key (60).
X-RateLimit-Remaininginteger200; 400, 404, 429, 503 when the key is validRequests left in the current minute for this key.
Allowstring405The only method the API accepts.
Retry-Afterinteger429, 503Seconds to wait before you try again.

The 30-day rule

Riff keeps a call, its events and its transcript for 30 days after the call ends, then deletes them. The API follows the same rule exactly: a call older than 30 days is not in the list, GET /calls/{id} answers 404 not_found, and its events and transcript are gone. Each call's deleted_after tells you the day.

If you need a call for longer, copy it into your own system before then. The call log script does that for you.

For AI agents

These docs are written for people and for AI agents. An agent does not need a web page:

Get this contract

GET/openapi.json

This OpenAPI 3.1 document, as JSON. It needs no key. Tools and AI agents can read it to learn every route, parameter, answer and error. The same file is at https://whappai.com/openapi.json.

This route takes no parameters.

Errors

curl "https://api.chattychapters.com/api/v1/openapi.json"
Sample answerGET /openapi.json (shortened)
{
  "openapi": "3.1.0",
  "info": {
    "title": "Riff API",
    "version": "1.1.0"
  },
  "servers": [
    {
      "url": "https://api.chattychapters.com/api/v1"
    }
  ],
  "paths": {
    "/calls": {
      "get": {
        "operationId": "listCalls",
        "summary": "List calls"
      }
    }
  }
}

A ready prompt

Before you start, put your key in the RIFF_API_KEY environment variable on the computer where the agent runs code. Then give your AI assistant this prompt. Change the last line to your task.

Text
Use the Riff API described at https://whappai.com/llms.txt.
My API key is in the RIFF_API_KEY environment variable. Read it from there,
and send it only in the Authorization header. Never ask me to paste the key
into this chat, never print it, and never write it into a file or into code
you show me. Send your own User-Agent header (not Python's default).
Task: list my booked calls from the last 7 days, with the caller's name,
phone number and booking time.

Warning: Never paste a key into a chat with an AI assistant. A chat is stored, so a key in a chat is a leaked key. If you did paste one, revoke it and make a new one.

Coming next

  • Recordings: coming in the next release. recording is null until then. The link will arrive in this field.
  • Webhooks: Riff will send your server each event as it happens, so you do not need to poll the feed.

Changelog

2026-10-07

  • GET /calls takes the filters until, outcome, kind and phone, next to since.
  • expand=transcript on GET /calls (at most 20 calls a page) and on GET /calls/{id}.
  • A new event type, call.claimed: a demo call was claimed into your organization.
  • Every answer carries X-Request-Id. Answers to a valid key carry X-RateLimit-Limit and X-RateLimit-Remaining.
  • Phones (caller.phone, details.phone) are +1 and 10 digits, or null.
  • outcome comes from the booking first, then the urgency.
  • An unknown parameter answers 400 unknown_parameter and names it.
  • GET /openapi.json serves the contract, with no key. These docs have Markdown copies and an llms.txt.

2026-10-06

  • API v1: GET /calls, GET /calls/{id}, GET /calls/{id}/events, GET /calls/{id}/transcript and GET /events. Admins make keys in the gear on whappai.com. Calls follow the 30-day rule.