Skip to main content
TurnellaEarly access

Automatic Data Feeds (API & CRM Connections)


What this is

Turnella can receive your contact volumes automatically. Instead of exporting a CSV from your phone system, helpdesk or CRM and uploading it by hand every week, your tools push the numbers straight into a workstream over HTTPS. Anything that can make a web request can feed Turnella: a helpdesk (Zendesk, Freshdesk, Intercom), a telephony platform, a CRM, a scheduled script, or an automation tool such as Zapier, Make or Power Automate.

Data that arrives through the API is stored exactly like a CSV upload — it appears on the Data tab, feeds the Forecast, and flows through Requirements, Schedule and Cost the same way. There is one endpoint to learn:

POST https://turnella.com/api/v1/observations

When to use the API instead of CSV upload

  • You (or a colleague) currently export and upload the same report every week — automate it once and stop.
  • You want yesterday's volumes in Turnella every morning without anyone touching it.
  • You are building a connector between your own product and Turnella.

If you upload data occasionally or are still exploring, the CSV import on the Data tab is simpler — see the Data Import chapter.


How it works — the three ideas

1. An API key identifies your account. You create a key in the app (Account → API keys). Whoever holds the key can write data to your workstreams — and only yours. Treat it like a password.

2. Data is per interval, per workstream. Each data point says: "workstream X had volume contacts in the interval starting at intervalStart" — optionally with the average handle time (ahtSeconds). Use the same interval length your workstream is set to (for a 30-minute workstream, send one entry per 30-minute slot, starting on the hour and half hour).

3. Re-sending replaces — it never duplicates and never adds up. Posting the same interval again REPLACES that interval's volume with the new number. This makes re-runs and history backfills completely safe. It also means you must send interval TOTALS, not one request per ticket — see the golden rule below.


Step 1 — Create an API key

  1. Open the app and go to Account (turnella.com/app/account).
  2. Find the API keys (advanced) section.
  3. Name the key after what will use it (e.g. "Zendesk feed") and click Create key.
  4. Copy the key immediately — it is shown once and cannot be retrieved later. Store it in your automation tool's credential store, not in a shared document.

Rules and safety:

  • Keys look like tk_live_…. Turnella stores only a hash of the key, never the key itself.
  • You can hold up to 10 active keys. Use one key per integration so you can revoke one feed without breaking the others.
  • Revoke a key any time from the same screen. A revoked key stops working immediately.
  • A key can only write observations. It cannot read your data, delete workstreams, or touch your account settings.

Step 2 — Find your workstream ID

Open the workstream you want to feed. The ID is in the browser address bar after /app/ — for example in turnella.com/app/ws_abc123/data the workstream ID is ws_abc123. Each request feeds exactly one workstream; if you feed several queues, send one request per workstream (with the same key).

Step 3 — Send the data

Make a POST request to https://turnella.com/api/v1/observations with two headers and a JSON body:

Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{
  "workstreamId": "ws_abc123",
  "observations": [
    { "intervalStart": "2026-07-01T09:00:00Z", "volume": 42, "ahtSeconds": 300 },
    { "intervalStart": "2026-07-01T09:30:00Z", "volume": 51, "ahtSeconds": 295 }
  ]
}

A success returns HTTP 200:

{ "accepted": 2, "workstreamId": "ws_abc123", "storedAt": "2026-07-01T09:32:10.000Z" }

Field rules (exact)

Field Required Rules
workstreamId yes The workstream's ID string. Must belong to the same account as the API key, otherwise you get 404.
observations yes Array with 1 to 10,000 entries per request.
intervalStart yes ISO 8601 date-time, e.g. 2026-07-01T09:00:00Z. Timezone offsets are accepted (2026-07-01T11:00:00+02:00 is the same moment) — timestamps are normalised, so equivalent times always match the same interval. Use the START of each interval, aligned to your workstream's interval length.
volume yes Number ≥ 0. The total contacts in that interval.
ahtSeconds no Number > 0 — average handle time in seconds for that interval. If you omit it on an update, any previously stored AHT for that interval is kept, not wiped.

