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

# Check Replies

> Get replies for a lead or campaign — who replied, what they said, and the conversation context. REST endpoint in the Tuco AI iMessage API — bearer-token auth.

<Note>
  Use this endpoint to check if a specific lead has replied, or to list all leads who replied (optionally scoped to a campaign). Returns the reply text, up to 10 recent replies from the lead, and up to 10 outbound messages you sent them.

  When a lead replies with a **photo or file**, the row (and each `recentReplies` entry) carries `attachmentDownloadUrls` — public, signed URLs you can `GET` directly to download the image/file bytes, no auth header needed. See [Downloading inbound attachments](#downloading-inbound-attachments).
</Note>

## Authentication

```bash theme={null}
Authorization: Bearer tuco_sk_xxxxxxxxxxxxx
```

## Query parameters

<ParamField query="leadId" type="string">Replies from this lead only (valid ObjectId; returns 404 if lead not found).</ParamField>
<ParamField query="campaignId" type="string">Restrict to leads who replied and are in this campaign.</ParamField>
<ParamField query="recipientEmail" type="string">Filter by reply sender email.</ParamField>
<ParamField query="recipientPhone" type="string">Filter by reply sender phone (E.164 or normalized).</ParamField>
<ParamField query="lineId" type="string">Restrict to replies received on this Tuco line (24-char ObjectId hex). Returns `400` if malformed.</ParamField>
<ParamField query="dateFrom" type="string">Inclusive ISO 8601 UTC lower bound on the reply timestamp (e.g. `2026-06-01T00:00:00Z`). Returns `400` if unparseable or greater than `dateTo`.</ParamField>
<ParamField query="dateTo" type="string">Inclusive ISO 8601 UTC upper bound on the reply timestamp.</ParamField>
<ParamField query="page" type="number" default="1">Page number.</ParamField>
<ParamField query="limit" type="number" default="50">Results per page (max 100).</ParamField>

***

## Examples

<CodeGroup>
  ```bash By phone number theme={null}
  curl "https://app.tuco.ai/api/replies?recipientPhone=%2B14422646782&limit=10" \
    -H "Authorization: Bearer tuco_sk_xxxxxxxxxxxxx"
  ```

  ```bash By lead ID theme={null}
  curl "https://app.tuco.ai/api/replies?leadId=69b2c41d352a78479d2c623b" \
    -H "Authorization: Bearer tuco_sk_xxxxxxxxxxxxx"
  ```

  ```bash By campaign theme={null}
  curl "https://app.tuco.ai/api/replies?campaignId=69ab444ac1feb77d7f46462f&page=1&limit=50" \
    -H "Authorization: Bearer tuco_sk_xxxxxxxxxxxxx"
  ```

  ```bash By email theme={null}
  curl "https://app.tuco.ai/api/replies?recipientEmail=john%40example.com" \
    -H "Authorization: Bearer tuco_sk_xxxxxxxxxxxxx"
  ```

  ```bash By line + date window theme={null}
  curl "https://app.tuco.ai/api/replies?lineId=60d5ec49e3a8a0001f9b9999&dateFrom=2026-06-01T00:00:00Z&dateTo=2026-06-10T23:59:59Z&page=1&limit=100" \
    -H "Authorization: Bearer tuco_sk_xxxxxxxxxxxxx"
  ```
</CodeGroup>

***

## Success response (200)

```json theme={null}
{
  "replies": [
    {
      "leadId": "69b2c41d352a78479d2c623b",
      "messageId": "69b340dd6dae1d942ece4d09",
      "message": "Stop",
      "respondedAtUtc": "2026-03-12T22:40:29.174Z",
      "recipientPhone": "+919042956129",
      "recipientEmail": "goforbg@icloud.com",
      "name": "BG Real",
      "hasReplied": false,
      "hasEverReplied": true,
      "attachmentDownloadUrls": [
        "https://app.tuco.ai/api/attachments/crm-media/IMG_1380.jpeg?u=aHR0cHM...&exp=1786752000000&sig=Vh9k..."
      ],
      "attachmentNames": ["IMG_1380.jpeg"],
      "recentReplies": [
        {
          "messageId": "69b340dd6dae1d942ece4d09",
          "message": "Stop",
          "receivedAtUtc": "2026-03-12T22:40:29.174Z"
        },
        {
          "messageId": "69b33fdd6dae1d942ece4d08",
          "message": "Here's the photo you asked for",
          "receivedAtUtc": "2026-03-12T21:15:00.000Z",
          "attachmentDownloadUrls": [
            "https://app.tuco.ai/api/attachments/crm-media/IMG_1380.jpeg?u=aHR0cHM...&exp=1786752000000&sig=Vh9k..."
          ],
          "attachmentNames": ["IMG_1380.jpeg"]
        }
      ],
      "parentMessages": [
        {
          "messageId": "69b344bc6dae1d942ece4d0e",
          "message": "Here's your document.",
          "sentAtUtc": "2026-03-12T22:57:02.892Z",
          "batchId": null,
          "stepIndex": null
        }
      ]
    }
  ],
  "total": 1,
  "page": 1,
  "limit": 50,
  "totalPages": 1
}
```

### Reply fields

<ResponseField name="leadId" type="string">Lead who replied.</ResponseField>
<ResponseField name="messageId" type="string">ID of the most recent reply message.</ResponseField>
<ResponseField name="message" type="string">The reply text (e.g. "Stop", "Interested!", "Call me tomorrow").</ResponseField>
<ResponseField name="respondedAtUtc" type="string">ISO 8601 timestamp of the most recent reply.</ResponseField>
<ResponseField name="recipientPhone" type="string">Lead's phone number.</ResponseField>
<ResponseField name="recipientEmail" type="string">Lead's email address.</ResponseField>
<ResponseField name="name" type="string">Lead's display name.</ResponseField>
<ResponseField name="hasReplied" type="boolean">`true` when the lead's newest inbound is newer than your newest outbound to them (no outbound on record → `true`). Any follow-up you send after their reply — including AI copilot auto-replies — flips it back to `false`. With `campaignId` set, outbounds are scoped to that campaign, so this reads as "replied since the campaign's last message".</ResponseField>
<ResponseField name="hasEverReplied" type="boolean">Always `true` for a returned row — leads who never replied have no row. Included so this endpoint and the GHL Check for Replies action expose the same field names; branch on it when an auto-responder makes `hasReplied` flip to `false` within seconds.</ResponseField>

<ResponseField name="attachmentDownloadUrls" type="string[]">
  Present only when the most recent reply included attachments. Public, signed, 30-day URLs — `GET` each one (no auth header) to download the actual image/file bytes, served with the correct `Content-Type`. Omitted for text-only replies. See [Downloading inbound attachments](#downloading-inbound-attachments).
</ResponseField>

<ResponseField name="attachmentNames" type="string[]">
  Filenames aligned 1:1 with `attachmentDownloadUrls` (e.g. `["IMG_1380.jpeg"]`). Omitted for text-only replies.
</ResponseField>

### recentReplies (up to 10)

All inbound replies from this lead, newest first. Gives you the full conversation context from their side.

<ResponseField name="recentReplies" type="array">
  <Expandable title="Reply object">
    <ResponseField name="messageId" type="string">Reply message ID.</ResponseField>
    <ResponseField name="message" type="string">Reply text (truncated to 200 chars).</ResponseField>
    <ResponseField name="receivedAtUtc" type="string">When the reply was received (UTC ISO 8601).</ResponseField>
    <ResponseField name="attachmentDownloadUrls" type="string[]">Present only when this reply included attachments. Public, signed download URLs for the image/file bytes (omitted otherwise).</ResponseField>
    <ResponseField name="attachmentNames" type="string[]">Filenames aligned 1:1 with `attachmentDownloadUrls`.</ResponseField>
  </Expandable>
</ResponseField>

### parentMessages (up to 10)

Outbound messages you sent to this lead, newest first. Shows what you said before they replied.

<ResponseField name="parentMessages" type="array">
  <Expandable title="Parent message object">
    <ResponseField name="messageId" type="string">Outbound message ID.</ResponseField>
    <ResponseField name="message" type="string">Message text (truncated to 200 chars).</ResponseField>
    <ResponseField name="sentAtUtc" type="string">When the message was sent (UTC ISO 8601).</ResponseField>
    <ResponseField name="batchId" type="string">Batch/campaign batch ID if applicable.</ResponseField>
    <ResponseField name="stepIndex" type="integer">Campaign step index if applicable.</ResponseField>
  </Expandable>
</ResponseField>

***

## Error responses

| Status | When                                          | Body                                                                   |
| ------ | --------------------------------------------- | ---------------------------------------------------------------------- |
| `400`  | Invalid `leadId` or `campaignId`              | `{ "error": "Invalid leadId" }` or `{ "error": "Invalid campaignId" }` |
| `400`  | Invalid `lineId` (not a 24-char ObjectId hex) | `{ "error": "Invalid lineId" }`                                        |
| `400`  | `dateFrom` / `dateTo` unparseable             | `{ "error": "Invalid dateFrom" }` or `{ "error": "Invalid dateTo" }`   |
| `400`  | `dateFrom > dateTo`                           | `{ "error": "dateFrom must be <= dateTo" }`                            |
| `401`  | Missing or invalid API key                    | `{ "error": "Unauthorized" }`                                          |
| `404`  | `leadId` provided but lead not found          | `{ "error": "Lead not found or access denied" }`                       |
| `404`  | `campaignId` provided but campaign not found  | `{ "error": "Campaign not found" }`                                    |
| `429`  | Rate-limited (120 req/min per workspace)      | `{ "error": "Rate limit exceeded" }`                                   |
| `500`  | Server error                                  | `{ "error": "Failed to fetch replies" }`                               |

***

## Downloading inbound attachments

When a lead replies with a photo or file, Tuco stores the bytes in **private** blob storage. The raw storage URL is not directly fetchable — it returns `403`. Instead, every attachment-bearing reply exposes **`attachmentDownloadUrls`**: signed, 30-day proxy URLs that stream the real bytes with the correct `Content-Type`.

You can fetch a download URL with **either** credential — pick whichever fits:

<CardGroup cols={2}>
  <Card title="Just the URL (signature)" icon="link">
    The URL already carries an HMAC signature (`&sig=…`), so **no header is needed**. Drop it into an `<img src>`, a re-hosting job, or `curl -O`. This is the simplest path. The signature is unforgeable and expires after 30 days.
  </Card>

  <Card title="Your API key" icon="key">
    Prefer to pass a token? Send `Authorization: Bearer tuco_sk_…`. Tuco verifies the key's workspace **owns** that attachment, so a key can only ever read its own workspace's files. Revocable by rotating the key.
  </Card>
</CardGroup>

<Warning>
  The signed URL grants read access to that one file for 30 days. Treat it like a capability: fetch and store the bytes in your own system rather than passing the URL around long-term. To fetch a fresh signed URL, just call `GET /api/replies` (or listen for the [`message.reply` webhook](/api-reference/endpoint/webhook)) again.
</Warning>

```bash Download every photo a lead sent theme={null}
# 1) List the lead's replies and pull the download URLs + filenames
curl -s "https://app.tuco.ai/api/replies?leadId=69b2c41d352a78479d2c623b" \
  -H "Authorization: Bearer tuco_sk_xxxxxxxxxxxxx" \
| jq -r '.replies[].recentReplies[]
         | select(.attachmentDownloadUrls != null)
         | .attachmentNames[] as $n | .attachmentDownloadUrls[] | [$n, .] | @tsv' \
| while IFS=$'\t' read -r name url; do
    # 2) Fetch the bytes — the signature in the URL is the credential, no header needed.
    #    (Prefer your own token? add:  -H "Authorization: Bearer tuco_sk_xxxxxxxxxxxxx")
    curl -s "$url" -o "$name"
    echo "saved $name"
  done
```

The same `attachmentDownloadUrls` / `attachmentNames` fields are delivered in real time on the `message.reply` webhook and appear on inbound messages from [`GET /api/messages`](/api-reference/messages), so you can ingest photos push-style instead of polling.
