> ## Documentation Index
> Fetch the complete documentation index at: https://docs.telzino.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Reservation Analytics

> Call outcomes, drop-off, SMS delivery and third-party API usage for OpenTable Reservation agents over a date range

The same figures the dashboard's **Reports → Reservation Agent** page shows, for
[OpenTable Reservation agents](/functions/opentable-reservations).

Scoped to the authenticated account, and narrowable to one organization or one
agent. An account with no reservation agents gets a valid, empty report rather than
an error — no agents is not a failure, and it must not read as a restaurant that
took no bookings.

<Note>
  Third-party API **cost** is never returned. StayAPI and Twilio Lookup are funded at
  the Platform tier, so this exposes call counts only, for capacity planning and
  anomaly detection.
</Note>

## Query Parameters

<ParamField query="start_date" type="string" required>
  Inclusive start of the range, `YYYY-MM-DD`.
</ParamField>

<ParamField query="end_date" type="string" required>
  Inclusive end of the range, `YYYY-MM-DD`. Must be on or after `start_date`, or the
  request is rejected with `400`.
</ParamField>

<ParamField query="organization_id" type="string">
  Narrow to one organization (UUID). An organization outside your account returns an
  empty report, not an error.
</ParamField>

<ParamField query="agent_id" type="string">
  Narrow to one agent (UUID).
</ParamField>