What re-sending does (idempotency)

  • Same interval posted again → the stored volume is replaced with the new value (and AHT replaced only if you send one).
  • Intervals you do NOT include are left exactly as they were — a partial update never erases history.
  • Two identical syncs in a row produce identical data. Safe to retry on network errors.

The golden rule: send interval totals, not events

The single most common integration mistake: wiring an event trigger ("New ticket created → send to Turnella") that posts volume: 1 for the current interval on every ticket.

That does not count up. Because re-posting an interval REPLACES its value, each of those requests overwrites the interval with 1. At the end of a 40-ticket half hour, Turnella would show volume 1.

The correct pattern is always: on a schedule, ask your system "how many contacts per interval?", then post the totals. For example, every night at 02:00, query yesterday's tickets grouped into 30-minute buckets and post the 48 totals in one request.


Recipes

Recipe 0 — Prove the pipe works (2 minutes, curl)

Replace the key and workstream ID, then run:

curl -X POST https://turnella.com/api/v1/observations \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "workstreamId": "ws_abc123", "observations": [ { "intervalStart": "2026-07-01T09:00:00Z", "volume": 42, "ahtSeconds": 300 } ] }'

You should get { "accepted": 1, ... }. Open the workstream's Data tab (refresh the app) and the interval is there. To clean up afterwards, post "volume": 0 for the same interval — note this zeroes the interval (it remains stored as a real zero observation); there is no delete via the API.

Recipe 1 — Scheduled script (any CRM with an API)

The universal pattern, in pseudocode — works for Zendesk, Freshdesk, Intercom, Salesforce, HubSpot, a phone platform, or your own database:

every night at 02:00:
  rows = query CRM: count of contacts grouped by 30-min bucket, for yesterday
  observations = rows mapped to { intervalStart, volume, ahtSeconds? }
  POST https://turnella.com/api/v1/observations
    Authorization: Bearer $TURNELLA_KEY
    { workstreamId: "ws_abc123", observations }

A complete Node.js example:

// nightly-feed.js — run on a scheduler (cron, GitHub Actions, Task Scheduler)
const rows = await getYesterdayCountsFromYourCrm(); // [{ start: Date, count: n, aht: s }]
const res = await fetch("https://turnella.com/api/v1/observations", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.TURNELLA_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    workstreamId: "ws_abc123",
    observations: rows.map((r) => ({
      intervalStart: r.start.toISOString(),
      volume: r.count,
      ...(r.aht > 0 ? { ahtSeconds: r.aht } : {}),
    })),
  }),
});
if (!res.ok) throw new Error(`Turnella feed failed: ${res.status} ${await res.text()}`);

The same in Python:

import os, requests

rows = get_yesterday_counts_from_your_crm()  # [{"start": iso_string, "count": n, "aht": s}]
res = requests.post(
    "https://turnella.com/api/v1/observations",
    headers={"Authorization": f"Bearer {os.environ['TURNELLA_API_KEY']}"},
    json={
        "workstreamId": "ws_abc123",
        "observations": [
            {"intervalStart": r["start"], "volume": r["count"], **({"ahtSeconds": r["aht"]} if r.get("aht") else {})}
            for r in rows
        ],
    },
    timeout=30,
)
res.raise_for_status()

Recipe 2 — Zapier (no code)

Zapier's per-event triggers cannot aggregate, so use a scheduled Zap that fetches totals:

  1. Trigger: "Schedule by Zapier" → every day (or every hour for intraday freshness).
  2. Gather totals: add a step that returns counts per interval from your source system. Two good options: your helpdesk's "search/count" action (run one count per interval), or a "Code by Zapier" step that calls your CRM's API and groups the results into interval buckets.
  3. Action: "Webhooks by Zapier" → Custom Request:
    • Method: POST
    • URL: https://turnella.com/api/v1/observations
    • Data: the JSON body shown above (workstreamId + the observations array built in step 2)
    • Headers: Authorization: Bearer YOUR_API_KEY and Content-Type: application/json
  4. Test the Zap, then check the workstream's Data tab.

Do NOT use "New Ticket" style triggers to post volume 1 per ticket — see the golden rule.

