# Custom API (/docs/integrations/custom-api)

Connect your customer portal, feedback system, or other tool to Sift.
Custom API imports private conversations and starts Sift analysis.
Imported conversations use normal action statuses. Moderators can claim, tag, and change their status.
Sift cannot send replies to these conversations.

## At a glance [#at-a-glance]

| | |
| --- | --- |
| Import customer content | ✅ Single items or batches |
| Author information, channels, and tags | ✅ |
| Registered item and author fields | ✅ Map fields before importing |
| Stable IDs and exact retries | ✅ |
| Analysis and receipt tracking | ✅ |
| Import key access | Selected sources only |
| Batch size | Up to 100 items and 1 MiB |
| Outbound replies or attachments | ❌ Text intake only |

## What you're looking at [#what-youre-looking-at]

Open **Settings → Connections → Integrations → Custom API**. Each source represents an external system. Its three tabs guide setup:

| Tab | Use |
| --- | --- |
| Setup | Open API keys, configure the source, channels, and tags. Copy the endpoint and try an item. |
| Fields | Map producer field names to registered Sift item or author fields. Fields that a batch agent writes are not offered. |
| Activity | Track receipts, check analysis, and open stored conversations. |

The flow is: **Source → Import key → Validate → Import → Receipt → Sift analysis**.

## Set it up [#set-it-up]

{/* screenshot */}
<figure>
  <img src="/assets/screenshots/custom-api/source-setup.png" alt="Connect a source form with a source name and an optional default channel name." width="600" />
  <figcaption>Name the source. The default channel name is optional.</figcaption>
</figure>

1. Select **Add source**. Enter a source name. The default channel name is optional and defaults to the source name.
2. In **Setup**, add channels or source tags if needed. Keep the channel with external ID `default`.
3. In **Fields**, map custom fields before sending them.
4. In **API keys**, create an **Import** key. The default selects this source and a 90-day expiry.
5. Copy the secret once. Store it in your tool's secret store.
6. Copy the endpoint from **Setup**.

Organization admins manage sources and keys. Use one source per external system. Item and author IDs stay separate across sources.

## Choose the right key [#choose-the-right-key]

{/* screenshot */}
<figure>
  <img src="/assets/screenshots/custom-api/key-permissions.png" alt="Permission profile menu with Reporting, Import, Write, and Read and write." width="600" />
  <figcaption>Choose Import to send content from selected sources. Developer access defaults to Reporting.</figcaption>
</figure>

| Profile | Access |
| --- | --- |
| Import | Validate, import, correct items, retry failed intake, and read receipts for selected sources. Scopes: `ingest`, `ingest:read`. |
| Reporting | Read reporting data. No intake or receipt access. Scope: `read`. |
| Write | General API writes. No Custom API intake. Scope: `write`. |
| Read and write | General reads and writes. No Custom API intake. Scopes: `read`, `write`. |

Use separate keys for ingestion and reporting. Expiry options are 30 days, 90 days, or no expiry. Revocation blocks new requests.

## Send an item [#send-an-item]

Save this example as `item.json`. Replace the example values with your content.

```json
{
  "externalId": "feedback-184",
  "text": "Please add CSV export for feedback.",
  "occurredAt": "2026-10-12T09:00:00Z",
  "author": {
    "externalId": "customer-73",
    "name": "Maya Chen"
  }
}
```

Set `SIFT_API_BASE` to `https://api.getsift.ai`, `SIFT_SOURCE_ID` to your source ID, and `SIFT_IMPORT_KEY` to your Import key. Then send:

```bash
curl --fail-with-body -X POST \
  "$SIFT_API_BASE/v0/sources/$SIFT_SOURCE_ID/items" \
  -H "X-API-Key: $SIFT_IMPORT_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @item.json
```

For staging, use `https://api.staging.getsift.ai`. Keep staging and production sources and keys separate.

Required fields are `externalId`, `text`, `occurredAt`, `author.externalId`, and `author.name`. Use a stable item ID from your system. No platform message ID is required. Use the original event time in ISO format with `Z` or a timezone offset.

**Try an item** in Setup validates JSON before import. API validation uses `POST /v0/sources/{sourceId}/items/validate` with an `items` array. Validation does not store content or start analysis.

A successful import returns HTTP `202` and a `receipts` array. Save the receipt ID, item ID, revision, and status. Acceptance does not mean analysis is complete.

## Channels, tags, and custom fields [#channels-tags-and-custom-fields]

