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

ScopeDescription
metrics:readAggregate 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:readThe 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:readreservedA 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:readreservedIndividual call records. No endpoint uses this yet — it exists so that granting metrics access never implies it.
transcripts:readreservedTranscript text. Not yet exposed.
scores:readreservedPer-criterion scoring detail. Not yet exposed.
outcomes:readreservedPer-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"
  }
}
StatusCodeMeaning
200—Everything worked.
400invalid_rangefrom/to unparseable, inverted, or wider than 400 days.
400invalid_stagestage 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.
400invalid_paginglimit outside 1–200, or a negative offset.
401missing_keyNo bearer token supplied.
401invalid_keyUnknown, revoked or expired key. Deliberately identical for all three — telling an unauthenticated caller which it was tells them whether they found a real key.
403missing_scopeValid key, but it lacks the scope the endpoint requires.
404not_foundThe 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.
500internal_errorFault 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.

GET/api/v1/storesmetrics:read

Response attributes

FieldDescription
storeIduuidTune's identifier. Pass to other endpoints.
storeNamestringFull name, e.g. Rooster Ridge Car Care (Pearland).
storeNumberstring | nullThe shop's own number, where set.
timezonestringIANA zone. Daily buckets are computed in this, not UTC.
accountNamestring | nullThe business the store belongs to.
accountSlugstring | nullURL-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.

GET/api/v1/metrics/storesmetrics:read
ParameterDescription
fromISO dateInclusive start. Defaults to 30 days before `to`.
toISO dateExclusive 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

FieldDescription
callsEvery call in range.
inbound / outboundDirection split. May not sum to calls — some calls have no direction recorded.
answeredCalls a human picked up.
avgDurationSecondsMean call length.
avgScorenumber | nullMean script score, 0–100. Null when nothing was scored.
scoredCallsDenominator of avgScore.
checkedCallsDenominator of bookedPct. See “What the numbers mean” — it is not calls.
bookedCallsChecked calls that led to booked work.
bookedPctnumber | nullPercentage to one decimal. Null when checkedCalls is 0 — unknown, not zero.
authorizedCentsWork the customer approved, in cents.
declinedAskedForCentsDeclined work matching what the customer came in for.
declinedRecommendedCentsDeclined work the shop proposed. Inferred, not labelled by the source system.
unattributed / unattributedPctCalls no rep could be matched to.
pipelineobjectProcessing health: recordingPending, transcriptPending, recordingUnavailableUpstream, recordingFailedOther.
callTypesobjectCounts 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.

GET/api/v1/metrics/repsmetrics:read

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

FieldDescription
employeeIduuidStable across store moves.
namestringFirst name and last initial. No contact details are exposed.
storeId / storeNameWhere these calls happened, not the rep's home store.
callsEvery call attributed to them at this store.
scoredCallsSubset that was scored. Denominator of avgScore.
avgScorenumber | nullMean script score.
checkedCalls / bookedCalls / bookedPctAs for stores.
confidentAttributionCalls 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.

GET/api/v1/metrics/reps/skillsmetrics:read

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

FieldDescription
employeeId / nameAs for rep metrics.
storeId / storeNameWhere these calls happened. One row per rep per store, so it joins to rep metrics on employeeId + storeId.
scoredCallsCalls with a score in range. Matches rep metrics.
avgScorenumberMean script score over scoredCalls.
skillsarrayOne entry per rubric line the rep was scored on, in rubric order within each call type.
skills[].callTypestringThe rubric the line belongs to: anytime, isolate_broken, pmi, general, thank_you_callback, declined_callback or other.
skills[].key / labelkey is stable and safe to store. label is the current wording and changes when a rubric is reworded.
skills[].callsCalls where the line applied. Denominator of passPct.
skills[].passedOf those, the calls where the rep did it.
skills[].passPctnumber | nullpassed 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.

GET/api/v1/metrics/analyticsmetrics:read
ParameterDescription
fromISO dateFirst day, inclusive, in the shop's own timezone. Defaults to 30 days before `to`.
toISO dateLast 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

FieldDescription
rangeobject{ from, to, timeZone } — the days actually used and the zone they were read in.
summaryobjectThe 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.
storesarrayPer 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.
declinedCallbacksarrayPer 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.

GET/api/v1/metrics/declinedmetrics:read
ParameterDescription
fromISO dateDeclined on or after this date, inclusive. Omit for all time.
toISO dateDeclined 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