<RequestExample>
  ```bash cURL theme={null}
  curl "https://api.telzino.com/v1/reservations/analytics?start_date=2026-08-01&end_date=2026-08-17" \
    -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
  ```

  ```javascript JavaScript theme={null}
  const params = new URLSearchParams({
    start_date: '2026-08-01',
    end_date: '2026-08-17',
  });

  const response = await fetch(
    `https://api.telzino.com/v1/reservations/analytics?${params}`,
    { headers: { Authorization: 'Bearer YOUR_ACCESS_TOKEN' } },
  );

  const report = await response.json();
  console.log(report.outcomes.booking_rate);
  ```

  ```python Python theme={null}
  import requests

  response = requests.get(
      'https://api.telzino.com/v1/reservations/analytics',
      params={'start_date': '2026-08-01', 'end_date': '2026-08-17'},
      headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'},
  )

  report = response.json()
  print(report['outcomes']['booking_rate'])
  ```
</RequestExample>

## Response

<ResponseField name="start_date" type="string">
  Echoed back.
</ResponseField>

<ResponseField name="end_date" type="string">
  Echoed back.
</ResponseField>

<ResponseField name="agents" type="array">
  The reservation agents in scope: `id`, `name`, `organization_id`.
</ResponseField>

<ResponseField name="truncated" type="boolean">
  `true` when the range held more calls or messages than one report can load, so the
  figures cover only part of it. Narrow the dates for an exact answer.
</ResponseField>

<ResponseField name="outcomes" type="object">
  <Expandable title="fields">
    <ResponseField name="total_calls" type="integer" />

    <ResponseField name="booking_rate" type="number | null">
      Share of calls that sent a booking link or had an alternative accepted. `null`
      rather than `0` when there were no calls — an absence, not a failure. Every rate
      in this response follows that rule.
    </ResponseField>

    <ResponseField name="counts" type="object">
      Calls per outcome, keyed by outcome name.
    </ResponseField>

    <ResponseField name="drop_off" type="array">
      `{ step, calls }` for each step of the conversation, counting the furthest step
      an unsuccessful call reached. Successful calls are excluded, so the success case
      is not rendered as the biggest cliff.
    </ResponseField>

    <ResponseField name="average_duration_by_outcome" type="object">
      Mean call length in seconds, per outcome. `null` where unknown.
    </ResponseField>

    <ResponseField name="barge_in_rate" type="number | null" />

    <ResponseField name="by_hour" type="array">
      24 counts, bucketed in **each agent's own restaurant timezone**, not the
      server's or yours.
    </ResponseField>

    <ResponseField name="by_weekday" type="array">
      7 counts, `0` = Sunday, in the same restaurant-local clock.
    </ResponseField>

    <ResponseField name="fallback_steps" type="object">
      How often each rung of the "text cannot get through" ladder was used.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="deliveries" type="object">
  <Expandable title="fields">
    <ResponseField name="total_messages" type="integer" />

    <ResponseField name="delivered" type="integer" />

    <ResponseField name="failed" type="integer" />

    <ResponseField name="pending" type="integer">
      Sent, with no carrier verdict yet.
    </ResponseField>

    <ResponseField name="delivery_rate" type="number | null">
      Computed over **resolved** messages only, so the figure does not dip every time
      you look at it mid-service.
    </ResponseField>

    <ResponseField name="failure_rate" type="number | null" />

    <ResponseField name="alert" type="boolean">
      `true` when the range-wide failure rate is above 5%, **or** when any single
      restaurant-day breached it.
    </ResponseField>

    <ResponseField name="alerts" type="array">
      R6.6's threshold as the story states it — per restaurant, per day. Each entry
      is `{ agent_id, day, messages, failed, failure_rate }`, worst first. Days with
      fewer than five resolved messages are excluded, because a 50% failure rate over
      two messages says nothing. The range-wide `failure_rate` above averages every
      restaurant over every day, so one restaurant carrier-filtered for one day
      disappears into it; this is where it shows up.
    </ResponseField>

    <ResponseField name="by_provider" type="object">
      `{ sent, delivered, failed }` per carrier, so provider-specific degradation is
      visible rather than averaged away.
    </ResponseField>

    <ResponseField name="by_kind" type="object">
      Booking links, waitlist links, manage-reservation links and events-contact texts
      counted separately.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="usage" type="object">
  <Expandable title="fields">
    <ResponseField name="stayapi_calls" type="integer" />

    <ResponseField name="stayapi_cache_hits" type="integer">
      Availability answers served from the per-call cache instead of a fresh request.
    </ResponseField>

    <ResponseField name="cache_hit_rate" type="number | null" />

    <ResponseField name="lookup_calls" type="integer" />

    <ResponseField name="outliers" type="array">
      Agents over the daily anomaly thresholds: `{ agent_id, stayapi_calls,
                  lookup_calls }`.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseExample>
  ```json 200 theme={null}
  {
    "start_date": "2026-08-01",
    "end_date": "2026-08-17",
    "agents": [
      {
        "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "name": "Nerea",
        "organization_id": "9c1b2d3e-4f56-4789-a0b1-c2d3e4f56789"
      }
    ],
    "outcomes": {
      "total_calls": 412,
      "booking_rate": 0.61,
      "counts": { "booked_verified": 214, "alternative_accepted": 38, "human_handoff": 44 },
      "drop_off": [{ "step": "availability_checked", "calls": 27 }],
      "average_duration_by_outcome": { "booked_verified": 96 },
      "barge_in_rate": 0.04,
      "by_hour": [0, 0, 0, 0, 0, 0, 0, 2, 9, 21, 34, 48, 51, 39, 26, 22, 31, 44, 39, 28, 12, 5, 1, 0],
      "by_weekday": [41, 52, 58, 61, 74, 83, 43],
      "fallback_steps": { "email": 11, "callback": 7 }
    },
    "deliveries": {
      "total_messages": 252,
      "delivered": 241,
      "failed": 5,
      "pending": 6,
      "delivery_rate": 0.98,
      "failure_rate": 0.02,
      "alert": false,
      "alerts": [],
      "by_provider": { "twilio": { "sent": 252, "delivered": 241, "failed": 5 } },
      "by_kind": { "booking_link": 238, "waitlist_link": 14 }
    },
    "usage": {
      "stayapi_calls": 503,
      "stayapi_cache_hits": 147,
      "cache_hit_rate": 0.23,
      "lookup_calls": 249,
      "outliers": []
    },
    "truncated": false
  }
  ```

  ```json 400 theme={null}
  {
    "error": "invalid_request",
    "error_description": "start_date must be on or before end_date"
  }
  ```
</ResponseExample>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.