# Instasent - ingest-api (full documentation) > Autocontained dump of every page under /ingest-api. Paste this into an AI assistant or feed it to an agent as context for ingest-api integration work. Source: https://docs.instasent.com/ OpenAPI spec: https://docs.instasent.com/openapi/ingest.openapi.yaml --- URL: https://docs.instasent.com/ingest-api/guide # Guide The Ingest API imports contacts and events into an Instasent project so every downstream surface — audiences, campaigns, analytics — sees the same source of truth. The Ingest API is the door that customer data walks through on its way into Instasent. Every contact you create, every event you record, lands in a project as a **datasource** and feeds the audience that your campaigns, segments and analytics read from. It is a focused subset of the [Product API](/product-api/guide): three write endpoints for contacts and events, plus a handful of read endpoints for validating your payloads against the datasource specs. If you already run Product API calls, the same tokens work here — see [Authentication](/ingest-api/authentication). ## When to reach for it Pick the Ingest API when an **external system of record** owns your customer data and Instasent needs a steady feed of it: - **CRM or e-commerce syncs** — push new and updated contacts as your source platform mutates them. - **Product event streams** — record purchases, sign-ups, page views or any custom interaction you want to segment on. - **Backfills** — load historical data into a fresh project before flipping campaigns on. If instead you need to **read back** an audience, trigger a campaign, or orchestrate messaging, that is the Product API's job. ## Mental model Three nouns and one rule of thumb: - **Contacts** — the people in your audience. Identified by a `_user_id` that is unique within your datasource. Sending the same `_user_id` again updates the existing record. - **Events** — time-stamped interactions tied to a contact (`purchase`, `viewed_product`, `reservation_made`, anything you define). Events are **immutable** once accepted. - **Datasource** — the logical stream your API token writes to. A project can have several datasources, and when two disagree about the same contact, the Ingest datasource wins. The rule of thumb: **treat Ingest writes as idempotent for contacts and append-only for events.** Your client should be safe to retry contact uploads; event uploads must use a stable `_event_id` so duplicates are discarded on our side. ## A day in the life of a contact #### 1. Your CRM emits a change A new customer signs up, or an existing one updates their phone number. Your integration catches the change event. #### 2. POST /stream/contacts Send the contact — `_user_id`, identity fields, any custom attributes — as a single-item array. Omit attributes you do not own; nothing you leave out gets overwritten. #### 3. Instasent merges the record The datasource is updated synchronously; the audience view is refreshed within seconds. #### 4. Later: POST /stream/events The same customer places an order. You POST a `purchase` event with `_user_id` and `_event_parameters`. The event lands on the contact's timeline and becomes queryable for segmentation. ## Shape of the API All endpoints live under `https://api.instasent.com/v1/project/{project}/datasource/{datasource}/`: | Endpoint | Purpose | | ---------------------------------- | ------------------------------------------------------------------------------------- | | `POST /stream/contacts` | Create or update up to 100 contacts per call. | | `DELETE /stream/contacts/{userId}` | Permanently remove a contact. | | `POST /stream/events` | Record up to 100 events per call. | | `GET /stream` | Stream metadata and quota — also the simplest auth probe. | | `GET /stream/specs/*` | Discover the attributes, event types and per-event parameters the datasource accepts. | Per-parameter detail lives in the [API Reference](/ingest-api/reference). ## How contacts are merged Attributes in a contact payload follow a few simple rules: - **Omitted attribute** → existing value kept. Send partial updates without fear. - **Explicit `null`** → attribute cleared. - **Same `_user_id` across datasources** → contacts merge automatically into one audience record, and Ingest values take precedence when they collide. > **Tip**: Custom attributes are first-class citizens — just send them alongside the reserved `_`-prefixed ones. Validate the names against [`/stream/specs/attributes`](/ingest-api/reference) during integration to catch typos before they create new columns. ## How events flow Two shapes for `POST /stream/events`: - **Event-only** — just `_user_id`, `_event_id`, `_event_type` and `_event_parameters`. Skipped silently if the contact does not exist. - **Event with embedded contact** — add `_user_data` to create or update the contact in the same call. Saves a round-trip when your source system can produce both at once. `_event_id` must be stable and unique per event. Resend the same id and we drop the duplicate — that is what makes safe retries possible. ## Automations and the event time window Every event you send is stored on the contact's timeline regardless of its date, but **only events whose `_event_date` is close to reception time trigger automations**. The Ingest API applies a symmetric window of **±1 hour** around the moment we receive the request: ``` abs(_event_date − reception_time) ≤ 1 hour ``` Events outside that window are accepted, persisted and queryable for segmentation — they just do not fire any automation. This is a safeguard: without it, a single backfill of historical orders, a producer with clock drift, or a queue replayed after a long stall could enqueue thousands of SMS, emails and other messages for things that already happened. In practice: - **Omit `_event_date`** when the event is happening "now" — it defaults to the current time and is guaranteed to be inside the window. - **Send `_event_date`** only when you genuinely need a specific timestamp, and make sure it is within ±1h of reception. - **Backfills** of historical events for segmentation or analytics should be sent with their real dates. Automations will not fire for them — by design. > **Warning**: A future-dated `_event_date` (for example, a producer whose clock is two hours ahead) silently skips automations. If your automations stop firing but events still land on the timeline, check the clock of whatever system is generating `_event_date`. ## What to read next - [Quickstart](/ingest-api/quickstart) - Load your first contact and event in under five minutes. - [Authentication](/ingest-api/authentication) - Datasource tokens, Product API tokens, rotation. - [Errors](/ingest-api/errors) - Status codes and the partial-success response shape. - [API Reference](/ingest-api/reference) - Every endpoint, every parameter. --- URL: https://docs.instasent.com/ingest-api/quickstart # Quickstart Load a contact and record an event in under five minutes. You will need a project, a datasource token and curl. This walkthrough ends with one contact in your datasource and one event on their timeline. Budget five minutes. ## Before you start #### 1. Have a project Sign in to the [dashboard](https://dashboard.instasent.com) and pick the project that will receive the data. If this is your first time, **create a throw-away project** — it makes cleanup trivial once you have finished exploring. #### 2. Create a datasource Inside the project, open **Datasources** → **New datasource** → **API**. Copy the `project`, `datasource` and the generated token — you will not see the token again. #### 3. Export them as env vars ```bash export INSTASENT_PROJECT="proj_xxx" export INSTASENT_DATASOURCE="ds_xxx" export INSTASENT_TOKEN="eyJhbGciOi..." export BASE="https://api.instasent.com/v1/project/$INSTASENT_PROJECT/datasource/$INSTASENT_DATASOURCE" ``` ## 1. Verify the token `GET /stream` returns metadata and is the cheapest way to confirm your credentials work. ```bash curl "$BASE/stream" \ -H "Authorization: Bearer $INSTASENT_TOKEN" ``` You should get a `200` with an object describing the organization, project, datasource and recent usage. A `401` means the token is wrong; a `404` means the project or datasource id is wrong. ## 2. Create a contact `POST /stream/contacts` takes an **array** even when you only have one record. The reserved `_`-prefixed fields are Instasent's known attributes; everything else becomes a custom attribute on the contact. ```bash curl -X POST "$BASE/stream/contacts" \ -H "Authorization: Bearer $INSTASENT_TOKEN" \ -H "Content-Type: application/json" \ -d '[ { "_user_id": "USER-123", "_first_name": "Ada", "_last_name": "Lovelace", "_email": "ada@example.com", "_phone_mobile": "+34600000000", "plan": "pro", "signup_source": "landing-a" } ]' ``` A `202 Accepted` with a per-item outcome means the contact is in flight to the audience; it will be queryable within seconds. Partial failures come back in the same body — see [Errors](/ingest-api/errors). > **Tip**: Resending the same `_user_id` updates the contact. Attributes you omit are kept; attributes you send as `null` are cleared. ## 3. Record an event Events must carry a stable `_event_id` so retries are idempotent. Pick one from your source system (order id, message id, click id…) and reuse it if you ever resend. ```bash curl -X POST "$BASE/stream/events" \ -H "Authorization: Bearer $INSTASENT_TOKEN" \ -H "Content-Type: application/json" \ -d '[ { "_user_id": "USER-123", "_event_id": "ORDER-987", "_event_type": "purchase", "_event_parameters": { "currency": "EUR", "amount": 49.90, "product_sku": "SKU-42" } } ]' ``` `202 Accepted` again. The event lands on the contact's timeline and becomes queryable for segmentation. > **Warning**: Omitting `_event_date` (as above) defaults it to "now" and lets the event trigger automations. If you pass `_event_date` yourself, it must be within **±1 hour** of reception time — historical or future-dated events are stored and queryable but will not fire automations. See [Automations and the event time window](/ingest-api/guide#automations-and-the-event-time-window). ## 4. (Optional) embed the contact in the event If your source system produces the event and the contact at the same time, you can skip the separate contacts call: ```json { "_user_id": "USER-456", "_event_id": "ORDER-988", "_event_type": "purchase", "_user_data": { "_email": "grace@example.com", "_first_name": "Grace" }, "_event_parameters": { "currency": "EUR", "amount": 12.50 } } ``` The contact is upserted before the event is processed. If the upsert fails, the event is skipped. ## 5. Delete the contact (cleanup) When you are ready to tear down your test run: ```bash curl -X DELETE "$BASE/stream/contacts/USER-123" \ -H "Authorization: Bearer $INSTASENT_TOKEN" ``` The contact is permanently removed from the audience. ## What to read next - [Guide](/ingest-api/guide) - Mental model, merge rules and event handling. - [Errors](/ingest-api/errors) - Status codes and partial-success shape. - [API Reference](/ingest-api/reference) - Every endpoint, every parameter. --- URL: https://docs.instasent.com/ingest-api/authentication # Authentication The Ingest API authenticates each request with a bearer token scoped to a datasource. Product API tokens with the right scopes also work. Every request to the Ingest API carries a bearer token in the `Authorization` header. Two flavours of token are accepted: - **Datasource token** — issued per datasource from the dashboard. Can only read and write that datasource. The right choice for CRM, e-commerce or product-event integrations that should not see anything else. - **Product API token** — a broader token that also works here as long as it carries the Ingest scopes. Convenient when a single backend drives both APIs. Scope defaults to the datasource that minted the token; there is no way to widen it at request time. ## Getting a datasource token #### 1. Open the datasource In the [dashboard](https://dashboard.instasent.com), open the project and navigate to **Datasources** → pick the API datasource you want to write to, or create a new one with type **API**. #### 2. Issue the token Under **Credentials**, click **Generate token**. Copy the value straight away — it is shown once. #### 3. Note the URL parts The datasource page also shows the `project` and `datasource` identifiers that go in the URL: ``` https://api.instasent.com/v1/project/{project}/datasource/{datasource}/stream/... ``` > **Warning**: Treat tokens as production secrets. Keep them in an environment variable or a secrets manager; never commit them to the repo or embed them in client-side code. ## Sending the token ```bash curl "https://api.instasent.com/v1/project/$PROJECT/datasource/$DATASOURCE/stream" \ -H "Authorization: Bearer $INSTASENT_TOKEN" ``` A missing, malformed or revoked token returns `401 Unauthorized`. Mismatched project/datasource ids return `404 Not Found`. ## Rotating a token Tokens do not expire. Rotate them whenever a teammate leaves, whenever a secret might have been exposed, and at least once a year as a hygiene measure. #### 1. Issue the replacement Create a new token in the dashboard **before** revoking the old one. This keeps traffic flowing while you redeploy. #### 2. Roll it out Update your secrets store and redeploy the workers that call the API. #### 3. Revoke the old token Once the replacement is live everywhere, delete the old token in the dashboard. Any request still using it will fail with `401`. ## What's next - **[Rate limits](/ingest-api/rate-limits)** — per-endpoint ceilings and the `X-RateLimit-*` headers. - **[Errors](/ingest-api/errors)** — status codes and partial-success shape. --- URL: https://docs.instasent.com/ingest-api/rate-limits # Rate limits Ingest traffic is rate-limited per datasource and per endpoint. Each response reports the current window through X-RateLimit headers, and breaching the limit returns 429. Every Ingest endpoint enforces a request-per-minute ceiling, scoped by datasource and by endpoint. Limits are sized to your subscription plan and documented per endpoint in the [API Reference](/ingest-api/reference) — if you need more throughput for a backfill or a traffic spike, open a ticket with the expected peak rate and we will raise the ceiling on your datasource. ## Reading the headers Every response includes three headers with the current window state. Log them in production — they are the cheapest way to spot a client that is about to hit the wall. ``` X-RateLimit-Limit 600 X-RateLimit-Remaining 597 X-RateLimit-Reset 1893452400 ``` | Header | Meaning | | ----------------------- | --------------------------------------------- | | `X-RateLimit-Limit` | Total requests allowed in the current window. | | `X-RateLimit-Remaining` | Requests left before the window tightens. | | `X-RateLimit-Reset` | Unix timestamp when the counter resets. | ## Batch, don't hammer Both `POST /stream/contacts` and `POST /stream/events` accept up to **100 items per call**. A single batched request counts as one hit against the rate limit, so batching is the cheapest way to move volume without tripping the ceiling. Drop to smaller batches only when your source system produces records one at a time. ## When you hit the limit Requests that exceed the window return **`429 Too Many Requests`**. The body is empty; the `X-RateLimit-Reset` header tells you when to retry. > **Tip**: Back off exponentially rather than retrying in a tight loop. A client that hammers a 429 response keeps the window full and never recovers — waiting until `X-RateLimit-Reset` resolves the situation cleanly. ## What's next - **[Errors](/ingest-api/errors)** — every status code you might receive, including `429`. - **[API Reference](/ingest-api/reference)** — per-endpoint documentation. --- URL: https://docs.instasent.com/ingest-api/errors # Errors The Ingest API uses standard HTTP status codes plus a partial-success body for batched writes. This page explains each code and how to retry safely. Every response carries a meaningful HTTP status code. A `2xx` means the batch was accepted for processing; anything else signals a problem your client should handle explicitly. Error bodies, when present, are JSON. ## Success shape Batched writes (`POST /stream/contacts`, `POST /stream/events`) return **`202 Accepted`** with a per-item outcome so partial failures do not block the rest of the batch: ```json { "entity": { "successful": 2, "failed": 1, "items": [ { "status": "success", "_user_id": "USER-1" }, { "status": "success", "_user_id": "USER-2" }, { "status": "error", "_user_id": "USER-3", "errors": { "_email": ["Invalid format"] } } ] } } ``` Inspect `items[]` before assuming the whole batch landed. Failed items are safe to retry once fixed. ## Status codes ### `202 Accepted` The batch is accepted for processing. Check the body's `items[]` for per-record outcomes — some may still have validation errors. ### `400 Bad Request` The request body is malformed or has the wrong shape (not an array, wrong types, exceeds 100 items). Not retryable — fix the payload first. ```json { "message": "Body should be an array" } ``` ### `401 Unauthorized` The token is missing, revoked or malformed. Not retryable with the same token — check the [Authentication](/ingest-api/authentication) guide. ### `404 Not Found` The `project` or `datasource` in the URL does not exist, or the token is not authorized to write to it. ### `422 Unprocessable Entity` **Every item in the batch failed validation.** The body lists per-item errors with the same shape as a partial-success `202`. Not retryable as-is — fix the items and resubmit. ### `429 Too Many Requests` You hit the rate-limit window. Retry after `X-RateLimit-Reset`. See [Rate limits](/ingest-api/rate-limits). ### `500 Internal Server Error` Something broke on our side. Retry with exponential backoff; if the failure persists, contact support with the request id. ## Retry policy > **Tip**: Retry `429` and `5xx` with exponential backoff, starting at 1 s and capping at a minute or so. For `202` with partial failures, only retry the items that came back with `status: "error"` — and use the same `_event_id` so duplicates are discarded. Everything else in the `4xx` range means the request itself is wrong. ## What's next - **[Rate limits](/ingest-api/rate-limits)** — how the `X-RateLimit-*` headers work. - **[API Reference](/ingest-api/reference)** — per-endpoint responses and schemas.