Data streams

Feed new leads into campaigns as they arrive, from a webhook, your backend, a Google Sheet, HubSpot, Salesforce, Zoho CRM or Shopify.

A data stream is a live source of contacts. Attach it to one or more campaigns, and every new contact on the stream is added to each campaign’s calling list and dialled within the campaign’s calling window. Use one instead of uploading a CSV when leads keep arriving: website forms, ad leads, CRM sign-ups.

Streams belong to the workspace. One stream can feed several campaigns, and each campaign maps the stream’s fields to its own agent’s variables.

Kinds

kind How contacts arrive
webhook Anything that can send an HTTP POST (a form, Zapier, Make, your backend) posts to the stream’s private URL.
api Your backend pushes records with an API key: POST /v1/data-streams/{id}/records.
google_sheet New rows added to a Google Sheet. Checked about every 2 minutes.
hubspot Contacts created in HubSpot after the stream was created. Checked about every 2 minutes.
salesforce Leads (or Contacts) created in Salesforce after the stream was created. Checked about every 2 minutes.
zoho Leads (or Contacts) created in Zoho CRM after the stream was created. Checked about every 2 minutes.
shopify Customers created in Shopify after the stream was created. Checked about every 2 minutes.

Sheet and CRM streams read through an account connected in Integrations; pass its id as integrationId. HubSpot, Salesforce, Zoho and Shopify streams need CRM integrations on your plan (403 plan_feature_locked otherwise); if the plan changes later, the stream stops with an error until it’s restored.

Google Sheet rows are identified by their content, not their row number, so deleting or inserting rows above the ones already read never makes the stream skip new rows. Two identical rows count as one.

How a record becomes a call

  1. Phone. The stream’s fieldMap.phone field, or, when unset, the first field named phone, mobile, phone_number, mobilephone (and similar). Local numbers are read in config.countryCode (default IN). A record with no valid phone is logged as invalid and not dialled.
  2. Name. fieldMap.name, or a name / full_name field, or first_name + last_name.
  3. Variables. Each attached campaign maps its agent’s variables to stream fields (variableMapping). Nested JSON is flattened with dots, so {"lead": {"city": "Pune"}} is the field lead.city.
  4. Name script. Attach with "transliterate": true to write each name in the campaign agent’s language (for example Devanagari for a Hindi agent) so it’s pronounced correctly.
  5. Once only. A record is identified by fieldMap.externalId (or the sheet row / CRM record id, or else the phone). Sending the same record again is a no-op, so retries are safe. A number already on a campaign’s list is not added twice.

Contacts are queued on every attached campaign that is a draft, active or paused. Records that arrived before a campaign was attached are only queued on it if you attach with backfillDays. A finished, archived or past-end-date campaign gets nothing. Do-not-call lists, calling windows and frequency caps apply at dial time, exactly as for a CSV list.

A stream-fed campaign does not complete when its list runs out. It waits for the next contact, and closes on its end date (or when you complete it). It can be published before its first contact arrives.

Webhook ingest

POST /ingest/streams/{id}

Authenticate with the stream token (dsk_…, shown once when the stream is created or its token is rotated):

curl -X POST 'https://api.oyehello.ai/ingest/streams/dst_…' \
  -H 'Authorization: Bearer dsk_…' \
  -H 'Content-Type: application/json' \
  -d '{"name": "Asha Verma", "phone": "+919876543210", "city": "Pune"}'

Tools that can’t set headers can pass the token as ?token=dsk_… instead.

The body is one JSON object, an array of up to 500 objects, or { "records": [ … ] } (5 MB max). The response is 202:

{
  "data": {
    "received": 2, "accepted": 1, "invalid": 1, "duplicate": 0, "queued": 1,
    "results": [
      { "index": 0, "status": "accepted", "recordId": "dsr_…" },
      { "index": 1, "status": "invalid", "recordId": "dsr_…", "reason": "invalid phone number" }
    ]
  }
}

queued counts the calling-list rows added across every attached campaign. Errors: 401 bad token, 403 workspace suspended, 409 stream_paused, 400 body isn’t JSON objects or has more than 500, 429 rate_limited above 600 requests a minute per stream or 3,000 a minute across a workspace’s streams (and after repeated bad tokens from one IP). Objects nested more than 8 levels deep are kept as JSON text; a record keeps at most 100 fields.

API

These need streams:read (the GETs) or streams:write (everything else). Responses use the { "data": … } envelope. Test-mode keys can read streams but not change them or push records.

POST /v1/data-streams

{
  "name": "Website enquiries",
  "kind": "webhook",
  "fieldMap": { "phone": "contact.mobile", "externalId": "lead_id" },
  "config": { "countryCode": "IN" }
}

Returns the stream, with token set for a webhook stream (only this once) and ingestUrl. Per-kind config:

  • google_sheet: spreadsheetId (required), sheetName (default: first tab), includeExisting (default false: only rows added from now on). Row 1 must be the headings.
  • hubspot: lifecycleStage (optional, e.g. lead).
  • salesforce: object: Lead (default) or Contact.
  • zoho: module: Leads (default) or Contacts.

GET /v1/data-streams

Lists the workspace’s streams, newest first.

GET /v1/data-streams/{id}

One stream, with the campaigns it feeds. status is active, paused or error (a Sheet or CRM stream that failed 5 checks in a row; lastError says why).

PATCH /v1/data-streams/{id}

Change name, fieldMap, config, or status (active / paused). Setting active on an errored stream clears the error and checks it again right away. Pointing a Sheet or CRM stream at a different source (spreadsheet, tab, object, lifecycle stage) restarts it from that source’s current end. config is validated per kind (unknown keys are dropped; countryCode must be a 2-letter code).

DELETE /v1/data-streams/{id}

Detaches it from every campaign. Contacts it already added stay on their lists.

POST /v1/data-streams/{id}/records

Push records into an api (or webhook) stream. Same body, limits and response as the webhook.

GET /v1/data-streams/{id}/records

What the stream received, newest first (limit up to 200; pass the response’s nextBefore as before for the next page).

PUT /v1/campaigns/{id}/streams/{streamId}

Attach a stream to a campaign (or change its mapping):

{ "variableMapping": { "order_amount": "order.amount", "due_date": "due" }, "backfillDays": 7, "transliterate": false }

backfillDays (0 to 90, default 0) also queues the valid records the stream received in that many past days; the response’s backfilled says how many were added. Attaching and detaching need both streams:write and campaigns:write.

If the campaign’s agent requires variables that the mapping doesn’t cover, the request fails 400 missing_required_variables with meta.missing. A campaign with a stream attached can’t have its contacts replaced (409 stream_attached), only added to.

DELETE /v1/campaigns/{id}/streams/{streamId}

Detach a stream from a campaign. Contacts it already queued stay.

GET /v1/campaigns/{id}/streams

The streams attached to a campaign, with each one’s variableMapping.

Events

stream.record_rejected fires for each received record that can’t be dialled (for example, a bad phone number). See Webhooks & events.