Zendesk to BigQuery

Explore answers support questions. Zendesk in BigQuery answers business questions, the ones that need tickets sitting next to revenue, product usage and churn.

Why bother at all

Be honest about the reason first, because the wrong reason leads to an expensive pipeline nobody queries.

Good reasons: joining ticket volume to account revenue, correlating support contacts with churn, feeding a company-wide dashboard that already lives in BigQuery, or retaining history longer than your helpdesk keeps it conveniently queryable.

Bad reason: a report Explore already produces. If the question is "what is our first response time by group", Explore answers it in five minutes and the warehouse answers it in three weeks.

Four routes from Zendesk into BigQuery

Availability and naming change over time, so check the current Zendesk documentation for what your plan includes.

RouteEffortFreshnessBest when
Managed data export from ZendeskLowBatchIt is on your plan and the schema fits
Third party ELT connectorLow to mediumHourly or dailyYou already run Fivetran, Airbyte or similar
Incremental export API plus your own loaderHighAs often as you scheduleYou need control, or the connector schema is wrong for you
Explore scheduled CSV, loaded manuallyVery lowPoorA one-off analysis, never a production pipeline

Picking one

Start with whichever managed data export options your plan already includes. Free capability beats clever engineering.

If your company runs an ELT tool already, use the Zendesk connector. Fivetran, Airbyte, Stitch and the rest all have one, and they handle pagination, retries, schema drift and incremental state, which is genuinely most of the work. You pay per row or per connector and you skip a month of building.

Build it yourself when the connector schema does not carry the fields you need, when you want a specific refresh cadence, or when data residency rules mean the data can't pass through a third party. Building is a real project, not a weekend.

The incremental export pattern

If you do build it, the incremental export endpoints are the backbone. They exist precisely so you do not page through the entire ticket history every night.

# cursor based incremental ticket export
curl -s -u you@example.com/token:APITOKEN \
  "https://acme.zendesk.com/api/v2/incremental/tickets/cursor.json?start_time=1735689600"

The response carries a batch of tickets, an after_cursor and an end_of_stream flag. Loop until end_of_stream is true, persist the cursor, and start from that cursor on the next run. Store the cursor somewhere durable, because losing it means a full resync.

Two disciplines make the difference between a pipeline that runs for years and one that quietly drifts. Land the raw JSON responses in a staging table or a GCS bucket before you transform anything, so a schema surprise costs you a re-transform rather than a re-extract. And rerun a small overlap window each time, since exact boundary semantics are fiddly and a few duplicated rows are cheap when your merge key is the record id.

Rate limits are real. Check the current documented limits for the incremental endpoints and back off politely rather than retrying in a tight loop.

A schema that survives contact

The naive approach is one wide tickets table with every custom field as a column. It works for a month, then someone adds a field and your DDL is a migration.

`tickets`. One row per ticket, current state, ingestion timestamp included. Rebuilt or merged on each run.
`ticket_field_values`. Long, not wide. One row per ticket per field: ticket_id, field_id, value. New custom fields become rows, never schema changes. This one decision saves the most pain.
`ticket_metrics`. Response and resolution durations, kept separate because they update independently of the ticket itself.
`ticket_audits` or events. Append only, one row per change. This is what lets you reconstruct state at a point in time, which is the thing Explore cannot do for you.
`users` and `organizations`. Slowly changing dimensions if you care about history, plain snapshots if you do not. Decide once, up front.

Keep a field_definitions table alongside it, loaded from the ticket fields endpoint, so field_id resolves to a human title in your queries. See custom ticket fields for why ids and titles need keeping apart.

Partition the big tables by created or updated date and cluster on the ids you join. BigQuery bills by bytes scanned, and an unpartitioned audit table is the classic way to discover that.

Five things that will break it

Deletes. Records deleted in Zendesk don't vanish from your warehouse. Track deletions explicitly or your counts drift upward forever.
Custom field values are typed loosely. A drop-down value, a date and a number all arrive as strings in the same array. Cast in the transform layer, not the load layer.
Time zones. Zendesk timestamps are UTC. Your business hours are not. Store UTC, convert in the query, and never store local time.
Field renames. A drop-down option renamed in Zendesk changes what your historic rows say about the past. The field_definitions snapshot per load is your defence.
Merged tickets. Merges close the source ticket and tag it closed_by_merge. Leave those in your volume counts and every ticket-per-customer figure is inflated by the duplicate rate, which typically runs 8% to 20% of a queue. Exclude the tag from volume metrics and keep the rows for auditing.
FAQ

Frequently asked questions

Is there a Zendesk BigQuery connector, or do you build it?

Both exist. A managed Zendesk BigQuery connector from an ELT vendor is the fast route into a Zendesk data warehouse, and building it yourself means the Zendesk incremental export API on a schedule.

What is the easiest way to get Zendesk data into BigQuery?

A third party ELT connector if your company already runs one. It handles pagination, retries and incremental state, which is most of the work.

Does Zendesk have a native BigQuery export?

There are managed export options on some plans, and the exact capability has changed over time. Check the current Zendesk documentation against your subscription before building anything.

What's the incremental export API?

A set of endpoints that return records changed since a cursor or timestamp, so you sync only what moved instead of paging the full history each run.

How should I model custom fields in BigQuery?

Long, not wide. One row per ticket per field, with a separate field definitions table for titles. New custom fields then become rows rather than schema migrations.

How do I handle merged tickets in warehouse reporting?

Merges tag the closed ticket closed_by_merge. Keep the rows for auditing but exclude the tag from volume and handle-time metrics, or those numbers read high.

Warehouse numbers only as clean as the queue

Duplicates inflate every count you load. Ticket Merger removes them at the source, before they reach BigQuery.

Start free trial

14-day free trial. No credit card required.