Recipe 3 — Make (Integromat)

  1. Scheduler: set the scenario to run on a schedule (e.g. daily 02:00).
  2. Source module: your CRM/helpdesk module (e.g. "Search rows/tickets") pulling yesterday's contacts; use an aggregator to group counts per interval, or run one search per interval.
  3. HTTP module → "Make a request":
    • URL: https://turnella.com/api/v1/observations, Method: POST
    • Headers: Authorization: Bearer YOUR_API_KEY
    • Body type: Raw / JSON, with the standard body.
  4. Run once, confirm accepted in the response, then check the Data tab.

Recipe 4 — Power Automate

  1. Recurrence trigger (e.g. daily 02:00).
  2. Fetch totals from your source (a connector action or an HTTP GET to your system), then a Select/Compose step to shape the observations array.
  3. HTTP action: POST https://turnella.com/api/v1/observations, header Authorization: Bearer YOUR_API_KEY, JSON body as above.

Recipe 5 — Backfilling history

To load months of history for forecasting, send batches of up to 10,000 intervals per request (a year of 30-minute intervals is about 17,500 — two requests). Backfills are safe to re-run: re-posting identical data changes nothing, and intervals you skip stay untouched.


Verifying it worked

  1. The API response is { "accepted": N, ... } with HTTP 200 — N is the number of intervals stored.
  2. Open the workstream's Data tab in the app (or refresh if it is open). Pushed data lands in the cloud and is adopted on the app's next load or sync.
  3. Run the Forecast as usual — ingested data is indistinguishable from a CSV upload.

Troubleshooting

Response Meaning Fix
401 Missing or malformed API key No Authorization: Bearer header, or the key does not look like tk_live_… Send the header exactly as Authorization: Bearer tk_live_…
401 Invalid or revoked API key The key is wrong, or was revoked Check for copy/paste truncation; create a new key if revoked
404 Workstream not found for this API key's account The workstream ID is wrong, or belongs to a different account than the key Re-copy the ID from the address bar; make sure key and workstream are the same account
400 …intervalStart must be an ISO datetime Timestamp not parseable Use ISO 8601, e.g. 2026-07-01T09:00:00Z
400 …volume must be a number ≥ 0 Volume missing, negative, or sent as a string Send a plain number: "volume": 42, not "volume": "42"
400 …ahtSeconds must be a number > 0 when present AHT sent as 0, negative, or a string Omit ahtSeconds if you don't have it
400 Too many observations (max 10000 per request) Batch too large Split into requests of ≤ 10,000 intervals
503 This key is not linked to an organization. Recreate it. The key predates account-organisation linking Not retryable — revoke the key and create a new one in Account
503 (any other message) Temporary server-side issue Retry later — re-sending is safe
200 but numbers look wrong in the app Usually the golden rule: an event-per-ticket feed overwriting intervals with 1 Switch to scheduled interval totals

FAQ

Which timezone should timestamps use?

Any ISO 8601 timestamp works, including offsets — 2026-07-01T11:00:00+02:00 and 2026-07-01T09:00:00Z are the same moment and land in the same interval. The safest habit is to send UTC (Z). Make sure the interval start times align with how your workstream's operating hours are defined.

Can I feed several workstreams with one key?

Yes. A key covers your whole account; send one request per workstream. For clarity and safer revocation, you can also create one key per feed (up to 10 active keys).

Does the API read data out of Turnella?

No. The feed is one-way in: external systems push volumes into Turnella. There is no read API for forecasts or schedules yet.

Is there a ready-made Zendesk / Salesforce / etc. connector?

Not yet — the recipes above use each platform's generic scheduler + HTTP request features, which work today. Ready-made connectors are planned on top of this same endpoint; tell support@turnella.com which system you would use so it can be prioritised.

Is my key safe on Turnella's side?

Turnella stores a SHA-256 hash of the key, not the key itself — it cannot be shown again after creation, and a leaked database backup would not expose usable keys. Revocation is immediate.

What about live chat or email AHT?

ahtSeconds works for any channel — send the interval's average handle time in seconds. If your source system cannot compute it, omit the field and maintain AHT in your workstream's assumptions instead.