When to use one
Reach for a custom connector when:- You want to manage the connection yourself. Stand up your own pipeline against any source — including ones we support natively — and forward the same first-party event shapes (
email,page,message, …) to Adapter via/ingest/typed. Downstream processing — entity extraction, relationship resolution, the whole pipeline — runs identically. - The source isn’t on the supported providers list. Internal tools, legacy systems, anything you can hit with HTTPS — push it as a typed kind if it fits one of our shapes, otherwise as
custom. - You want to mix typed first-party data with arbitrary records Adapter doesn’t otherwise see — system logs, ticket metadata, audit events. One connector can emit any combination of kinds it’s configured to accept.
Creating a connector
Open the Console
Go to Connectors in app.adapter.com and switch to the Custom tab. Click Create connector.
Name it
Give it a display name and a source slug (auto-derived from the name, lowercase + hyphens). The slug is immutable after creation — it shows up in the event source (
custom:your-slug) and the storage path. Pick something descriptive.Pick accepted kinds
Select which first-party event types this connector may emit (
email, page, message, calendar, issue, or generic custom). Selecting none means all kinds are accepted. Anything outside the list is rejected with a 400.Pushing data
Each connector exposes four ingest endpoints under/v1/custom-connectors/{connector_id}. All require Authorization: Bearer pk_live_….
Typed events (recommended)
Use the typed path when your data fits one of Adapter’s first-party shapes (email, page, etc.). Adapter applies the same processing as native connectors — entity extraction, relationship resolution, the works.
external_ids list — useful if you want to look up the resulting document later.
Plain text and arbitrary JSON
When your data doesn’t fit a typed shape — free-text notes, log lines, custom records from a tool that doesn’t have an Adapter integration — use the generic/ingest endpoint. It stores the data field verbatim as a StandardCustom event with event_type: "adapter.data.custom".
Free text:
data field accepts any JSON value — it’s stored as-is. There’s no required shape, but using a stable key (e.g. text, body, content) makes downstream queries easier.
Custom record with structure:
LLM enrichment does not run on
/ingest (JSON-only). If you have a long blob of text and want Adapter to extract summary/key-facts from it, save it to a file and post via /ingest/upload — the enricher picks up text-based documents, PDFs, images, and Office docs.Binary uploads
For files (PDFs, images, Office docs, and text-based documents), use the multipart upload endpoint:enriched_content block with summary, extracted text, and key facts. Use /ingest/upload (without /typed/) for binaries that don’t map to a typed kind.
Typed event shapes
Thepayload you send to /ingest/typed is validated against the corresponding type schema. Fields like source, user_id, container_id, and event_type are filled in automatically from the connector — you only supply the fields below. external_id is optional everywhere; Adapter generates one if you omit it. timestamp (ISO 8601) is also optional and defaults to ingest time.
email
| Field | Type | Required | Notes |
|---|---|---|---|
sender | string | ✓ | Display name or address. |
subject | string | ✓ | |
body_snippet | string | First few hundred chars of the body. Larger content goes through enrichment if attached as a binary. |
page
For documents — Notion pages, Confluence pages, internal wikis, PDFs uploaded as kind=page.
| Field | Type | Required | Notes |
|---|---|---|---|
title | string | ✓ | |
content_snippet | string | ||
url | string | Canonical link back to the source. | |
parent_id | string | Parent page/folder identifier. | |
parent_type | string | e.g. "folder", "page". | |
created_by | string | ||
last_edited_by | string | ||
last_edited_at | datetime | ISO 8601. |
message
For chat / Slack-like messages.
| Field | Type | Required | Notes |
|---|---|---|---|
channel_id | string | ✓ | |
sender | string | ✓ | |
text | string | ✓ | |
is_direct | boolean | ✓ | true for DMs, false for channels. |
thread_ts | string | Thread parent timestamp, if replying. |
calendar
| Field | Type | Required | Notes |
|---|---|---|---|
title | string | ✓ | |
start_time | datetime | Use for timed events. | |
end_time | datetime | ||
start_date | date | Use for all-day events. | |
end_date | date |
_time pair (timed) or the _date pair (all-day), not both.
issue
For tickets — Linear, Jira, GitHub issues, internal tracker rows.
| Field | Type | Required | Notes |
|---|---|---|---|
title | string | ✓ | |
number | integer | Display number (e.g. ENG-1234 → 1234). | |
description | string | ||
status | string | Free-form (e.g. "open", "in-progress"). | |
assignee | string | ||
priority | integer | ||
labels | array | List of label strings. | |
url | string | ||
action | string | One of "create", "update", "remove". |
custom
Escape hatch for data that doesn’t fit any typed shape. The whole data payload is stored verbatim.
| Field | Type | Required | Notes |
|---|---|---|---|
data | object | Any JSON. |
custom via the simpler /ingest endpoint (no kind field required) — see Generic events above.
Event metadata
Every event carries ametadata field — a flat string-to-string key/value map. Set it in two places; the two get merged at ingest time.
Connector-level metadata
Setmetadata on the connector to apply it to every event the connector emits. Useful for source-environment markers (env, region, tenant), data lineage (pipeline, version), or anything that’s constant for the connector’s lifetime.
PATCH /v1/custom-connectors/{connector_id} — changes apply to events ingested after the patch lands. Previously ingested events keep the metadata they had at the time.
Per-item metadata
Setmetadata on an individual ingest item to apply it to that event only. Useful for per-batch markers, request-correlation IDs, or anything that varies between calls.
/ingest and /ingest/typed.
Shape
Keys and values are strings. Non-string values are rejected at the API boundary. If you want to express a label-like marker without a meaningful value, use the key as the label and"true" as the value (e.g. { "urgent": "true" }). This shape keeps the field cleanly queryable once retrieval-side filtering ships.
Merge rules
When both are present, per-item metadata overrides connector metadata on key conflict. Disjoint keys merge.Connector metadata | Item metadata | Resulting event.metadata |
|---|---|---|
{ "env": "staging" } | { "batch": "b1" } | { "env": "staging", "batch": "b1" } |
{ "env": "staging", "team": "growth" } | { "env": "prod" } | { "env": "prod", "team": "growth" } |
| absent | absent | absent (no metadata field) |
Where metadata is visible
The merged map lands onevent.metadata — a top-level field on every event. That matters because the event’s raw blob is stripped before delivery to subscribers and trigger routing, but metadata is not. Practically:
- Trigger rules and webhook subscribers see
metadataon the payload they receive. - Stored canonical documents (in object storage) carry the full
metadata. - Retrieval via
/v1/knowledge/searchand/v1/knowledge/ask— pass ametadataobject to restrict results to evidence carrying ALL the given key/value pairs (AND-of-exact-match). Max 20 keys, string values only. The same filter applies to/ask: it scopes every search the agent runs and rejects out-of-scope document reads, so the answer can only be grounded in matching evidence.
Looking at ingested data
Each connector card in the console has a View Data button that expands to show document counts per resource. Counts update as data lands; if they ever drift, the recovery path is to re-ingest or contact support.Tips
external_idis optional — Adapter generates one if you omit it. Provide your own when you want idempotent re-ingest (re-posting the sameexternal_idoverwrites the document).- API key scope:
pk_live_…keys are tenant-scoped — one key works against any connector in your container. Rotate by creating a new key and revoking the old. - Deactivation stops new ingest but retains existing data.
What’s next
API reference
Full schema for the ingest endpoints.
Evidence types
The shape of typed first-party events.