{
  "openapi": "3.1.0",
  "info": {
    "title": "Riff API",
    "version": "1.1.0",
    "summary": "Read your Riff calls: who called, what they needed, what was booked, and what was said.",
    "description": "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 (GET only), answers JSON, and keeps each call for 30 days. The base URL is https://api.chattychapters.com/api/v1. Send your key on every request as `Authorization: Bearer rk_live_…` (never in the URL). An admin makes keys in the gear on whappai.com, API tab. Each key sees every call of its organization and may make 60 requests a minute.\n\nTo copy your calls, first page `GET /events` until `data` is empty and keep its `next_cursor`, so a call that ends meanwhile is not lost. Then page `GET /calls?expand=transcript&limit=20`, sending each `next_cursor` back as `cursor` until it is `null`. To stay up to date, send the saved cursor to `GET /events` as `after`, and fetch the call on `call.ended` or `call.claimed`. Errors answer `{\"error\": {\"code\": \"…\", \"message\": \"…\"}}`: act on `code`. On 429 or 503, wait `Retry-After` seconds. Quote the `X-Request-Id` header when you ask for help. Recordings come in the next release.",
    "contact": {
      "name": "Riff developers",
      "url": "https://whappai.com/developers.html"
    }
  },
  "servers": [
    {
      "url": "https://api.chattychapters.com/api/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Calls",
      "description": "Ended calls, their events and transcripts."
    },
    {
      "name": "Events",
      "description": "The event feed for keeping your system up to date."
    },
    {
      "name": "Meta",
      "description": "This contract."
    }
  ],
  "paths": {
    "/calls": {
      "get": {
        "tags": [
          "Calls"
        ],
        "operationId": "listCalls",
        "summary": "List calls",
        "description": "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": [
          {
            "$ref": "#/components/parameters/CallsLimit"
          },
          {
            "$ref": "#/components/parameters/CallsCursor"
          },
          {
            "$ref": "#/components/parameters/Since"
          },
          {
            "$ref": "#/components/parameters/Until"
          },
          {
            "$ref": "#/components/parameters/Outcome"
          },
          {
            "$ref": "#/components/parameters/Kind"
          },
          {
            "$ref": "#/components/parameters/Phone"
          },
          {
            "$ref": "#/components/parameters/Expand"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of calls.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CallList"
                },
                "examples": {
                  "page": {
                    "summary": "GET /calls?limit=2 — a first page with more to come",
                    "value": {
                      "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"
                    }
                  },
                  "last_page": {
                    "summary": "GET /calls?limit=2&cursor=eyJpZCI6IndlYi10cmlhbC04YWY5ZTJmMmVhNGY0M2MzYWRlZTEyYTVmODZjYzY2MSIsImsiOiJjYWxscyIsInQiOiIyMDI2LTEwLTA1VDExOjQwOjAyLjExODMwNSswMDowMCJ9 — the last page",
                    "value": {
                      "data": [
                        {
                          "id": "web-trial-8b7f33595b3542e09b0ed4ef3c28b868",
                          "started_at": "2026-10-04T22:15:40.031877+00:00",
                          "ended_at": "2026-10-04T22:16:28.644019+00:00",
                          "duration_seconds": 49,
                          "line": "+15126411499",
                          "kind": "line",
                          "caller": {
                            "name": null,
                            "phone": null
                          },
                          "need": null,
                          "outcome": "no_booking",
                          "booking": null,
                          "details": {},
                          "deleted_after": "2026-11-03T22:16:28.644019+00:00",
                          "recording": null
                        },
                        {
                          "id": "web-trial-cb7f71cefe9f4e3f80c7873d77c87589",
                          "started_at": "2026-10-04T18:03:12.774501+00:00",
                          "ended_at": "2026-10-04T18:04:51.090236+00:00",
                          "duration_seconds": 98,
                          "line": "+15126411499",
                          "kind": "line",
                          "caller": {
                            "name": null,
                            "phone": "+15125550129"
                          },
                          "need": "Toilet — running all night",
                          "outcome": "no_booking",
                          "booking": null,
                          "details": {
                            "phone": "+15125550129",
                            "problem": "Toilet — running all night"
                          },
                          "deleted_after": "2026-11-03T18:04:51.090236+00:00",
                          "recording": null
                        }
                      ],
                      "next_cursor": null
                    }
                  },
                  "filtered": {
                    "summary": "GET /calls?outcome=booked&phone=(512) 555-0147",
                    "value": {
                      "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
                        }
                      ],
                      "next_cursor": null
                    }
                  },
                  "with_transcript": {
                    "summary": "GET /calls?expand=transcript&limit=20",
                    "value": {
                      "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,
                          "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."
                              }
                            ]
                          }
                        },
                        {
                          "id": "web-trial-8b7f33595b3542e09b0ed4ef3c28b868",
                          "started_at": "2026-10-04T22:15:40.031877+00:00",
                          "ended_at": "2026-10-04T22:16:28.644019+00:00",
                          "duration_seconds": 49,
                          "line": "+15126411499",
                          "kind": "line",
                          "caller": {
                            "name": null,
                            "phone": null
                          },
                          "need": null,
                          "outcome": "no_booking",
                          "booking": null,
                          "details": {},
                          "deleted_after": "2026-11-03T22:16:28.644019+00:00",
                          "recording": null,
                          "transcript": {
                            "turns": []
                          }
                        }
                      ],
                      "next_cursor": null
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`bad_request`, `unknown_parameter` or `key_in_url`. A parameter value is not valid, a parameter is not one this route knows, or the key was sent in the URL.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit-IfKey"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining-IfKey"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "bad_outcome": {
                    "summary": "GET /calls?outcome=maybe",
                    "value": {
                      "error": {
                        "code": "bad_request",
                        "message": "outcome must be one of: booked, emergency, no_booking."
                      }
                    }
                  },
                  "bad_phone": {
                    "summary": "GET /calls?phone=web",
                    "value": {
                      "error": {
                        "code": "bad_request",
                        "message": "phone must have at least 10 digits."
                      }
                    }
                  },
                  "bad_cursor": {
                    "summary": "A cursor that is not valid",
                    "value": {
                      "error": {
                        "code": "bad_request",
                        "message": "The cursor is not valid."
                      }
                    }
                  },
                  "after_on_calls": {
                    "summary": "GET /calls?after=… (the /events parameter)",
                    "value": {
                      "error": {
                        "code": "unknown_parameter",
                        "message": "Unknown parameter 'after'. /calls pages with 'cursor'."
                      }
                    }
                  },
                  "unknown": {
                    "summary": "GET /calls?sort=asc",
                    "value": {
                      "error": {
                        "code": "unknown_parameter",
                        "message": "Unknown parameter 'sort'."
                      }
                    }
                  },
                  "key_in_url": {
                    "summary": "A key sent in the URL",
                    "value": {
                      "error": {
                        "code": "key_in_url",
                        "message": "Send the key in the Authorization header, never in the URL (URLs are logged)."
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/calls/{id}": {
      "get": {
        "tags": [
          "Calls"
        ],
        "operationId": "getCall",
        "summary": "Get one call",
        "description": "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": [
          {
            "$ref": "#/components/parameters/CallId"
          },
          {
            "$ref": "#/components/parameters/Expand"
          }
        ],
        "responses": {
          "200": {
            "description": "The call.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Call"
                },
                "examples": {
                  "call": {
                    "summary": "GET /calls/web-trial-862861dca69e41079dd1bcd6b864992d",
                    "value": {
                      "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
                    }
                  },
                  "with_transcript": {
                    "summary": "GET /calls/web-trial-862861dca69e41079dd1bcd6b864992d?expand=transcript",
                    "value": {
                      "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,
                      "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."
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`bad_request`, `unknown_parameter` or `key_in_url`. A parameter value is not valid, a parameter is not one this route knows, or the key was sent in the URL.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit-IfKey"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining-IfKey"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "bad_expand": {
                    "summary": "GET /calls/{id}?expand=events",
                    "value": {
                      "error": {
                        "code": "bad_request",
                        "message": "expand must be: transcript."
                      }
                    }
                  },
                  "unknown": {
                    "summary": "GET /calls/{id}?fields=need",
                    "value": {
                      "error": {
                        "code": "unknown_parameter",
                        "message": "Unknown parameter 'fields'."
                      }
                    }
                  },
                  "key_in_url": {
                    "summary": "A key sent in the URL",
                    "value": {
                      "error": {
                        "code": "key_in_url",
                        "message": "Send the key in the Authorization header, never in the URL (URLs are logged)."
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/calls/{id}/events": {
      "get": {
        "tags": [
          "Calls"
        ],
        "operationId": "listCallEvents",
        "summary": "List one call's events",
        "description": "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": [
          {
            "$ref": "#/components/parameters/CallId"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/CallEventsCursor"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of the call's events.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventList"
                },
                "examples": {
                  "all": {
                    "summary": "GET /calls/web-trial-862861dca69e41079dd1bcd6b864992d/events",
                    "value": {
                      "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
                    }
                  },
                  "page": {
                    "summary": "GET /calls/web-trial-862861dca69e41079dd1bcd6b864992d/events?limit=3",
                    "value": {
                      "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"
                          }
                        }
                      ],
                      "next_cursor": "eyJpZCI6MTA0MywiayI6ImNhbGxldmVudHMiLCJ0IjoiMjAyNi0xMC0wNVQxNDowNToyMy45MDcxMTQrMDA6MDAifQ"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`bad_request`, `unknown_parameter` or `key_in_url`. A parameter value is not valid, a parameter is not one this route knows, or the key was sent in the URL.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit-IfKey"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining-IfKey"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "bad_limit": {
                    "summary": "GET /calls/{id}/events?limit=0",
                    "value": {
                      "error": {
                        "code": "bad_request",
                        "message": "limit must be a whole number from 1 to 100."
                      }
                    }
                  },
                  "after_on_call_events": {
                    "summary": "GET /calls/{id}/events?after=…",
                    "value": {
                      "error": {
                        "code": "unknown_parameter",
                        "message": "Unknown parameter 'after'. /calls pages with 'cursor'."
                      }
                    }
                  },
                  "key_in_url": {
                    "summary": "A key sent in the URL",
                    "value": {
                      "error": {
                        "code": "key_in_url",
                        "message": "Send the key in the Authorization header, never in the URL (URLs are logged)."
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/calls/{id}/transcript": {
      "get": {
        "tags": [
          "Calls"
        ],
        "operationId": "getCallTranscript",
        "summary": "Get one call's transcript",
        "description": "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": [
          {
            "$ref": "#/components/parameters/CallId"
          }
        ],
        "responses": {
          "200": {
            "description": "The transcript.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Transcript"
                },
                "examples": {
                  "transcript": {
                    "summary": "GET /calls/web-trial-862861dca69e41079dd1bcd6b864992d/transcript",
                    "value": {
                      "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."
                        }
                      ]
                    }
                  },
                  "not_yet": {
                    "summary": "Right after call.ended, before the transcript is copied",
                    "value": {
                      "turns": []
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`unknown_parameter` or `key_in_url`. This route takes no query parameters.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit-IfKey"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining-IfKey"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "unknown": {
                    "summary": "GET /calls/{id}/transcript?format=text",
                    "value": {
                      "error": {
                        "code": "unknown_parameter",
                        "message": "Unknown parameter 'format'."
                      }
                    }
                  },
                  "key_in_url": {
                    "summary": "A key sent in the URL",
                    "value": {
                      "error": {
                        "code": "key_in_url",
                        "message": "Send the key in the Authorization header, never in the URL (URLs are logged)."
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/events": {
      "get": {
        "tags": [
          "Events"
        ],
        "operationId": "listEvents",
        "summary": "Follow the event feed",
        "description": "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": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/After"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of the feed.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventList"
                },
                "examples": {
                  "new_events": {
                    "summary": "GET /events?after=eyJpZCI6MTA0NiwiayI6ImV2ZW50cyJ9",
                    "value": {
                      "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"
                    }
                  },
                  "nothing_new": {
                    "summary": "GET /events?after=eyJpZCI6MTA0OSwiayI6ImV2ZW50cyJ9",
                    "value": {
                      "data": [],
                      "next_cursor": "eyJpZCI6MTA0OSwiayI6ImV2ZW50cyJ9"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`bad_request`, `unknown_parameter` or `key_in_url`. A parameter value is not valid, a parameter is not one this route knows, or the key was sent in the URL.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit-IfKey"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining-IfKey"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "bad_after": {
                    "summary": "An after that is not a cursor",
                    "value": {
                      "error": {
                        "code": "bad_request",
                        "message": "The cursor is not valid."
                      }
                    }
                  },
                  "cursor_on_events": {
                    "summary": "GET /events?cursor=… (the /calls parameter)",
                    "value": {
                      "error": {
                        "code": "unknown_parameter",
                        "message": "Unknown parameter 'cursor'. /events pages with 'after'."
                      }
                    }
                  },
                  "key_in_url": {
                    "summary": "A key sent in the URL",
                    "value": {
                      "error": {
                        "code": "key_in_url",
                        "message": "Send the key in the Authorization header, never in the URL (URLs are logged)."
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "tags": [
          "Meta"
        ],
        "operationId": "getOpenApi",
        "summary": "Get this contract",
        "description": "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.",
        "security": [],
        "responses": {
          "200": {
            "description": "The OpenAPI document.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "openapi",
                    "info",
                    "paths"
                  ],
                  "properties": {
                    "openapi": {
                      "const": "3.1.0"
                    },
                    "info": {
                      "type": "object"
                    },
                    "paths": {
                      "type": "object"
                    }
                  }
                },
                "examples": {
                  "contract": {
                    "summary": "GET /openapi.json (shortened)",
                    "value": {
                      "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"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`key_in_url`: a key was sent in the URL.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "key_in_url": {
                    "summary": "A key sent in the URL",
                    "value": {
                      "error": {
                        "code": "key_in_url",
                        "message": "Send the key in the Authorization header, never in the URL (URLs are logged)."
                      }
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "rk_live_ + 43 characters",
        "description": "An API key: `rk_live_` and 43 characters from `A-Z a-z 0-9 _ -`. Send it as `Authorization: Bearer rk_live_…`. A key in the URL is refused with 400 `key_in_url`; treat such a key as leaked and revoke it."
      }
    },
    "parameters": {
      "CallId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "The call's `id` from `GET /calls`.",
        "example": "web-trial-862861dca69e41079dd1bcd6b864992d"
      },
      "CallsLimit": {
        "name": "limit",
        "in": "query",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 50
        },
        "description": "How 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.",
        "example": 20
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 50
        },
        "description": "How many events in one page: 1 to 100, default 50. A larger number is treated as 100.",
        "example": 100
      },
      "CallsCursor": {
        "name": "cursor",
        "in": "query",
        "schema": {
          "type": "string"
        },
        "description": "The `next_cursor` from the page before. It means \"everything older than the last call of that page\": send the same filters with it.",
        "example": "eyJpZCI6IndlYi10cmlhbC04YWY5ZTJmMmVhNGY0M2MzYWRlZTEyYTVmODZjYzY2MSIsImsiOiJjYWxscyIsInQiOiIyMDI2LTEwLTA1VDExOjQwOjAyLjExODMwNSswMDowMCJ9"
      },
      "Since": {
        "name": "since",
        "in": "query",
        "schema": {
          "type": "string",
          "format": "date-time"
        },
        "description": "Only calls that started at or after this time (inclusive). ISO 8601 with a UTC offset; a time with no offset is read as UTC.",
        "example": "2026-10-01T00:00:00+00:00"
      },
      "Until": {
        "name": "until",
        "in": "query",
        "schema": {
          "type": "string",
          "format": "date-time"
        },
        "description": "Only calls that started before this time (exclusive). ISO 8601 with a UTC offset; a time with no offset is read as UTC.",
        "example": "2026-10-06T00:00:00+00:00"
      },
      "Outcome": {
        "name": "outcome",
        "in": "query",
        "schema": {
          "type": "string",
          "enum": [
            "booked",
            "emergency",
            "no_booking"
          ]
        },
        "description": "Only calls with this outcome.",
        "example": "booked"
      },
      "Kind": {
        "name": "kind",
        "in": "query",
        "schema": {
          "type": "string",
          "enum": [
            "line",
            "demo"
          ]
        },
        "description": "Only calls to your line (`line`), or only demo calls you claimed (`demo`).",
        "example": "line"
      },
      "Phone": {
        "name": "phone",
        "in": "query",
        "schema": {
          "type": "string",
          "pattern": "^(?:[^0-9]*[0-9]){10,}[^0-9]*$"
        },
        "description": "Only 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`.",
        "example": "(512) 555-0147"
      },
      "Expand": {
        "name": "expand",
        "in": "query",
        "schema": {
          "type": "string",
          "enum": [
            "transcript"
          ]
        },
        "description": "`transcript` adds each call's `transcript` to the answer, so you need no extra request per call.",
        "example": "transcript"
      },
      "CallEventsCursor": {
        "name": "cursor",
        "in": "query",
        "schema": {
          "type": "string"
        },
        "description": "The `next_cursor` from the page before.",
        "example": "eyJpZCI6MTA0MywiayI6ImNhbGxldmVudHMiLCJ0IjoiMjAyNi0xMC0wNVQxNDowNToyMy45MDcxMTQrMDA6MDAifQ"
      },
      "After": {
        "name": "after",
        "in": "query",
        "schema": {
          "type": "string"
        },
        "description": "The `next_cursor` from your last `/events` answer. You get only events after it. Leave it out to start from the oldest event.",
        "example": "eyJpZCI6MTA0NiwiayI6ImV2ZW50cyJ9"
      }
    },
    "headers": {
      "X-Request-Id": {
        "required": true,
        "schema": {
          "type": "string",
          "pattern": "^[0-9a-f]{32}$"
        },
        "description": "A random id for this request. Quote it when you ask for help.",
        "example": "3f9c2b7e8d1a4c6f9b0e5d2a7c4f1e08"
      },
      "Cache-Control": {
        "required": true,
        "schema": {
          "type": "string",
          "const": "no-store"
        },
        "description": "Answers are never cached. Ask again for fresh data.",
        "example": "no-store"
      },
      "X-RateLimit-Limit": {
        "required": true,
        "schema": {
          "type": "integer",
          "minimum": 1
        },
        "description": "Requests allowed per minute for this key (60).",
        "example": 60
      },
      "X-RateLimit-Remaining": {
        "required": true,
        "schema": {
          "type": "integer",
          "minimum": 0
        },
        "description": "Requests left in the current minute for this key.",
        "example": 57
      },
      "X-RateLimit-Limit-IfKey": {
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 1
        },
        "description": "Requests allowed per minute for this key. Present when the key is valid.",
        "example": 60
      },
      "X-RateLimit-Remaining-IfKey": {
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 0
        },
        "description": "Requests left in the current minute for this key. Present when the key is valid.",
        "example": 0
      },
      "Retry-After": {
        "required": true,
        "schema": {
          "type": "integer",
          "minimum": 0
        },
        "description": "Seconds to wait before you try again.",
        "example": 12
      },
      "Allow": {
        "required": true,
        "schema": {
          "type": "string",
          "const": "GET"
        },
        "description": "The only method the API accepts.",
        "example": "GET"
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "`invalid_key`: the Authorization header is missing, or the key is malformed, unknown or revoked. The answer is the same in every case.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          },
          "Cache-Control": {
            "$ref": "#/components/headers/Cache-Control"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "invalid_key": {
                "summary": "No key, or a key that is not valid",
                "value": {
                  "error": {
                    "code": "invalid_key",
                    "message": "The API key is missing or not valid. Send it as 'Authorization: Bearer <key>'."
                  }
                }
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "`not_found`: no such call, or the call is another organization's, or it is older than 30 days. The answer is the same in every case.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          },
          "Cache-Control": {
            "$ref": "#/components/headers/Cache-Control"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit-IfKey"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining-IfKey"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "not_found": {
                "summary": "No such call",
                "value": {
                  "error": {
                    "code": "not_found",
                    "message": "No such resource."
                  }
                }
              }
            }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "`method_not_allowed`: the API is read-only; use GET.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          },
          "Cache-Control": {
            "$ref": "#/components/headers/Cache-Control"
          },
          "Allow": {
            "$ref": "#/components/headers/Allow"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "method_not_allowed": {
                "summary": "A POST",
                "value": {
                  "error": {
                    "code": "method_not_allowed",
                    "message": "This API is read-only: use GET."
                  }
                }
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "`rate_limited`: more 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.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          },
          "Cache-Control": {
            "$ref": "#/components/headers/Cache-Control"
          },
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit-IfKey"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining-IfKey"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "rate_limited": {
                "summary": "Too many requests with this key",
                "value": {
                  "error": {
                    "code": "rate_limited",
                    "message": "Rate limit exceeded: 60 requests per minute per key."
                  }
                }
              },
              "rate_limited_invalid_key": {
                "summary": "Too many requests with invalid keys",
                "value": {
                  "error": {
                    "code": "rate_limited",
                    "message": "Too many requests with an invalid key. Slow down."
                  }
                }
              }
            }
          }
        }
      },
      "InternalError": {
        "description": "`internal_error`: something failed on our side. Try again; if it keeps failing, send us the `X-Request-Id`.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          },
          "Cache-Control": {
            "$ref": "#/components/headers/Cache-Control"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "internal_error": {
                "summary": "A failure on our side",
                "value": {
                  "error": {
                    "code": "internal_error",
                    "message": "Something went wrong on our side."
                  }
                }
              }
            }
          }
        }
      },
      "Unavailable": {
        "description": "`unavailable`: the service is busy for a moment. Wait `Retry-After` seconds and try again.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          },
          "Cache-Control": {
            "$ref": "#/components/headers/Cache-Control"
          },
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit-IfKey"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining-IfKey"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "unavailable": {
                "summary": "Busy",
                "value": {
                  "error": {
                    "code": "unavailable",
                    "message": "The service is busy. Try again in a few seconds."
                  }
                }
              }
            }
          }
        }
      }
    },
    "schemas": {
      "Call": {
        "type": "object",
        "description": "One ended call. The same calls, in the same order, as the Calls tab in the gear on whappai.com. A call still in progress is never listed.",
        "additionalProperties": false,
        "required": [
          "id",
          "started_at",
          "ended_at",
          "duration_seconds",
          "line",
          "kind",
          "caller",
          "need",
          "outcome",
          "booking",
          "details",
          "deleted_after",
          "recording"
        ],
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "description": "The call's id. Web and demo calls look like `web-trial-` plus 32 hex digits. Treat it as an opaque string."
          },
          "started_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the call began (UTC)."
          },
          "ended_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the call ended (UTC). Every listed call has ended."
          },
          "duration_seconds": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "description": "How long the call ran, in whole seconds."
          },
          "line": {
            "type": [
              "string",
              "null"
            ],
            "description": "The number the caller dialed (your Riff line), in E.164 form."
          },
          "kind": {
            "type": "string",
            "enum": [
              "line",
              "demo"
            ],
            "description": "`line`: a call to your business line. `demo`: a demo call you claimed in the gear."
          },
          "caller": {
            "$ref": "#/components/schemas/Caller"
          },
          "need": {
            "type": [
              "string",
              "null"
            ],
            "description": "What the caller needed, in a few words (for example `Water heater — not heating`). `null` when Riff did not learn it."
          },
          "outcome": {
            "type": "string",
            "enum": [
              "booked",
              "emergency",
              "no_booking"
            ],
            "description": "How the call ended. `booked`: a job was booked. `emergency`: no booking, and the caller said it was an emergency. `no_booking`: anything else. Always present, never `null`: Riff works it out when it answers, from the booking first, then the urgency."
          },
          "booking": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Booking"
              },
              {
                "type": "null"
              }
            ],
            "description": "The booked job, or `null` when nothing was booked."
          },
          "details": {
            "type": "object",
            "description": "Every detail Riff captured, by name. Common names: `name`, `phone`, `address`, `urgency`, `problem`. Which names appear depends on your call flow, so read the ones you know and keep the rest. `phone` is normalized like `caller.phone`.",
            "properties": {
              "phone": {
                "type": [
                  "string",
                  "null"
                ],
                "pattern": "^\\+1[0-9]{10}$",
                "description": "The number the caller gave: `+1` and 10 digits, or `null`."
              }
            },
            "additionalProperties": true
          },
          "deleted_after": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When Riff deletes this call (30 days after it ended). Copy it into your own system before then if you need it longer."
          },
          "recording": {
            "type": "null",
            "description": "Always `null` in this release. Recordings come in the next release."
          },
          "transcript": {
            "$ref": "#/components/schemas/Transcript",
            "description": "Present only when you ask for `expand=transcript`."
          }
        }
      },
      "Caller": {
        "type": "object",
        "description": "Who called.",
        "additionalProperties": false,
        "required": [
          "name",
          "phone"
        ],
        "properties": {
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "The caller's name as they gave it, or `null` when they gave no name. Never the phone number."
          },
          "phone": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\+1[0-9]{10}$",
            "description": "The caller's number in E.164 form: `+1` and 10 digits. `null` when the caller gave no usable number (for example a web call)."
          }
        }
      },
      "Booking": {
        "type": "object",
        "description": "The booked job.",
        "additionalProperties": false,
        "required": [
          "start",
          "end"
        ],
        "properties": {
          "start": {
            "type": "string",
            "format": "date-time",
            "description": "When the job starts, with the business's UTC offset."
          },
          "end": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the job ends, or `null` when it is not known."
          }
        }
      },
      "Transcript": {
        "type": "object",
        "description": "What was said on a call.",
        "additionalProperties": false,
        "required": [
          "turns"
        ],
        "properties": {
          "turns": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Turn"
            },
            "description": "What was said, in order. Empty when there is no transcript yet: it can be empty for a few seconds after `call.ended`, so fetch it again."
          }
        }
      },
      "Turn": {
        "type": "object",
        "description": "One turn of the conversation.",
        "additionalProperties": false,
        "required": [
          "speaker",
          "text"
        ],
        "properties": {
          "speaker": {
            "type": "string",
            "enum": [
              "agent",
              "caller"
            ],
            "description": "`agent` is Riff. `caller` is the person who called."
          },
          "text": {
            "type": "string",
            "minLength": 1,
            "description": "What they said."
          }
        }
      },
      "Event": {
        "description": "Something that happened on a call. `type` tells you the shape of `data`. This schema lists the event types of this version. New event types are announced in the changelog first; skip types you do not recognise.",
        "oneOf": [
          {
            "$ref": "#/components/schemas/CallStartedEvent"
          },
          {
            "$ref": "#/components/schemas/DetailCapturedEvent"
          },
          {
            "$ref": "#/components/schemas/AppointmentBookedEvent"
          },
          {
            "$ref": "#/components/schemas/CallEndedEvent"
          },
          {
            "$ref": "#/components/schemas/CallClaimedEvent"
          }
        ],
        "discriminator": {
          "propertyName": "type",
          "mapping": {
            "call.started": "#/components/schemas/CallStartedEvent",
            "call.detail_captured": "#/components/schemas/DetailCapturedEvent",
            "call.appointment_booked": "#/components/schemas/AppointmentBookedEvent",
            "call.ended": "#/components/schemas/CallEndedEvent",
            "call.claimed": "#/components/schemas/CallClaimedEvent"
          }
        }
      },
      "CallStartedEvent": {
        "type": "object",
        "description": "The call began.",
        "additionalProperties": false,
        "required": [
          "id",
          "type",
          "version",
          "occurred_at",
          "call_id",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[0-9]+$",
            "description": "The event's id: digits, as a string. Ids rise over time across the whole feed."
          },
          "type": {
            "const": "call.started"
          },
          "version": {
            "type": "integer",
            "minimum": 1,
            "description": "The version of this event type's `data` shape. Every type is at version 1."
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time",
            "description": "When it happened. `call.detail_captured` and `call.appointment_booked` carry the call's end time."
          },
          "call_id": {
            "type": "string",
            "minLength": 1,
            "description": "The call this event belongs to."
          },
          "data": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "dialed"
            ],
            "properties": {
              "dialed": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The number the caller dialed, as stored."
              }
            }
          }
        }
      },
      "DetailCapturedEvent": {
        "type": "object",
        "description": "Riff captured one detail. One event per detail.",
        "additionalProperties": false,
        "required": [
          "id",
          "type",
          "version",
          "occurred_at",
          "call_id",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[0-9]+$",
            "description": "The event's id: digits, as a string. Ids rise over time across the whole feed."
          },
          "type": {
            "const": "call.detail_captured"
          },
          "version": {
            "type": "integer",
            "minimum": 1,
            "description": "The version of this event type's `data` shape. Every type is at version 1."
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time",
            "description": "When it happened. `call.detail_captured` and `call.appointment_booked` carry the call's end time."
          },
          "call_id": {
            "type": "string",
            "minLength": 1,
            "description": "The call this event belongs to."
          },
          "data": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "field",
              "value"
            ],
            "properties": {
              "field": {
                "type": "string",
                "minLength": 1,
                "description": "The detail's name, like `address`."
              },
              "value": {
                "type": [
                  "string",
                  "number",
                  "boolean",
                  "object",
                  "array"
                ],
                "description": "The value as the caller gave it. Events keep raw values: a phone here is not normalized (the call's `details.phone` is)."
              }
            }
          }
        }
      },
      "AppointmentBookedEvent": {
        "type": "object",
        "description": "Riff booked a job on your calendar.",
        "additionalProperties": false,
        "required": [
          "id",
          "type",
          "version",
          "occurred_at",
          "call_id",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[0-9]+$",
            "description": "The event's id: digits, as a string. Ids rise over time across the whole feed."
          },
          "type": {
            "const": "call.appointment_booked"
          },
          "version": {
            "type": "integer",
            "minimum": 1,
            "description": "The version of this event type's `data` shape. Every type is at version 1."
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time",
            "description": "When it happened. `call.detail_captured` and `call.appointment_booked` carry the call's end time."
          },
          "call_id": {
            "type": "string",
            "minLength": 1,
            "description": "The call this event belongs to."
          },
          "data": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "booking",
              "calendar"
            ],
            "properties": {
              "booking": {
                "type": "object",
                "description": "The booking as the calendar confirmed it.",
                "additionalProperties": true,
                "required": [
                  "start"
                ],
                "properties": {
                  "event_id": {
                    "type": "string",
                    "description": "The calendar's id for the booked event."
                  },
                  "slot_id": {
                    "type": "string"
                  },
                  "offer_set_id": {
                    "type": "string"
                  },
                  "start": {
                    "type": "string",
                    "format": "date-time",
                    "description": "When the job starts."
                  },
                  "end": {
                    "type": "string",
                    "format": "date-time",
                    "description": "When the job ends. Can be missing when the calendar gave no end time."
                  },
                  "status": {
                    "type": "string",
                    "description": "`confirmed`."
                  }
                }
              },
              "calendar": {
                "anyOf": [
                  {
                    "type": "object",
                    "additionalProperties": true,
                    "required": [
                      "kind"
                    ],
                    "properties": {
                      "kind": {
                        "type": "string",
                        "description": "`live` (your calendar) or `mock` (a practice calendar)."
                      },
                      "label": {
                        "type": [
                          "string",
                          "null"
                        ]
                      }
                    }
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Which calendar took the booking."
              }
            }
          }
        }
      },
      "CallEndedEvent": {
        "type": "object",
        "description": "The call ended. Fetch the call and its transcript now.",
        "additionalProperties": false,
        "required": [
          "id",
          "type",
          "version",
          "occurred_at",
          "call_id",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[0-9]+$",
            "description": "The event's id: digits, as a string. Ids rise over time across the whole feed."
          },
          "type": {
            "const": "call.ended"
          },
          "version": {
            "type": "integer",
            "minimum": 1,
            "description": "The version of this event type's `data` shape. Every type is at version 1."
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time",
            "description": "When it happened. `call.detail_captured` and `call.appointment_booked` carry the call's end time."
          },
          "call_id": {
            "type": "string",
            "minLength": 1,
            "description": "The call this event belongs to."
          },
          "data": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "turns"
            ],
            "properties": {
              "turns": {
                "type": "integer",
                "minimum": 0,
                "description": "How many turns the transcript has."
              }
            }
          }
        }
      },
      "CallClaimedEvent": {
        "type": "object",
        "description": "A demo call was claimed into your organization. It now appears in `GET /calls` with `kind: demo`; fetch it.",
        "additionalProperties": false,
        "required": [
          "id",
          "type",
          "version",
          "occurred_at",
          "call_id",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[0-9]+$",
            "description": "The event's id: digits, as a string. Ids rise over time across the whole feed."
          },
          "type": {
            "const": "call.claimed"
          },
          "version": {
            "type": "integer",
            "minimum": 1,
            "description": "The version of this event type's `data` shape. Every type is at version 1."
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time",
            "description": "When it happened. `call.detail_captured` and `call.appointment_booked` carry the call's end time."
          },
          "call_id": {
            "type": "string",
            "minLength": 1,
            "description": "The call this event belongs to."
          },
          "data": {
            "type": "object",
            "additionalProperties": false,
            "description": "Always empty: `{}`."
          }
        }
      },
      "CallList": {
        "type": "object",
        "description": "A page of calls.",
        "additionalProperties": false,
        "required": [
          "data",
          "next_cursor"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Call"
            },
            "description": "Calls, newest first."
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Send it as `cursor` (with the same filters) for the next page. `null` when there are no more calls."
          }
        }
      },
      "EventList": {
        "type": "object",
        "description": "A page of events.",
        "additionalProperties": false,
        "required": [
          "data",
          "next_cursor"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Event"
            },
            "description": "Events, oldest first."
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "On `/calls/{id}/events`: send it as `cursor` for the next page; `null` when there are no more. On `/events`: always a string; store it and send it as `after` next time."
          }
        }
      },
      "Error": {
        "type": "object",
        "description": "Every error has this shape. Act on `code`; `message` is for people and can change. Codes: `invalid_key` (401), `bad_request` (400: a parameter value is not valid; the message names the parameter and the allowed values), `unknown_parameter` (400: the 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'.`), `key_in_url` (400), `not_found` (404: no such call, or not yours; the answer is the same), `method_not_allowed` (405), `rate_limited` (429), `internal_error` (500), `unavailable` (503).",
        "additionalProperties": false,
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "invalid_key",
                  "bad_request",
                  "unknown_parameter",
                  "key_in_url",
                  "not_found",
                  "method_not_allowed",
                  "rate_limited",
                  "internal_error",
                  "unavailable"
                ]
              },
              "message": {
                "type": "string",
                "description": "What went wrong, for a person."
              }
            }
          }
        }
      }
    }
  }
}