| Optional field | Use |
| --- | --- |
| `channelExternalId` | A configured channel ID. Defaults to `default`. |
| `title`, `sourceUrl` | An optional title and HTTPS link to the original item. |
| `author.username`, `author.email` | Optional username and valid email address. |
| `author.avatarUrl`, `author.profileUrl` | Optional HTTPS URLs. |
| `customFields`, `author.customFields` | Values for mapped, registered Sift fields. |
| `metadata`, `author.metadata` | JSON context. Metadata does not create report fields. |
| `tagIds` | Up to 32 existing Sift tag IDs from your organization. Use IDs, not names. |

A source supports up to 50 channels. Source tags and channel tags can filter imported conversations.

Map producer keys to registered fields under **Fields**. For example, map `plan` to an author field, then send `"customFields": {"plan": "Pro"}` inside `author`. Unmapped keys are rejected. Human edits and verified item fields stay protected.

## Threads and replies [#threads-and-replies]

For a standalone item, omit thread and parent IDs. For a thread root, set `threadExternalId` to its own `externalId`. For a reply, send the root ID as `threadExternalId` and the immediate parent ID as `parentExternalId`.

Import roots first. A reply cannot be its own parent or root. A reply received before its root can show `waiting_for_root`.

## Track processing [#track-processing]

Open **Activity**, or read recent receipts with `GET /v0/sources/{sourceId}/receipts?limit=50`. The limit is 1–100; the default is 50.

| Intake status | Meaning |
| --- | --- |
| `accepted` | A durable receipt is reserved. |
| `processing` | Intake work is running. |
| `waiting_for_root` | Import the matching thread root. |
| `stored` | Content is stored. Check analysis separately. |
| `failed` | Fix the cause, then retry the receipt. |
| `superseded` | A newer revision replaces this pending work. |

Stored receipts show current conversation analysis as complete, pending, skipped, or unavailable. **Open conversation** opens the imported conversation. Analysis describes the current record, not a specific receipt revision.

The recent-receipts response includes `recordId`, `analysisStatus`, and `url`. Read one intake receipt with `GET /v0/sources/{sourceId}/receipts/{receiptId}`. This single-receipt response does not include analysis status.

Sift uses its existing analysis pipeline for imported content, including generated titles, summaries, and sentiment. Processing takes time. If Search shows no results, include the original `occurredAt` date in its range.

## Batches, retries, and corrections [#batches-retries-and-corrections]

Send `{"items": [...]}` to `POST /v0/sources/{sourceId}/items/batch`. Use distinct item IDs. Each item has a 100 KiB limit and text supports up to 64,000 characters. Custom field and metadata objects allow 64 keys and six nested levels. Split batches that exceed 100 items or 1 MiB.

If an import times out, resend the same item ID and content. An exact retry returns the original receipt. Changed content under the same ID conflicts. Retry failed intake with `POST /v0/sources/{sourceId}/receipts/{receiptId}/retry`. Pending or completed work cannot be retried this way.

To correct content, send `PATCH /v0/sources/{sourceId}/items/{externalId}`:

```json
{
  "expectedRevision": 1,
  "requestId": "7192cdcb-c9d4-4cf4-8c37-d09d8a2ff715",
  "text": "Please add CSV export with dates."
}
```

Use the latest receipt revision and a new UUID for each correction. For a network retry, reuse the same UUID and body. Corrections can change text, title, item custom fields, or metadata. Identity, original time, author, channel, and thread stay unchanged. Custom field and metadata objects replace the corresponding objects; include values you want to retain.

## Troubleshooting [#troubleshooting]

| Problem | Action |
| --- | --- |
| Invalid or expired key | Check expiry and revocation. Replace the key if needed. |
| Missing scope | Use an Import key for intake and receipts. |
| Source not found | Check the source ID and the key's allowed sources. |
| Invalid item or unknown channel | Check required fields, JSON types, channels, and field mappings. Validate again. |
| Conflict | Keep retry content unchanged. Use the latest revision for a correction. |
| Source paused or archived | Resume a paused source before importing. Archived sources block new items. |
| Analysis pending or unavailable | Refresh Activity. If the state persists, contact your administrator. |

Pause blocks new imports and corrections. Archive retains stored conversations and receipts.

## Tips [#tips]

- Keep item and author IDs stable. URL-encode IDs in route paths.
- Save request bodies and receipt IDs so you can retry safely.
- Do not put keys in browser code, logs, or source control.
- Use the original timestamp rather than the time you send the request.

## Related [#related]

<Cards>
  <Card href="/docs/developer/api-keys" title="API Keys">Choose permissions and manage expiry.</Card>
  <Card href="/docs/developer/api" title="API">Explore Sift's REST API.</Card>
  <Card href="/docs/integrations/custom-form" title="Custom Form">Collect customer content with a hosted form.</Card>
</Cards>