FieldDescription
declinedJobs / declinedCentsEverything declined in range, however it ended up.
openJobs / openCentsStill outstanding — neither recovered nor dismissed. The number to act on.
recoveredJobs / recoveredCentsDeclined work later authorised. Cents are what was actually authorised where the matched repair order says so, otherwise the original quote.
dismissedJobsTaken off the list for a reason that is not recovery — car sold, done elsewhere, do not call.
recoveryRatePctnumber | nullrecoveredJobs as a share of everything settled either way, to one decimal. Null when nothing has settled.
dueNow / dueNowCentsOpen jobs whose next callback is due today under the 3/10/30 day cadence.
byStageobjectdueNow 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.

GET/api/v1/metrics/declined/dailymetrics:read
ParameterDescription
fromISO dateFirst day, inclusive. Required.
toISO dateLast 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

FieldDescription
storesarrayOne row per store per day: { storeId, date, due, doneSameDay, doneSince, cameBackJobs, cameBackCents }. A day with nothing due and nothing back is absent.
stores[].dueVisits (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 / doneSinceOf 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 / cameBackCentsDeclined jobs authorised that day after a callback on them, at the authorised value where known, otherwise the quote.
repsarrayOne row per rep per store per day: { employeeId, name, storeId, date, callbacks, reached, booked, bookedCents, cameBackJobs, cameBackCents }. employeeId null is Unattributed.
reps[].callbacksDeclined-callback calls the rep made that day.
reps[].reached / booked / bookedCentsVisits 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 / cameBackCentsWork that came back that day where this rep made the first callback on it — each job credited once.
rosterarrayEvery 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.

GET/api/v1/metrics/declined/repsmetrics:read
ParameterDescription
fromISO dateCallbacks made on or after this date, inclusive. Omit for all time.
toISO dateCallbacks 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

FieldDescription
employeeId / nameAs for rep metrics. One row has a null employeeId and the name Unattributed — callbacks nobody could be matched to.
callbacksDeclined-callback calls the rep made in range.
scoredCalls / avgScoreThose calls scored against the declined-callback script, and their mean score.
reachedVisits where a person answered, counted once per visit by the latest logged outcome. Voicemail and no answer are not reached.
booked / bookedCentsVisits whose logged outcome is booked, and the declined value of those visits.
openCentsDeclined value still outstanding on the visits this rep has called about.
recoveredAfterCallCentsDeclined 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[].reachedPctreached as a share of callbacks at that store, a whole number.
byStore[].recoveredJobs / recoveredCentsThe 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.
storesarrayThe 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.
minCallsToRankBelow 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.

GET/api/v1/metrics/declined/overviewmetrics:read
ParameterDescription
fromISO dateStart of the period, inclusive. Omit for all time. See the note on what it filters.
toISO dateEnd 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

FieldDescription
totalsobjectThe four cards: declinedJobs / declinedCents, openJobs / openCents, recoveredJobs / recoveredCents and recoveryRatePct, plus dismissedJobs, over every store in scope.
storesarrayThe 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[].stagesobjectKeyed not_due, day_3, day_10, day_30, overdue. Each is { visits, calledBack, calledToday, openCents }. A stage with nothing in it is absent.
recoveryRatePctThe 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.
queueobjectvisits 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.
waitingVisitsVisits 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 / recoveredJobsThe 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.

GET/api/v1/declineddeclined:read
ParameterDescription
fromISO dateDeclined on or after this date, inclusive.
toISO dateDeclined on or before this date, inclusive.
stageenumday_3, day_10, day_30 or overdue. Omit for every stage.
limitinteger1–200. Defaults to 50.
offsetintegerRows 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

FieldDescription
totalMatching rows before paging, returned alongside the limit and offset used.
declinedJobIduuidStable identifier for the declined job.
jobNamestringThe work, as Tekmetric names it.
amountCentsWhat it was quoted at when declined.
declinedOn / daysSinceThe date it was turned down, and how long ago that was.
stageenumWhich callback is due: day_3, day_10, day_30, or overdue past 45 days.
offerstringWhat 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 / customerPhoneWho to call. Phone is E.164.
vehiclestring | nullThe car, where Tekmetric recorded one.
lastCallbackAt / callbackCountCalls Tune has already matched to this job.
declineReasonstring | nullWhy they said no — from the call where it happened, or typed in by a rep.
declineReasonSourceenum | nulldecline_call, callback, manual or none — which of those it came from.
tekmetricNotestring | nullThe 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 / tekmetricRepairOrderIdFor 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.