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

# Poll for Finished Calls and Sync Them to Your Database

> Use GET /api/v1/conversations?since= to continuously pull completed calls into your database, spreadsheet, or case management system.

export const FORMBHARO_URL = "https://api.formbharo.artpark.ai";

FormBharo's cross-agent conversations endpoint lets you pull finished calls into any external system on a schedule. By filtering on `since`, you retrieve only calls updated since your last poll — making it easy to keep a local database in sync without re-processing the full history on every request.

## The polling pattern

<Steps>
  <Step title="First poll">
    On your very first run, call `GET /api/v1/conversations?since=0` to retrieve all calls. Process each record and insert it into your database. After processing, find the **largest `created_at` value** in the response and save it — this becomes your cursor for the next poll.

    ```bash curl theme={null}
    curl "{FORMBHARO_URL}/api/v1/conversations?since=0" \
      -H "Authorization: Bearer <api-key>"
    ```
  </Step>

  <Step title="Subsequent polls">
    On every subsequent run, call `GET /api/v1/conversations?since=<last_created_at>`, substituting the cursor you saved in the previous step.

    The `since` filter is **inclusive**: it returns calls whose `created_at` is greater than or equal to the timestamp you pass. That means the call at the boundary will appear again. Match incoming records by `conversation_id` and **update** the existing row rather than inserting a duplicate.
  </Step>

  <Step title="Repeat">
    Wait a few seconds to a minute, then return to step 2. How often you poll depends on how fresh your data needs to be — a CRM sync can afford a one-minute interval; a live dashboard might poll every five seconds.
  </Step>
</Steps>

## Example request

<CodeGroup>
  ```bash curl theme={null}
  curl -G "{FORMBHARO_URL}/api/v1/conversations" \
    -H "Authorization: Bearer <api-key>" \
    --data-urlencode "since=0" \
    --data-urlencode "status=complete" \
    --data-urlencode "limit=50"
  ```

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

  BASE_URL = "{FORMBHARO_URL}"
  HEADERS = {"Authorization": "Bearer <api-key>"}

  def poll_conversations(since: int = 0) -> list[dict]:
      params = {
          "since": since,
          "status": "complete",
          "limit": 50,
      }

      all_conversations = []
      cursor = None

      while True:
          if cursor:
              params["cursor"] = cursor

          response = requests.get(
              f"{BASE_URL}/api/v1/conversations",
              headers=HEADERS,
              params=params,
          )
          response.raise_for_status()
          data = response.json()

          conversations = data.get("conversations", [])
          all_conversations.extend(conversations)

          cursor = data.get("next_cursor")
          if not cursor:
              break

      return all_conversations

  # First poll — fetch everything
  results = poll_conversations(since=0)

  # Save the largest created_at as your next cursor
  if results:
      last_created_at = max(r["created_at"] for r in results)
      print(f"Next poll: since={last_created_at}")
  ```
</CodeGroup>

## Filtering options

Use query parameters to narrow the result set before it reaches your system.

<ResponseField name="agent_id" type="string">
  Limit results to conversations belonging to a single agent.
</ResponseField>

<ResponseField name="workspace_id" type="string">
  Limit results to conversations belonging to a single workspace.
</ResponseField>

<ResponseField name="status" type="string">
  Filter by conversation status. Accepted values: `in_progress`, `complete`, `screened_out`, `failed`.
</ResponseField>

<ResponseField name="source" type="string">
  Filter by the source that initiated the call (for example, a specific integration or channel).
</ResponseField>

<ResponseField name="since" type="integer">
  Unix timestamp (inclusive). Returns calls where `created_at >= since`.
</ResponseField>

<ResponseField name="until" type="integer">
  Unix timestamp (inclusive). Returns calls where `created_at <= until`.
</ResponseField>

<ResponseField name="limit" type="integer">
  Number of rows per page. Accepts `1` to `200`. Defaults to `50`.
</ResponseField>

<ResponseField name="cursor" type="string">
  Pagination offset returned in the previous response as `next_cursor`. Pass it to retrieve the next page.
</ResponseField>

## Two things to know

<Note>
  `created_at` is the **last-write time**, not the call start time. If someone edits an answer on a past call, that call's `created_at` moves forward into the current window. A date range you've already polled is never truly final — always upsert by `conversation_id` rather than assuming a record won't change.
</Note>

<Note>
  Poll at most a few times per minute. The endpoint scans history on every request, and response time grows as your total call count increases. If you need near-real-time sync, consider polling every 5–10 seconds for only the most recent window rather than fetching large ranges on every request.
</Note>

## Fetching transcripts

Transcripts are **not** included in the cross-agent conversations list. Fetch them one call at a time once you have the identifiers you need.

```bash curl theme={null}
curl "{FORMBHARO_URL}/api/v1/agents/AGENT_ID/conversations/CONVERSATION_ID/transcript" \
  -H "Authorization: Bearer <api-key>"
```

If no transcript exists for a conversation, the endpoint returns `404` with the body:

```json json theme={null}
{ "error": "Transcript not found" }
```

This is expected for calls that ended before any speech was recorded — treat it as an empty transcript rather than an error condition.
