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 that 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
1
Open the Console
Go to Connectors in mind.adapter.com and switch to the Custom tab. Click Create connector.
2
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), the storage path, and the name of the table your records land in. Use lowercase letters, digits, _ and -, up to 48 characters. Each slug can be used once per workspace, and - and _ count as the same character.3
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.4
Save the connector ID
The console shows the
connector_id. Copy it — you’ll need it in the ingest endpoint paths below.5
Create an API key
Create an API key under Settings → API keys and store it securely — the secret is shown only once. See API keys for the full flow. Use this
pk_live_… key as the bearer token when calling the ingest endpoints.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.
Generic events
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.Realtime vs bulk
Each pushed item is scheduled on one of two lanes. Fresh items ride the realtime lane, which fires triggers as they are processed. Two kinds of push are moved to the bulk lane automatically, where they are processed on their own queue without delaying other tenants’ realtime data:- Items whose
timestampis more than 7 days old. History is loaded in bulk, and no trigger fires for it. - Any push once a connector has sent more than 2,000 items in the trailing 10 minutes. Chunking a corpus into 500-item requests is still the right shape; the lane just changes.
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.
Delivering a file
A file’s metadata and its bytes travel on different routes. The JSON evidence route (POST /v1/knowledge/connectors/{connector_id}/evidence, and /ingest/typed) carries a file record’s metadata only; it has no field for bytes. To deliver the bytes, upload them here with kind=file and the record’s fields in payload:
file record that names them: storage_uri, size_bytes, and a SHA-256 file_hash computed from the uploaded bytes. mime_type, filename and original_filename default to the uploaded part’s content type and filename unless payload sets them. Adapter then extracts the text (PDF, .docx, .pptx, spreadsheets, CSV/TSV and plain text, Markdown or JSON) and indexes it as the file’s content. storage_uri, size_bytes, file_hash and file_hash_algorithm describe the uploaded bytes, so a payload that sets any of them is rejected with a 400.
Adapter validates the payload before it stores anything, so a rejected upload (a 400 or a 422) stores no bytes. The bytes are stored at an address derived from their SHA-256, so each file record’s storage_uri keeps naming the bytes it was published with: uploading different bytes under the same external_id stores a second object and publishes a record that names it, and uploading the same bytes again names the same object.
Evidence types
kind also accepts any evidence type from GET /v1/knowledge/evidence-types — order, line_item, deal, contact, company, product, task, invoice and the rest of the catalogue, including provider-shaped types such as salesforce_opportunity and hubspot_deal. The payload is validated against that type’s schema and rejected with a 422 on the first mismatch or unknown field, so nothing is stored partially.
- Identity. Types with an identity in their update contract (an order’s
order_id, a deal’sdeal_id, a contact’scontact_id) are stored under the id derived from those payload fields, not underexternal_id. The response reports the stored id inexternal_ids; re-pushing the same identity follows the type’s update contract. - Dates. The item’s
timestampis the record’s own time and is what the knowledge graph dates the evidence by when the record carries nocreated_at. It defaults topayload.timestamp, then the record’screated_at, then the ingest time. Send the business date (order_date, a contract’s signed date) astimestampso time-filtered retrieval finds the record where it belongs.
Asserting facts
Content alone never creates a person. A name in an email body is a mention, not an identity, so the graph only mints a person from a structured position (a sender, an attendee, a CRM contact). When you know something the data does not say outright, send it as afact. A fact is your word: Adapter resolves the people and organizations it names against the graph, creates the ones that are missing, records the relation on each, and attaches corroborating evidence it finds by searching the statement.
Facts go through the same endpoint as every other kind. The connector needs fact in its accepted kinds, or an empty list, which accepts everything.
What happens to a fact:
- Resolution. Each party resolves by
urn, thenemailordomain, then an exact name that only one known entity goes by. A name no one is known by mints a new entity at theassertedtier. - Trust. Asserted entities and claims outrank what Adapter inferred from activity and are outranked only by a synced source record such as a CRM contact. A later enrichment pass cannot drop an asserted claim.
- Evidence. Supports on a claim are the evidence you passed, marked
caller, plus what Adapter found by searching the statement, markedsearch. Entities cite the fact and the caller’s evidence directly, so a later ask can trace the claim back to the record that justified it. - Graph. The relation appears on both entities and, between two people, as a
knowsedge carrying the stated relation. Every graph node built from the spine carriessource_tierin its metadata (l1_contact,asserted, orinferred), so a reader can separate synced records from stated facts from inference. The fact itself is searchable as evidence and every entity it touched cites it. - Withdrawal. Deleting the item retracts its claims and removes entities that existed only on its word.
Closing the loop from an ask
The common pattern: an ask surfaces someone who has no entity yet, because they never appeared as a sender, attendee, or contact. The answer is right, but there is nothing in the graph to come back to. Take the citations off that response and post them back as a fact.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
page
For documents — Notion pages, Confluence pages, internal wikis, PDFs uploaded as kind=page.
message
For chat / Slack-like messages.
calendar
Provide either the
_time pair (timed) or the _date pair (all-day), not both.
issue
For tickets — Linear, Jira, GitHub issues, internal tracker rows.
custom
Escape hatch for data that doesn’t fit any typed shape. The whole data payload is stored verbatim.
You can also send
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.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.Querying your records with SQL
Every record you push through the generic/ingest endpoint becomes a row of one table per connector in POST /v1/knowledge/query (experimental). The table is named custom_<source slug>, with - written as _, so acme-crm becomes custom_acme_crm. The connector’s table_name field gives the exact name. The table appears after the next knowledge build.
- Each top-level field of
databecomes a column, typed from its values: integer, number, boolean, date, timestamp, or text. Nested objects and arrays become JSON columns, so flatten records you want to compare field by field. source_document_idjoins the row to its document, andinvalid_fieldslists any column whose value on that row didn’t match the column’s type (that value is null).- Re-posting the same
external_idreplaces the row. - Records pushed through
/ingest/typeddon’t land here: they become their evidence type’s own records.
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.