Tune API
Read-only access to call-quality metrics across the shops you cover — volume, script scores, booked rate, declined work and attribution — plus the declined callback worklist. Resource-oriented URLs, query-string filters, JSON responses, standard HTTP status codes.
Overview
Most endpoints return aggregates, under a single metrics:read scope. No endpoint lists individual calls or transcripts, and the scopes that would grant that are separate ones — a key that reports totals cannot enumerate the calls behind them.
The one exception is the declined worklist, which returns one row per job and names the customer to call. It sits behind its own declined:read scope, granted separately and never implied by metrics:read. If your integration only charts declined dollars, ask for the totals endpoint and not this one.
Base URL: https://api.tune.srsandco.com
There is no sandbox
The API reads live production data. It is read-only — no endpoint writes anything — so the risk is disclosure rather than damage. With declined:read that disclosure includes customers' names and mobile numbers, so scope a key to only the stores its consumer needs, and only to the scopes it actually uses.
Authentication
Requests authenticate with a long-lived API key sent as a bearer token. There is no token exchange and nothing expires mid-session, so a client that can only attach a fixed header block works without a refresh cycle.
-H "Authorization: Bearer tune_sk_live_..."
A key is scoped twice: by what it may ask for (scopes, below) and by whose datait may see — an account, or an explicit list of stores. Both are checked on every request. Asking for a store outside the key's scope returns a 404, not a 403: existence itself is scoped, so the API never confirms a record it will not return.
Scopes
| Scope | Description |
|---|---|
| metrics:read | Aggregate metrics and the store directory — stores, store metrics, rep metrics and skills, the Analytics page, declined totals, the declined services page and declined callbacks by rep. |
| declined:read | The declined callback worklist, one row per job. Separate from metrics:read because those rows name a customer and carry their phone number; a key issued to chart declined dollars is not also a customer list. |
| directory:readreserved | A separate grant for the store and employee directory. No endpoint requires it yet — /api/v1/stores is under metrics:read, which every consumer of the metrics endpoints already holds. |
| calls:readreserved | Individual call records. No endpoint uses this yet — it exists so that granting metrics access never implies it. |
| transcripts:readreserved | Transcript text. Not yet exposed. |
| scores:readreserved | Per-criterion scoring detail. Not yet exposed. |
| outcomes:readreserved | Per-call outcome records. Not yet exposed. |
Versioning
The version is part of the path — /api/v1/… — never a header. Adding fields to a response is not a breaking change and will happen without a version bump, so clients should ignore unknown fields. Removing or renaming one means a new version.
Error responses
Errors carry a stable machine-readable code and a request id. Database messages are never surfaced.
{
"error": {
"code": "invalid_range",
"message": "Range is limited to 400 days.",
"request_id": "req_a57c695953e14f898de5c1ed"
}
}| Status | Code | Meaning |
|---|---|---|
| 200 | — | Everything worked. |
| 400 | invalid_range | from/to unparseable, inverted, or wider than 400 days. |
| 400 | invalid_stage | stage is not one of day_3, day_10, day_30, overdue. Rejected rather than ignored — a typo that silently returned every stage would read as no stage filter existing. |
| 400 | invalid_paging | limit outside 1–200, or a negative offset. |
| 401 | missing_key | No bearer token supplied. |
| 401 | invalid_key | Unknown, revoked or expired key. Deliberately identical for all three — telling an unauthenticated caller which it was tells them whether they found a real key. |
| 403 | missing_scope | Valid key, but it lacks the scope the endpoint requires. |
| 404 | not_found | The resource does not exist, or is outside this key's store scope. The two are the same response on purpose: the API never confirms a record it will not return. |
| 500 | internal_error | Fault on Tune's side. Quote the request_id when reporting it. |
Every response — success or error — carries the same id in an x-request-id header.
Limits
No rate limiting is enforced today
That is stated because it is true, not because the API is unlimited by design. Every request is recorded against its key, so abnormal volume is visible — expect a per-key limit before access widens beyond current integrations.
Two hard bounds are in place. The metrics endpoints take a 400-day maximum range — they run synchronously behind a user action, not as batch jobs. The declined worklist is paged at 200 rows; there are around 25,000 open jobs across ten shops, so walk it with offset rather than asking for all of it.
Stores
The stores this key can see. Use it to resolve ids before requesting metrics.
Response attributes
| Field | Description |
|---|---|
| storeIduuid | Tune's identifier. Pass to other endpoints. |
| storeNamestring | Full name, e.g. Rooster Ridge Car Care (Pearland). |
| storeNumberstring | null | The shop's own number, where set. |
| timezonestring | IANA zone. Daily buckets are computed in this, not UTC. |
| accountNamestring | null | The business the store belongs to. |
| accountSlugstring | null | URL-safe form of the account name. |
Example
curl 'https://api.tune.srsandco.com/api/v1/stores' \ -H "Authorization: Bearer $TUNE_API_KEY"
{
"stores": [
{
"storeId": "e480609c-d8b6-4f2d-9999-e34ac39255f6",
"storeName": "Rooster Ridge Car Care (Cypress)",
"storeNumber": "1111",
"timezone": "America/Chicago",
"accountName": "Rooster Ridge Car Care",
"accountSlug": "rooster-ridge"
}
]
}Store Metrics
Per-store totals for a date range, with a daily series.
| Parameter | Description |
|---|---|
| fromISO date | Inclusive start. Defaults to 30 days before `to`. |
| toISO date | Exclusive end. Defaults to now. For all of August, pass 2026-09-01. |
Two pipeline fields, not one
recordingUnavailableUpstream is the phone system's own retention expiry — nothing can recover it and it is not a Tune failure. Adding it to recordingFailedOther and plotting the total reports a vendor's retention policy as if it were an error rate.
daily can start a day before from
Buckets are computed in the store's own timezone, so a call at 2026-08-01T00:30Z belongs to July 31st in Central time. That is correct — it is the day the shop had it — but a client charting the series should expect an extra leading bucket.
Response attributes
| Field | Description |
|---|---|
| calls | Every call in range. |
| inbound / outbound | Direction split. May not sum to calls — some calls have no direction recorded. |
| answered | Calls a human picked up. |
| avgDurationSeconds | Mean call length. |
| avgScorenumber | null | Mean script score, 0–100. Null when nothing was scored. |
| scoredCalls | Denominator of avgScore. |
| checkedCalls | Denominator of bookedPct. See “What the numbers mean” — it is not calls. |
| bookedCalls | Checked calls that led to booked work. |
| bookedPctnumber | null | Percentage to one decimal. Null when checkedCalls is 0 — unknown, not zero. |
| authorizedCents | Work the customer approved, in cents. |
| declinedAskedForCents | Declined work matching what the customer came in for. |
| declinedRecommendedCents | Declined work the shop proposed. Inferred, not labelled by the source system. |
| unattributed / unattributedPct | Calls no rep could be matched to. |
| pipelineobject | Processing health: recordingPending, transcriptPending, recordingUnavailableUpstream, recordingFailedOther. |
| callTypesobject | Counts keyed by script type, plus unscored. |
| dailyarray | { day, calls, avgScore } per local day. |
Example
curl 'https://api.tune.srsandco.com/api/v1/metrics/stores?from=2026-08-01&to=2026-09-01' \ -H "Authorization: Bearer $TUNE_API_KEY"
{
"range": { "from": "2026-08-01T00:00:00+00:00", "to": "2026-09-01T00:00:00+00:00" },
"stores": [
{
"storeId": "41b2914a-c26d-426f-a994-5c5b7336961b",
"storeName": "Rooster Ridge Car Care (Pearland)",
"storeNumber": "1714",
"accountName": "Rooster Ridge Car Care",
"accountSlug": "rooster-ridge",
"calls": 2397,
"inbound": 1281,
"outbound": 1116,
"answered": 2397,
"avgDurationSeconds": 162,
"avgScore": 70.5,
"scoredCalls": 1999,
"checkedCalls": 1342,
"bookedCalls": 667,
"bookedPct": 49.7,
"authorizedCents": 61764630,
"declinedAskedForCents": 6153903,
"declinedRecommendedCents": 87104091,
"unattributed": 915,
"unattributedPct": 38.2,
"pipeline": {
"recordingPending": 0,
"transcriptPending": 0,
"recordingUnavailableUpstream": 32,
"recordingFailedOther": 0
},
"callTypes": {
"general": 1544, "other": 323, "isolate_broken": 211,
"anytime": 124, "pmi": 120, "unscored": 75
},
"daily": [
{ "day": "2026-07-31", "calls": 3, "avgScore": 38.3 },
{ "day": "2026-08-01", "calls": 83, "avgScore": 57 }
]
}
]
}Rep Metrics
Per-rep totals — the leaderboard source. Same from/to parameters as store metrics.
One row per rep per store
Someone who worked at two stores in the period appears twice, with their calls filed where the work actually happened. Do not add those rows together and call the result one rep's average — group by employeeId and recompute, or state which store you mean.
Response attributes
| Field | Description |
|---|---|
| employeeIduuid | Stable across store moves. |
| namestring | First name and last initial. No contact details are exposed. |
| storeId / storeName | Where these calls happened, not the rep's home store. |
| calls | Every call attributed to them at this store. |
| scoredCalls | Subset that was scored. Denominator of avgScore. |
| avgScorenumber | null | Mean script score. |
| checkedCalls / bookedCalls / bookedPct | As for stores. |
| confidentAttribution | Calls matched by stated name or assigned by hand, as opposed to inferred. Lower than calls means some attribution is weaker. |
Example
curl 'https://api.tune.srsandco.com/api/v1/metrics/reps?from=2026-08-01&to=2026-09-01' \ -H "Authorization: Bearer $TUNE_API_KEY"
{
"range": { "from": "2026-08-01T00:00:00+00:00", "to": "2026-09-01T00:00:00+00:00" },
"reps": [
{
"employeeId": "0e2f9c0f-67f9-4aec-99f7-a364e5f826b9",
"name": "Bailey C.",
"storeId": "41b2914a-c26d-426f-a994-5c5b7336961b",
"storeName": "Rooster Ridge Car Care (Pearland)",
"calls": 503,
"avgDurationSeconds": 166,
"avgScore": 68.8,
"scoredCalls": 485,
"checkedCalls": 385,
"bookedCalls": 177,
"bookedPct": 46,
"confidentAttribution": 503
}
]
}Rep Skills
Each rep's score taken apart into the rubric lines it was built from — which parts of the call they do, and which they skip. Same from/to parameters as store metrics.
A line that did not apply is not a miss
Some lines only arise when the call goes a particular way — handling an objection needs an objection. A call where the line never came up is left out of both passed and calls, the same way the score itself treats it. So calls differs from line to line, and is often well below scoredCalls.
Read the denominator before the percentage
A rep with 3 PMI calls can show 0% or 100% on a line off a single call. calls is shipped beside every passPct for that reason. Decide a minimum before ranking anyone on it.
Response attributes
| Field | Description |
|---|---|
| employeeId / name | As for rep metrics. |
| storeId / storeName | Where these calls happened. One row per rep per store, so it joins to rep metrics on employeeId + storeId. |
| scoredCalls | Calls with a score in range. Matches rep metrics. |
| avgScorenumber | Mean script score over scoredCalls. |
| skillsarray | One entry per rubric line the rep was scored on, in rubric order within each call type. |
| skills[].callTypestring | The rubric the line belongs to: anytime, isolate_broken, pmi, general, thank_you_callback, declined_callback or other. |
| skills[].key / label | key is stable and safe to store. label is the current wording and changes when a rubric is reworded. |
| skills[].calls | Calls where the line applied. Denominator of passPct. |
| skills[].passed | Of those, the calls where the rep did it. |
| skills[].passPctnumber | null | passed as a share of calls, to one decimal. Null when the line never applied. |
Example
curl 'https://api.tune.srsandco.com/api/v1/metrics/reps/skills?from=2026-08-01&to=2026-09-01' \ -H "Authorization: Bearer $TUNE_API_KEY"
{
"range": { "from": "2026-08-01T00:00:00+00:00", "to": "2026-09-01T00:00:00+00:00" },
"reps": [
{
"employeeId": "0e2f9c0f-67f9-4aec-99f7-a364e5f826b9",
"name": "Bailey C.",
"storeId": "41b2914a-c26d-426f-a994-5c5b7336961b",
"storeName": "Rooster Ridge Car Care (Pearland)",
"scoredCalls": 522,
"avgScore": 68.8,
"skills": [
{
"callType": "anytime",
"key": "greeting_name",
"label": "Greets with store and own name",
"calls": 33,
"passed": 32,
"passPct": 97
},
{
"callType": "anytime",
"key": "redirect_with_question",
"label": "Redirects price question with a symptom question",
"calls": 33,
"passed": 22,
"passPct": 66.7
},
{
"callType": "anytime",
"key": "mirrors_words",
"label": "Mirrors the customer's own words",
"calls": 33,
"passed": 10,
"passPct": 30.3
}
]
}
]
}Analytics Page
Every number on Tune's Analytics page for a range of days, worked out the way the page works it out.
| Parameter | Description |
|---|---|
| fromISO date | First day, inclusive, in the shop's own timezone. Defaults to 30 days before `to`. |
| toISO date | Last day, inclusive. Defaults to today at the shop. The range is limited to 400 days. |
Days are the shop's days
from and to are calendar dates read in the timezone of the key's first store, and each call is filed under the day it happened at its own store. Store Metrics and Rep Metrics take instants instead, so the same dates on those endpoints include or drop the calls either side of local midnight. Use this endpoint when the number has to match the page.
One row per rep here
Unlike Rep Metrics, a rep who worked two stores is a single row with all their calls. That is how the page's leaderboard counts them.
It reads every call in the range
A month across ten stores is about twelve thousand calls and takes a few seconds. Ask for the range you need rather than polling a year.
Response attributes
| Field | Description |
|---|---|
| rangeobject | { from, to, timeZone } — the days actually used and the zone they were read in. |
| summaryobject | The four headline cards: totalCalls, avgScore, unassignedPct (calls no rep could be matched to, whole number) and inbound / outbound. unknownDirection is the footnote under the volume table: calls with no direction recorded, which is why inbound and outbound may not add up to the total. |
| volumearray | { date, inbound, outbound } for every day in range, zero days included. |
| scoresarray | { date, avgScore } per day over scored calls. avgScore is null on a day nothing was scored. |
| callTypesarray | { callType, label, count }, largest first. unscored is calls the scorer has not classified yet. |
| storesarray | Per store: calls, inbound, outbound, unknownDirection, avgScore, checkedCalls, bookedCalls, notBookedCalls, bookedPct. The score chart and both store tables on the page are this array. bookedPct is null for a store with nothing checked, and the page leaves that store off the booked-rate panel. |
| repsarray | { employeeId, name, calls, avgScore } for every rep with a call in range, best average first. The page shows the first ten. |
| declinedCallbacksarray | Per store: declinedCallbacks made in range, recoveredJobs / recoveredCents that came back in range, and afterCallbackJobs / afterCallbackCents — the part that had a call before it. |
Example
curl 'https://api.tune.srsandco.com/api/v1/metrics/analytics?from=2026-09-01&to=2026-09-30' \ -H "Authorization: Bearer $TUNE_API_KEY"
{
"range": { "from": "2026-09-01", "to": "2026-09-30", "timeZone": "America/Chicago" },
"summary": {
"totalCalls": 11456,
"avgScore": 73.8,
"unassignedPct": 24,
"inbound": 5409,
"outbound": 6047,
"unknownDirection": 0
},
"volume": [
{ "date": "2026-09-01", "inbound": 212, "outbound": 205 },
{ "date": "2026-09-02", "inbound": 178, "outbound": 190 }
],
"scores": [
{ "date": "2026-09-01", "avgScore": 77.3 },
{ "date": "2026-09-02", "avgScore": 73.7 }
],
"callTypes": [
{ "callType": "general", "label": "General", "count": 6477 },
{ "callType": "other", "label": "No conversation", "count": 1944 },
{ "callType": "unscored", "label": "Not yet scored", "count": 618 }
],
"stores": [
{
"storeId": "41b2914a-c26d-426f-a994-5c5b7336961b",
"storeName": "Rooster Ridge Car Care (Pearland)",
"calls": 2324,
"inbound": 1141,
"outbound": 1183,
"unknownDirection": 0,
"avgScore": 71.2,
"checkedCalls": 1242,
"bookedCalls": 685,
"bookedPct": 55.2,
"notBookedCalls": 557
}
],
"reps": [
{
"employeeId": "0e2f9c0f-67f9-4aec-99f7-a364e5f826b9",
"name": "Bailey C.",
"calls": 579,
"avgScore": 73.1
}
],
"declinedCallbacks": [
{
"storeId": "41b2914a-c26d-426f-a994-5c5b7336961b",
"storeName": "Rooster Ridge Car Care (Pearland)",
"declinedCallbacks": 41,
"recoveredJobs": 13,
"recoveredCents": 460140,
"afterCallbackJobs": 0,
"afterCallbackCents": 0
}
]
}Declined Totals
Per-store declined work: what is still outstanding, what came back, and how much is due a callback today.
| Parameter | Description |
|---|---|
| fromISO date | Declined on or after this date, inclusive. Omit for all time. |
| toISO date | Declined on or before this date, inclusive. Omit for all time. |
No date range means all time, not 30 days
Unlike the other metrics endpoints, from and to default to unbounded. Outstanding declined work is a running balance built up over years — around $14.9M across the ten shops — and only a few hundred thousand of that was declined in the last month. A rolling-30-day default would answer a different question with a number that looks like the answer to this one.
Recovery is a match, not a credit
A job counts as recovered when the same work later turns up authorised for that customer. Nothing here establishes that a callback caused it — they may simply have come back. Report it as work that returned, not as revenue the callback programme generated.
Response attributes
| Field | Description |
|---|---|
| declinedJobs / declinedCents | Everything declined in range, however it ended up. |
| openJobs / openCents | Still outstanding — neither recovered nor dismissed. The number to act on. |
| recoveredJobs / recoveredCents | Declined work later authorised. Cents are what was actually authorised where the matched repair order says so, otherwise the original quote. |
| dismissedJobs | Taken off the list for a reason that is not recovery — car sold, done elsewhere, do not call. |
| recoveryRatePctnumber | null | recoveredJobs as a share of everything settled either way, to one decimal. Null when nothing has settled. |
| dueNow / dueNowCents | Open jobs whose next callback is due today under the 3/10/30 day cadence. |
| byStageobject | dueNow broken out: { day_3, day_10, day_30, overdue }, each { jobs, cents }. |
Example
curl 'https://api.tune.srsandco.com/api/v1/metrics/declined' \ -H "Authorization: Bearer $TUNE_API_KEY"
{
"range": { "from": null, "to": null },
"stores": [
{
"storeId": "e480609c-d8b6-4f2d-9999-e34ac39255f6",
"storeName": "Rooster Ridge Car Care (Cypress)",
"storeNumber": "1111",
"accountName": "Rooster Ridge Car Care",
"accountSlug": "rooster-ridge",
"declinedJobs": 4791,
"declinedCents": 281516558,
"openJobs": 4761,
"openCents": 281172429,
"recoveredJobs": 30,
"recoveredCents": 349946,
"dismissedJobs": 0,
"recoveryRatePct": 0.6,
"dueNow": 4711,
"dueNowCents": 277816044,
"byStage": {
"day_3": { "jobs": 213, "cents": 13497152 },
"day_10": { "jobs": 652, "cents": 42035468 },
"day_30": { "jobs": 596, "cents": 38628132 },
"overdue": { "jobs": 3250, "cents": 183655292 }
}
}
]
}Declined Callbacks by Day
Day by day: callbacks that came due and how many were made, who made them, what they booked, and what came back — the rows to build daily scoreboards and running totals from.
| Parameter | Description |
|---|---|
| fromISO date | First day, inclusive. Required. |
| toISO date | Last day, inclusive. Required; at most 93 days after `from`. |
Booked is the commitment, came back is the sale
bookedCents is what the rep logged on the call; cameBackCents is work Tekmetric later shows authorised. They are different claims and will not match — show them side by side, and add them only as a total of value derived.
Days are the store's own
Every date is the store's calendar day in its own timezone, so a day's rows for a Central and an Eastern store cover different hours of UTC.
Response attributes
| Field | Description |
|---|---|
| storesarray | One row per store per day: { storeId, date, due, doneSameDay, doneSince, cameBackJobs, cameBackCents }. A day with nothing due and nothing back is absent. |
| stores[].due | Visits (repair orders) that reached their 3-, 10- or 30-day callback that day and were still open. A due date on a Sunday counts on the Monday. |
| stores[].doneSameDay / doneSince | Of those, visits called back that same day / that day or any day since — a callback call or a logged attempt on the visit, by anyone. |
| stores[].cameBackJobs / cameBackCents | Declined jobs authorised that day after a callback on them, at the authorised value where known, otherwise the quote. |
| repsarray | One row per rep per store per day: { employeeId, name, storeId, date, callbacks, reached, booked, bookedCents, cameBackJobs, cameBackCents }. employeeId null is Unattributed. |
| reps[].callbacks | Declined-callback calls the rep made that day. |
| reps[].reached / booked / bookedCents | Visits whose latest outcome logged that day is a person reached / booked, and the declined value of the visits booked. Hand-logged outcomes count for the rep whose call on the visit was closest, within 12 hours. |
| reps[].cameBackJobs / cameBackCents | Work that came back that day where this rep made the first callback on it — each job credited once. |
| rosterarray | Every active employee of the stores: { employeeId, name, storeId, lastCallbackOn } — lastCallbackOn null if they never made one. Lists the reps with no rows above. |
Example
curl 'https://api.tune.srsandco.com/api/v1/metrics/declined/daily?from=2026-10-01&to=2026-10-05' \ -H "Authorization: Bearer $TUNE_API_KEY"
{
"range": { "from": "2026-10-01", "to": "2026-10-05" },
"stores": [
{
"storeId": "e480609c-d8b6-4f2d-9999-e34ac39255f6",
"date": "2026-10-05",
"due": 64,
"doneSameDay": 21,
"doneSince": 22,
"cameBackJobs": 1,
"cameBackCents": 48211
}
],
"reps": [
{
"employeeId": "0e2f9c0f-67f9-4aec-99f7-a364e5f826b9",
"name": "Chris",
"storeId": "e480609c-d8b6-4f2d-9999-e34ac39255f6",
"date": "2026-10-05",
"callbacks": 14,
"reached": 5,
"booked": 1,
"bookedCents": 85348,
"cameBackJobs": 1,
"cameBackCents": 48211
}
],
"roster": [
{
"employeeId": "0e2f9c0f-67f9-4aec-99f7-a364e5f826b9",
"name": "Chris",
"storeId": "e480609c-d8b6-4f2d-9999-e34ac39255f6",
"lastCallbackOn": "2026-10-05"
}
]
}Declined Callbacks by Rep
Who is making the declined callbacks: how many, how many reached a person, what was booked, and how well the calls were made.
| Parameter | Description |
|---|---|
| fromISO date | Callbacks made on or after this date, inclusive. Omit for all time. |
| toISO date | Callbacks made on or before this date, inclusive. Omit for all time. |
One row per rep here, not per store
Unlike rep metrics, a rep who called for two stores is one row with two byStore lines. The headline numbers are the sum of the lines.
The dates are when the call was made
from and to filter the day of the callback, not the day the work was declined. /api/v1/metrics/declined filters on the declined date, so the same range on the two endpoints is two different sets of work. Omitted, both mean all time.
Booked and recovered are not the same claim
booked is what the rep logged at the end of the call. recoveredAfterCallCents is work Tekmetric later shows authorised. Neither establishes the call caused it, and they will not agree: a customer who says yes and never comes in is booked and not recovered.
Two recovered figures, and the table shows the second
recoveredAfterCallCents on the rep is work that came back after any call of theirs. byStore[].recoveredCents is work that came back where they were the FIRST to call, and it is the one printed on the team table. It is windowed on the day the work was declined, while callbacks is windowed on the day the call was made — the board passes one period to both, and so does this.
Response attributes
| Field | Description |
|---|---|
| employeeId / name | As for rep metrics. One row has a null employeeId and the name Unattributed — callbacks nobody could be matched to. |
| callbacks | Declined-callback calls the rep made in range. |
| scoredCalls / avgScore | Those calls scored against the declined-callback script, and their mean score. |
| reached | Visits where a person answered, counted once per visit by the latest logged outcome. Voicemail and no answer are not reached. |
| booked / bookedCents | Visits whose logged outcome is booked, and the declined value of those visits. |
| openCents | Declined value still outstanding on the visits this rep has called about. |
| recoveredAfterCallCents | Declined work on those visits that was later authorised, after the rep's first call about it. |
| skillsarray | { label, calls, passPct } per line of the declined-callback script, weakest first. passPct is a whole number. |
| byStorearray | { storeId, storeName, callbacks, reached, reachedPct, booked, bookedCents, avgScore, recoveredJobs, recoveredCents } per store the rep called for, busiest first. This is the row the team table prints. |
| byStore[].reachedPct | reached as a share of callbacks at that store, a whole number. |
| byStore[].recoveredJobs / recoveredCents | The table's Recovered column: declined work at that store that came back after THIS rep was the first to call about it. Zero when nobody's call came first. |
| storesarray | The board's shop headers: { storeId, storeName, reps, callbacks, reached, reachedPct, booked, bookedCents, needsCoaching }, busiest first. needsCoaching counts reps scoring under 40 on at least minCallsToRank callbacks there. |
| weakestSkillsarray | { label, passPct } for the three script steps the whole team skips most, weakest first — the coaching panel. |
| minCallsToRank | Below this many callbacks the board marks a line as too few calls to judge. Currently 10. |
Example
curl 'https://api.tune.srsandco.com/api/v1/metrics/declined/reps?from=2026-09-01&to=2026-09-30' \ -H "Authorization: Bearer $TUNE_API_KEY"
{
"range": { "from": "2026-09-01", "to": "2026-09-30" },
"minCallsToRank": 10,
"weakestSkills": [
{ "label": "Asks what is preventing the work", "passPct": 9 },
{ "label": "Closes with a choice of appointment", "passPct": 10 },
{ "label": "Asks if now is a good time", "passPct": 23 }
],
"stores": [
{
"storeId": "41b2914a-c26d-426f-a994-5c5b7336961b",
"storeName": "Rooster Ridge Car Care (Pearland)",
"reps": 5,
"callbacks": 41,
"reached": 3,
"reachedPct": 7,
"booked": 0,
"bookedCents": 0,
"needsCoaching": 0
}
],
"reps": [
{
"employeeId": "0e2f9c0f-67f9-4aec-99f7-a364e5f826b9",
"name": "Bailey C.",
"callbacks": 15,
"scoredCalls": 15,
"avgScore": 48.6,
"reached": 2,
"booked": 0,
"bookedCents": 0,
"openCents": 3060267,
"recoveredAfterCallCents": 0,
"skills": [
{ "label": "Asks what is preventing the work", "calls": 15, "passPct": 0 },
{ "label": "Closes with a choice of appointment", "calls": 15, "passPct": 0 },
{ "label": "Asks if now is a good time", "calls": 15, "passPct": 7 }
],
"byStore": [
{
"storeId": "41b2914a-c26d-426f-a994-5c5b7336961b",
"storeName": "Rooster Ridge Car Care (Pearland)",
"callbacks": 15,
"reached": 2,
"reachedPct": 13,
"booked": 0,
"bookedCents": 0,
"avgScore": 48.6,
"recoveredJobs": 0,
"recoveredCents": 0
}
]
}
]
}Declined Services Page
Every number on Tune's declined services page that is not about one rep: the headline cards, the store table, the callback queue, logged outcomes, what came back and who called first.
| Parameter | Description |
|---|---|
| fromISO date | Start of the period, inclusive. Omit for all time. See the note on what it filters. |
| toISO date | End of the period, inclusive. Omit for all time. |
from and to do not filter everything, or the same thing
They mean here what they mean on the page. totals, stores and recovery are the work DECLINED in the period. recovered is the work that CAME BACK in the period, so it will not equal totals.recoveredJobs. queue is the visits declined in the period, and with a period set it includes visits too recent to call. stages, queue.byStage and outcomes ignore the period entirely: a stage is defined by how old a decline is, and an outcome is where a visit stands now.
Stages count visits, Declined Totals counts jobs
A visit is one repair order and usually carries several declined jobs. byStage on Declined Totals is in jobs and cents; stages here is in visits, the unit the worklist lists. Neither is wrong and they will not add up to each other.
The curve can run past the period
recovery.series follows work declined in the period to the day it came back, which is often in the next month. That is why its last date can be later than to.
Response attributes
| Field | Description |
|---|---|
| totalsobject | The four cards: declinedJobs / declinedCents, openJobs / openCents, recoveredJobs / recoveredCents and recoveryRatePct, plus dismissedJobs, over every store in scope. |
| storesarray | The same figures per store, largest outstanding first, each with stages. A store with a queue and nothing declined in the period still appears, with zeros. |
| stores[].stagesobject | Keyed not_due, day_3, day_10, day_30, overdue. Each is { visits, calledBack, calledToday, openCents }. A stage with nothing in it is absent. |
| recoveryRatePct | The page's Recovery rate card: recovered DOLLARS as a share of declined dollars, to two decimals. On totals and on every store. Not the figure of the same name on Declined Totals, which is recovered jobs over jobs that have settled. |
| queueobject | visits and dueVisits on the worklist for the period; notDueYet { visits, jobs, amountCents, firstDueOn } for work too recent to call; byStage, the stage figures summed over stores. |
| outcomesarray | { outcome, visits, amountCents, waiting } by the latest logged outcome per visit: booked, thinking, voicemail, mailbox_full, no_answer, declined_again, done_elsewhere, do_not_call, wrong_number. |
| waitingVisits | Visits parked by a logged outcome and due to come back. The sum of outcomes[].waiting. |
| recoveredobject | { jobs, cents, afterCallbackJobs } — work that came back IN the period, and how much of it had a call first. |
| recovery.seriesarray | { date, cents, jobs, cumulativeCents, cumulativeJobs } per day: the work declined in the period, on the day each piece came back. |
| recovery.recoveredCents / recoveredJobs | The figure above the curve: everything in the series, added up. |
| recovery.creditarray | { rep, called, storeId, jobs, cents }: recovered work by who called first, per store. rep null with called true is a call nobody is matched to; called false is nobody having called. |
| recovery.creditByReparray | { rep, called, jobs, cents }: the same, added across stores — the Who called first card. Named reps first, then unmatched calls, then nobody called. |
Example
curl 'https://api.tune.srsandco.com/api/v1/metrics/declined/overview?from=2026-09-01&to=2026-09-30' \ -H "Authorization: Bearer $TUNE_API_KEY"
{
"range": { "from": "2026-09-01", "to": "2026-09-30" },
"totals": {
"declinedJobs": 4717,
"declinedCents": 324860040,
"openJobs": 4655,
"openCents": 312785900,
"recoveredJobs": 45,
"recoveredCents": 3469891,
"dismissedJobs": 17,
"recoveryRatePct": 1.07
},
"stores": [
{
"storeId": "e480609c-d8b6-4f2d-9999-e34ac39255f6",
"storeName": "Rooster Ridge Car Care (Cypress)",
"declinedJobs": 962,
"declinedCents": 63609652,
"openJobs": 942,
"openCents": 62253772,
"recoveredJobs": 12,
"recoveredCents": 1169283,
"dismissedJobs": 8,
"recoveryRatePct": 1.84,
"stages": {
"not_due": { "visits": 25, "calledBack": 5, "calledToday": 1, "openCents": 3148072 },
"day_3": { "visits": 49, "calledBack": 2, "calledToday": 13, "openCents": 7489181 },
"day_10": { "visits": 257, "calledBack": 17, "calledToday": 6, "openCents": 39323425 },
"day_30": { "visits": 261, "calledBack": 25, "calledToday": 0, "openCents": 32209405 },
"overdue": { "visits": 1340, "calledBack": 64, "calledToday": 3, "openCents": 189229161 }
}
}
],
"queue": {
"visits": 1790,
"dueVisits": 1733,
"notDueYet": { "visits": 58, "jobs": 124, "amountCents": 5951463, "firstDueOn": "2026-10-03" },
"byStage": {
"day_3": { "visits": 373, "calledBack": 7, "calledToday": 17, "openCents": 55347777 }
}
},
"outcomes": [
{ "outcome": "voicemail", "visits": 349, "amountCents": 71784074, "waiting": 28 },
{ "outcome": "booked", "visits": 22, "amountCents": 3768139, "waiting": 7 }
],
"waitingVisits": 49,
"recovered": { "jobs": 78, "cents": 4439914, "afterCallbackJobs": 7 },
"recovery": {
"recoveredCents": 3469891,
"recoveredJobs": 45,
"series": [
{ "date": "2026-09-07", "cents": 0, "jobs": 0, "cumulativeCents": 0, "cumulativeJobs": 0 },
{ "date": "2026-09-08", "cents": 28994, "jobs": 1, "cumulativeCents": 28994, "cumulativeJobs": 1 }
],
"credit": [
{
"rep": "Damien",
"called": true,
"storeId": "3a6cf8d2-bfb4-4697-91d1-602267e3bbe0",
"jobs": 1,
"cents": 172340
},
{
"rep": null,
"called": false,
"storeId": "e480609c-d8b6-4f2d-9999-e34ac39255f6",
"jobs": 12,
"cents": 1169283
}
],
"creditByRep": [
{ "rep": "KYLER", "called": true, "jobs": 1, "cents": 903816 },
{ "rep": "Damien", "called": true, "jobs": 1, "cents": 172340 },
{ "rep": null, "called": false, "jobs": 43, "cents": 2393735 }
]
}
}Declined Worklist
One row per declined job due a callback, in the order the calls should be made.
| Parameter | Description |
|---|---|
| fromISO date | Declined on or after this date, inclusive. |
| toISO date | Declined on or before this date, inclusive. |
| stageenum | day_3, day_10, day_30 or overdue. Omit for every stage. |
| limitinteger | 1–200. Defaults to 50. |
| offsetinteger | Rows to skip. Defaults to 0. |
These rows name a customer
Every row carries a real person's name and mobile number. That is why this endpoint needs declined:read, which is granted separately and never implied by metrics:read. Treat the response as customer data: do not log it, cache it in a browser, or forward it anywhere the rest of your customer records would not go. If you only need to chart declined dollars, ask for the totals endpoint instead.
The order is the work
Rows come back earliest-stage first, and most valuable first within a stage, because a day-3 call still has the full quote to offer and is worth more than a day-30 discount on the same job. A consumer that takes the first page and stops has taken the right calls rather than an arbitrary fifty.
A job leaves the list once it has been called
When a callback is matched to a job at its current stage the row disappears until the next stage falls due — so one call does not discharge every later follow-up. Totals therefore fall during a working day without anything having been recovered.
Response attributes
| Field | Description |
|---|---|
| total | Matching rows before paging, returned alongside the limit and offset used. |
| declinedJobIduuid | Stable identifier for the declined job. |
| jobNamestring | The work, as Tekmetric names it. |
| amountCents | What it was quoted at when declined. |
| declinedOn / daysSince | The date it was turned down, and how long ago that was. |
| stageenum | Which callback is due: day_3, day_10, day_30, or overdue past 45 days. |
| offerstring | What the script offers at this stage — the original quote, then 10% off, then 15%. Carried on the row so the worklist and the script cannot disagree. |
| customerName / customerPhone | Who to call. Phone is E.164. |
| vehiclestring | null | The car, where Tekmetric recorded one. |
| lastCallbackAt / callbackCount | Calls Tune has already matched to this job. |
| declineReasonstring | null | Why they said no — from the call where it happened, or typed in by a rep. |
| declineReasonSourceenum | null | decline_call, callback, manual or none — which of those it came from. |
| tekmetricNotestring | null | The shop's own note on the job in Tekmetric. Kept apart from declineReason on purpose: the shop's record and Tune's reading of a transcript are two different claims. |
| tekmetricShopId / tekmetricCustomerId / tekmetricRepairOrderId | For deep-linking back to the system of record. |
Example
curl 'https://api.tune.srsandco.com/api/v1/declined?stage=day_3&limit=2' \ -H "Authorization: Bearer $TUNE_API_KEY"
{
"range": { "from": null, "to": null },
"total": 213,
"limit": 2,
"offset": 0,
"jobs": [
{
"declinedJobId": "582b45ff-e35e-4296-ae02-3f6e5e3d714d",
"storeId": "e480609c-d8b6-4f2d-9999-e34ac39255f6",
"storeName": "Rooster Ridge Car Care (Cypress)",
"jobName": "Remove & Replace Differential",
"amountCents": 449915,
"declinedOn": "2026-09-15",
"daysSince": 6,
"stage": "day_3",
"offer": "Honour the original quote",
"customerName": "Maged S.",
"customerPhone": "+13094300829",
"vehicle": null,
"lastCallbackAt": null,
"callbackCount": 0,
"declineReason": null,
"declineReasonSource": null,
"tekmetricNote": null,
"tekmetricShopId": 15320,
"tekmetricCustomerId": 116735539,
"tekmetricRepairOrderId": 364096727
}
]
}What the numbers mean
The things that are easy to get wrong without being told.
Booked rate is not over all calls
bookedPct is bookedCalls / checkedCalls. checkedCalls counts only customer calls whose outcome has actually been checked. Vendor and internal calls are excluded because they cannot book work — including them measured about five points of drag on every shop. Unclassified calls are excluded too: an unknown counterparty is not evidence of a missed sale. Always show the denominator alongside the rate.
Null means unknown, never zero
avgScore and bookedPct are null when there is nothing to average. Rendering that as 0 says “this shop converts nobody”, which is a different and much worse claim than “not enough data yet”.
Small samples are not rankings
A rep with three calls at 100 is not the best rep. Tune's own leaderboards require at least 25 calls before ranking anyone. The API returns raw figures and leaves the threshold to the consumer — so the consumer has to apply one.
Declined work is normal
Declined dollars are mostly work found on inspection rather than what the customer came in asking for, which is why the two are reported separately. A high declinedRecommendedCents means the shop is inspecting and recommending, not that it is failing.
Two different declined numbers, and they do not reconcile
declinedAskedForCents and declinedRecommendedCents on the metrics endpoints are read off call transcripts — what was turned down on a call in that window. openCents on the declined endpoints is read from Tekmetric — jobs on a repair order that were never authorised, whether or not a call ever mentioned them. The second is far larger and is the outstanding balance; the first is a per-call signal. Putting them side by side as though one checks the other will not add up, because they are counting different things.
Recovered is a match, not a credit
A declined job counts as recovered when the same work later appears authorised for that customer. Nothing establishes that a callback caused it — the customer may simply have come back. Report it as work that returned, not as revenue the callback programme generated.
Getting a key
Keys are issued by the Tune team. Ask for one and say which stores or account the integration needs and what it is for — a key is scoped to exactly that, so a narrower request is quicker to grant.
You will receive the key once, in plaintext. Store it as a secret in your deployment environment, never in source control. Tune keeps only a hash of it, so if it is lost it cannot be recovered or re-sent — a replacement has to be issued.
Keys expire after a year by default. Rotation needs no coordinated cutover: ask for the replacement, deploy it, then tell us to revoke the old one. Both work at once until you do.
If a key is exposed, say so immediately rather than waiting for the rotation window. Revocation takes effect on the next request.