The Data 360 Ingestion API: streaming vs bulk, and how to push data in without surprises
The native connectors cover Salesforce and the big warehouses. Everything else — your billing system, an IoT feed, a custom app — comes in through the Ingestion API. Here's the two patterns, the setup nobody documents well, the token dance that 401s your first call, and the latency and credit math that catch teams out.
Most of what you read about getting data into Data 360 is about the easy 80%: click the Salesforce CRM connector, click the Snowflake or S3 connector, map the objects, done. Those are genuinely good — pre-built, pre-mapped, low-effort. But the moment your source is a billing platform, a homegrown app, a device fleet, or anything that doesn’t have a native connector, you fall off that path and land on the one API that does the rest: the Ingestion API. It’s the general-purpose, server-to-server door into Data 360, and it’s where the interesting decisions — and the interesting failures — live.
This is a build-it-yourself channel, which is exactly why it rewards understanding before you write the first line. You define the schema, you choose streaming or bulk, you handle the two-step authentication, and you own the plumbing that the native connectors hide. Get those choices right and you get a clean, current, cost-predictable feed. Get them wrong and you get 401s on the first call, ghost records that never delete, a “streaming” pipeline that’s three minutes behind, and a credit bill 2.5x what it needed to be. This post is the map: the two ingestion patterns and when each is correct, the setup end to end, the auth flow, and the latency and credit gotchas that aren’t on the quickstart page.
A naming note first, because it will confuse you otherwise. Salesforce rebranded Data Cloud to Data 360, and the docs now say “Data 360 Integration Guide.” But the wire protocol didn’t rename: the API paths still say data-cloud, the tenant domain is still *.c360a.salesforce.com, and the OAuth grant is still urn:salesforce:grant-type:external:cdp. Brand is Data 360; the plumbing still answers to “data cloud.” Don’t let the mismatch make you think you’re reading the wrong docs.
One connector, two patterns
The first thing to internalize is that the Ingestion API is a single RESTful interface with two interaction patterns — streaming and bulk — and one connector serves both. You don’t pick “the streaming connector” or “the bulk connector.” You stand up one Ingestion API connector against a schema, and then you choose per workload which pattern to call.
- Streaming accepts incremental updates as your source captures them. It’s a fire-and-forget pattern: you
POSTa small batch of records, get an acknowledgement, and Data 360 synchronizes those micro-batches asynchronously. Salesforce documents the processing as running roughly every three minutes, and payloads are capped at under 200 KB per request. Reach for streaming when the value of the data decays quickly — engagement events, status changes, telemetry — and you want the profile continuously fresh. - Bulk accepts CSV files and is built for volume that moves on a schedule: nightly syncs, weekly reconciliations, historical backfills. Instead of dribbling records in, you create a job, upload data to it, and close it for processing.
Streaming and bulk are not “real-time” and “not-real-time.” They’re “small and continuous” versus “large and periodic.” Choosing between them is a data-freshness decision and a cost decision — and the two don’t always point the same way.
The reason the distinction matters beyond ergonomics is that it maps directly onto the same batch-versus-streaming credit split we cover in the credit optimization playbook: the streaming data pipeline meters at roughly 2.5x the batch rate per row. A feed that doesn’t genuinely need three-minute freshness but gets built as streaming “to be safe” is one of the most common quiet overspends in Data 360. Decide freshness first, then pattern.
Setting up the connector and — the part everyone underestimates — the schema
Before any data moves, you create the connector and hand Data 360 a schema. In Setup, go to Data Cloud Setup → Salesforce Integrations → Ingestion API → New, and name your source. That name becomes part of every endpoint you’ll call, so pick something stable.
Then you upload a schema file, and this is where first-timers lose an afternoon. The schema is an OpenAPI 3.0 document (a .yml/.yaml file), and Data 360 enforces real constraints on it. As documented, each schema needs at least one object; each object at least one field; no nested objects; a ceiling around 1000 fields per object; object names up to 79 characters and field names up to 39, restricted to a-z A-Z 0-9 _ - (no Unicode); and strict ISO 8601 for temporal fields — yyyy-MM-dd for dates and the UTC “Zulu” form yyyy-MM-dd'T'HH:mm:ss.SSS'Z' for datetimes. Confirm the exact current limits in the live docs before you finalize a wide schema, because these are the kind of numbers that shift between releases.
Here’s a minimal, valid schema for a telemetry feed:
openapi: 3.0.3
info:
title: solar_telemetry
version: 1.0.0
components:
schemas:
solar_reading: # object name ≤ 79 chars, [a-zA-Z0-9_-]
type: object
properties:
eventId: # field name ≤ 39 chars
type: string
deviceId:
type: string
batteryLevel:
type: number
eventTime: # DateTime → ISO 8601 UTC Zulu
type: string
format: date-time
The trap that bites hardest: you can’t remove an object from a schema after it’s uploaded. The schema is close to a one-way door. Model the object set deliberately up front, because a rushed schema leaves you with dead objects you can’t cleanly delete — the API equivalent of a table you’re stuck with forever. (The friction of hand-authoring these YAML files is real enough that practitioners have built utilities just to generate them from existing object definitions.)
With the schema uploaded, you create a data stream from the connector: pick the schema objects, and for each set a primary key and a category — Profile, Engagement, or Other. The primary key is load-bearing (more on that under upsert behavior). Finally, the connector’s detail page exposes your tenant-specific ingestion endpoint and lets you download the object endpoints as a file — that’s the reference you build your API calls from, rather than guessing the paths.
Where does the data land? Ingested records arrive in a Data Lake Object (DLO), the raw landing zone, and only become business-ready once you map the DLO to a Data Model Object (DMO). If that DLO/DMO split is new to you, we walk through it in data lake vs data model objects — it’s the difference between “data is technically in Data 360” and “data an agent or a segment can actually use.”
The auth flow that 401s your first call
Almost everyone’s first Ingestion API request fails with a 401, and almost always for the same reason: they used the wrong token. Data 360 uses a two-step token exchange, and skipping the second step is the classic mistake.
- Get a core access token. Authenticate to Salesforce through a connected app (OAuth). The connected app needs Enable OAuth Settings and the right scopes —
apiis required for the exchange itself, alongside the Data Cloud scopes (cdp_ingest_api,cdp_query_api,cdp_profile_api, plusrefresh_tokenas needed). - Exchange it for a Data 360 token.
POSTto/services/a360/tokenon your My Domain withgrant_type=urn:salesforce:grant-type:external:cdp, passing the core token as thesubject_token. What comes back is a Data Cloud access token and aDATA_CLOUD_INSTANCE_URL— your tenant domain, in the*.c360a.salesforce.compattern.
POST https://<my-domain>.my.salesforce.com/services/a360/token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:salesforce:grant-type:external:cdp
&subject_token=<CORE_ACCESS_TOKEN>
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
Every Ingestion API call then uses that exchanged token against that tenant URL — not the core token, not your My Domain. Fire the ingest call with the core token or at the core instance and you get the 401 that eats a debugging session. It’s the same “which credential, which host” discipline that any integration pattern on the platform demands; the Ingestion API is just less forgiving about it than most.
Streaming, concretely
A streaming insert is a POST to your source-and-object endpoint, with records wrapped in a data array. A successful call returns 202 Accepted — an acknowledgement that Data 360 received the batch, not that it’s queryable yet.
POST https://<tenant>.c360a.salesforce.com/api/v1/ingest/sources/solar_telemetry/solar_reading
Authorization: Bearer <DATA_CLOUD_ACCESS_TOKEN>
Content-Type: application/json
{
"data": [
{
"eventId": "SOLAR_PANEL_001-2026-07-22T10:30:00.000Z",
"deviceId": "SOLAR_PANEL_001",
"batteryLevel": 87,
"eventTime": "2026-07-22T10:30:00.000Z"
}
]
}
Two things to hold in your head. First, “fire and forget” is literal: the 202 means received, and processing happens on that roughly-three-minute cadence afterward. Second, keep each request under the ~200 KB cap — batch your records into requests sized to fit, rather than sending one enormous payload.
Deletes over streaming use a DELETE on the same path, passing primary-key values, and are capped at a small batch (documented around 200 records per delete request — go to bulk for more). Formatting those primary-key IDs exactly right is the usual snag, so a common pattern is a delete payload containing only the primary-key field.
Bulk, concretely
Bulk is a job you open, fill, and close. The native Data 360 endpoints live under your c360a tenant URL — don’t confuse them with core Bulk API 2.0 (/services/data/...), which is a different product for CRM records entirely.
# 1. Create the job
POST https://<tenant>.c360a.salesforce.com/api/v1/ingest/jobs
{ "object": "solar_reading", "sourceName": "solar_telemetry", "operation": "upsert" }
# → { "id": "<jobId>", "state": "Open" }
# 2. Upload CSV (RFC 4180, UTF-8, comma-delimited; ≤ 150 MB/file, ≤ 100 files/job)
PUT https://<tenant>.c360a.salesforce.com/api/v1/ingest/jobs/<jobId>/batches
Content-Type: text/csv
eventId,deviceId,batteryLevel,eventTime
SOLAR_PANEL_001-...,SOLAR_PANEL_001,87,2026-07-22T10:30:00.000Z
# 3. Close it → data is enqueued for processing
PATCH https://<tenant>.c360a.salesforce.com/api/v1/ingest/jobs/<jobId>
{ "state": "UploadComplete" }
# state moves UploadComplete → JobComplete | Failed
The lifecycle is the whole thing: create → upload → close, then watch the state transition to JobComplete or Failed. Nothing processes until you close the job with UploadComplete; a job left Open sits there holding your data and doing nothing, which is a surprisingly common “why isn’t my data showing up” cause. CSV files are constrained (documented around 150 MB per file, up to 100 files per job, strict RFC 4180 with comma delimiters and UTF-8) — verify the current caps before you architect a giant backfill, and slice accordingly.
The two latencies, the upsert, and the ghost records
Three behaviors surprise people enough to be worth stating flatly.
Ingestion is eventually consistent, and the delay stacks. After streaming or bulk, there’s the pattern’s processing time (streaming’s ~3-minute cadence; bulk’s job processing) plus a documented cache refresh — allow a minimum on the order of 30 seconds — before the data is actually queryable. So “streaming” is not “instant”: you push, wait for the micro-batch, then wait for caches. Teams who expect sub-second and start debugging a “broken” pipeline are usually just watching normal latency. (If you genuinely need sub-second, that’s the real-time layer, below — not standard ingestion.)
Upsert keys on your primary key. With operation: upsert, a matching primary key updates the existing record and a non-match inserts a new one. This is idempotent and forgiving — resending the same record is safe.
But upsert does not reconcile deletions. This is the one that quietly corrupts a profile base. If a record disappears from your source system, it does not disappear from Data 360 — the row persists until you explicitly delete it through the API. Feeds built as “upsert everything nightly” accumulate ghost records: customers who churned, test rows, cancelled orders, all still resolving into unified profiles months later. If your source can go backwards (deletes, not just inserts and updates), you need an explicit delete strategy — a DELETE stream or a bulk delete job — not just an upsert.
How the Ingestion API differs from the other doors
Because Data 360 has several ways in, it’s worth being crisp about when the Ingestion API is the right one rather than a reflex:
- vs. the native connectors (CRM, S3, Snowflake). Those are pre-built and pre-mapped for known systems — near-zero schema work. The Ingestion API is for the systems that don’t have a connector, and the price of that generality is that you author the schema and own the calls. If a native connector exists for your source, use it; the Ingestion API is the fallback, not the default.
- vs. zero-copy. Zero-copy federates — the data stays in the warehouse and Data 360 queries it in place. The Ingestion API ingests — it physically copies rows into Data 360. Different trade-offs on freshness, cost, and where the source of truth lives; a real architecture often uses both, for different data.
- vs. the Web/Mobile SDK. The SDKs are client-side instrumentation that auto-capture browser and app engagement events. The Ingestion API is server-to-server, for structured records your backend already holds. No page tag, but you own the pipeline.
- vs. Change Data Capture. CDC watches a source database’s change log and streams row-level changes automatically. The Ingestion API is push, not capture: your system decides what to send and when, and formats it. A common production shape is CDC or Platform Events feeding the Ingestion API.
Credits, and the Summer ‘26 real-time twist
Ingestion meters. On the official rate card, external ingestion runs about 2,000 credits per million rows in batch and 5,000 in streaming — the 2.5x premium that makes an unnecessarily-streaming feed an expensive default. One meaningful softening: since 2025, ingesting data that originates in Salesforce’s own products is free on the data pipeline meter, so first-party data arriving through a paid external route is often re-routable to a free native one. If ingestion is a big line on your bill, that’s the first thing to check — the mechanics are in our credit optimization playbook.
The newer capability worth knowing: real-time ingestion. An existing Ingestion API integration can feed Data 360’s real-time layer — powering real-time identity resolution, insights, segmentation, and actions with the sub-second freshness (Salesforce documents roughly 95% of events completing end-to-end within about 500 ms) that standard ingestion’s latency can’t touch. The catch is architectural: it applies when the DLO’s mapped DMO is a member of a real-time data graph, and it expects the ingested object to carry a foreign-key qualifier mapped to that graph’s primary DMO. It’s the escape hatch from the “streaming isn’t instant” latency — but it’s a deliberate design you opt into, not a default you get for free.
What to actually do
If a native connector covers your source, use it and stop reading. If it doesn’t, the Ingestion API is your door, and the sequence that avoids the common wounds is: model the schema carefully before you upload it (you can’t remove objects later); wire the two-step token exchange and always call with the exchanged Data 360 token against the c360a tenant URL; pick streaming for genuinely time-sensitive small updates and bulk for scheduled volume, deciding on real freshness needs rather than defaulting to streaming and paying 2.5x; expect the stacked latency and don’t debug normal eventual consistency as a bug; and design a delete strategy so upsert-only feeds don’t silently fill your profiles with ghosts. Getting data in is the unglamorous foundation under every segment, insight, and grounded agent you’ll build on top — and a foundation that’s late, duplicated, or over-billed is one you’ll be paying for long after go-live.
If your stack is a tangle of systems with no clean path into Salesforce, that’s exactly the integration and Data 360 work we do — building feeds that are current, deduplicated, and observable rather than brittle.
Understanding the basics
What is the Salesforce Data 360 Ingestion API?
It’s the RESTful, server-to-server API for loading data into Data 360 (formerly Data Cloud) from systems that don’t have a native connector — a billing platform, a custom app, an IoT feed. A single Ingestion API connector supports two patterns: streaming, for small incremental updates synchronized as fire-and-forget micro-batches (processed roughly every three minutes, payloads under ~200 KB), and bulk, for CSV files loaded periodically through a create-upload-close job lifecycle. Ingested data lands in a Data Lake Object and becomes usable once mapped to a Data Model Object.
What’s the difference between streaming and bulk ingestion in Data 360?
Streaming is small and continuous: you POST micro-batches of records and Data 360 processes them asynchronously on a roughly three-minute cadence, good for time-sensitive data you want kept fresh. Bulk is large and periodic: you create a job, upload CSV files to it, and close it for processing, good for scheduled syncs and backfills. Beyond ergonomics, the streaming data pipeline meters at about 2.5x the batch rate per row, so a feed that doesn’t truly need near-real-time freshness should usually run as bulk to control credit consumption.
Why does my Data 360 Ingestion API call return a 401?
Almost always because you’re using the wrong token. Data 360 requires a two-step exchange: authenticate through a connected app to get a core access token, then exchange it at /services/a360/token (grant type urn:salesforce:grant-type:external:cdp) for a Data Cloud access token and your tenant instance URL. Every ingest call must use that exchanged token against the *.c360a.salesforce.com tenant URL — not the original core token and not your My Domain host. Also confirm the connected app has the api scope plus the Data Cloud ingest and query scopes enabled.
Standing up a feed into Data 360 and fighting 401s, ghost records, or a credit bill that outgrew the data? Talk to us — building ingestion that’s current, deduplicated, and cost-aware is a normal week for our team.
Keep reading
All insights
Apache Iceberg in Data 360: file federation, the REST catalog, and the open-table bridge zero copy was missing
Keyword, vector, or hybrid: choosing the Data 360 search index that actually grounds your agent
Salesforce Data 360 vs. Microsoft Fabric: OneLake, the zero-copy bridge, and the job each one is actually for