Access to Contractor Independence clients and products. Reads are GET. The GBP edit calls (scope gbp:write) use PATCH, POST and DELETE and change the live Google profile. A fresh GBP photo check (scope gbp:check) is a POST and a paid AI call. Authenticate with Authorization: Bearer cim_live_... (or the X-API-Key header). Keys are created on Admin -> API and shown once. Every success response is { data, meta: { apiVersion, requestId, generatedAt } } (status 200, or 202 for a photo check that started or is running); every error is { error: { code, message } }. Dates are YYYY-MM-DD, timestamps ISO-8601 UTC, phone numbers as stored (E.164). Some fields are known to be unreliable in the source data and are documented as such.
Base URL: https://admin.contractorindependence.com/api/v1. Reads are GET; the GBP edit calls use PATCH, POST and DELETE and need the gbp:write scope; a fresh GBP photo check is a POST and needs the gbp:check scope (a paid AI call). To get a key, ask your Contractor Independence contact.
Building with AI? Give your assistant the AI version of these docs (plain Markdown, every call, parameter and example). The machine-readable contract is openapi.json.
Send your key on every call, either as Authorization: Bearer <key> or as X-API-Key: <key>. Keys start with cim_live_, are shown once when created, and can be revoked or set to expire. Each key carries scopes that decide which calls it may make (see Scopes).
Keep keys server-side: never put a key in a browser, a mobile app or a public repository.
Success: { "data": …, "meta": { "apiVersion", "requestId", "generatedAt" } }. Lists add counts or a nextCursor to meta. Errors: { "error": { "code", "message" } } with these HTTP statuses:
Status
Meaning
400
Bad request: an id or query parameter is malformed (code invalid_id or invalid_parameter).
401
No key, an unknown key, a revoked key or an expired key (code unauthorized).
403
The key lacks the scope this call needs (code forbidden_scope).
404
Not found (code not_found), or the product exists under another type (code wrong_product_type, with href).
409
Not possible on this profile: the photo was uploaded by a customer (code customer_photo: Google lets a business delete only its own photos; report it with takedownUrl), the product is Inactive (code product_not_live: its profile can be read, not changed), its Local Viking record is disabled (code local_viking_disabled), or the same photo was added in the last 10 minutes (code duplicate_photo).
410
CallTools no longer has the recording (code recording_expired): it keeps call audio about 60 days.
413
The body or the photo is too large (code too_large). Bodies over 5 MB are refused at the edge with a plain 413 page before reaching the API.
415
The body is not JSON (code unsupported_media_type). Send Content-Type: application/json.
422
The edit is invalid (code invalid_edit, nothing changed), or Local Viking or Google refused a section (code local_viking_rejected). Once any section has been saved, every error (422 or 503) carries applied: the fields already live.
429
Rate limited (code rate_limited). Retry-After says when to try again.
503
A dependency (database, Twilio, GHL or Local Viking) is unavailable (code upstream_unavailable), or the Local Viking session needs reconnecting (code local_viking_session), or Local Viking did not answer in time (code timed_out: the change may still go through, so read the profile before retrying).
Error shape (401)
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}
40 started per key per UTC day by default (dailyChecks.limit is the current number); a product checked in the last 60 minutes is returned, not checked again
5SOS calls that may read CallTools (one contact and its timeline, calls, one call, SMS, campaigns, live filters, recordings)
30 per minute per key. CallTools reads are also capped across the whole API: when that share is used up the call answers 503 upstream_unavailable with Retry-After
5SOS reports (/5sos/reports/...)
6 per minute per key; one report runs at a time across the API (503 with Retry-After while another runs)
5SOS recordings
3 streams at once across the API (503 while full)
Failed authentication
20 per minute per IP; further failed attempts get 429 for the rest of the minute. A valid key is never refused by this limit.
Over a limit you get 429 with a Retry-After header in seconds.
Scopes
Scope
Allows
clients:read
Read clients: status, team, dates and the product list.
clients:contacts
Adds the client's primary contact (name, email, phone) to client responses. Opt-in: personal data. Needs clients:read as well.
maps:read
Read Maps products, including the cached tracking-number check.
lsa:read
Read LSA (stored as GGA) products, including the cached tracking-number check.
5sos:read
Read 5SOS products and their review locations.
tracking:check
Allows ?live=1 on a Maps or LSA product: re-runs the tracking-number check now (10 per minute). Needs maps:read or lsa:read as well.
tracking:read
Read the tracking-number audit (main status and every sub-status) for all live Maps and LSA products: /tracking-checks.
gbp:read
Read a Maps product's Google Business Profile through Local Viking: every profile field and every photo and video, in Local Viking's own format: /gbp. Also the GBP photo checks (cover and search photo grades): /gbp-photos.
gbp:write
Edit a Maps product's Google Business Profile through Local Viking: profile fields, add a photo, delete a business photo (and read it, like gbp:read). Changes go live on Google.
gbp:check
Run a fresh GBP photo check of a Maps product now (POST /gbp-photos/{productId}/check), and read the results like gbp:read. Each check is a paid AI call: a recent result is returned instead, and a key starts only so many a day.
5sos:activity
Read 5SOS activity for every client: contacts (stage, campaign, HV date, link opens), calls, SMS, disposition events, unique RP/RG events, CallTools campaigns and live filters, and the 5SOS reports. Ids, dates, counts, agents and our own numbers only: no homeowner name, phone, email or address, no message or transcript text, no audio. It covers every client's homeowners, so a key with it is for CIM staff and tools and never goes to a client.
5sos:personal
Adds homeowner personal data to the 5SOS calls that ask for it: include=homeowner adds the homeowner's name, phone, email and address and their side of each call and SMS (and names in report rows); include=text adds SMS bodies, call transcripts and the call-scoring notes, which carry names and addresses too. Also allows the X-Phone lookup. It covers every client's homeowners: CIM staff only, never a client. Needs 5sos:activity as well.
5sos:recordings
Play a 5SOS call's recording: GET /5sos/calls/{callId}/recording streams the audio through CIM (CallTools keeps it about 60 days). It covers every client's calls: CIM staff only, never a client.
List clients
GET/api/v1/clients
Clients ordered by client number, newest first. Page with cursor = the previous page's meta.nextCursor (null on the last page). liveProductCounts counts Active and Paused products.
Access
Needs an API key with clients:read. clients:contacts adds primaryContact to each client.
Parameters
Name
In
Type
Description
status
query
one of Active | Paused | Inactive
Only clients with this status (client_status).
q
query
string
Search: part of the client name, or an exact client number.
404 · Not found (code not_found), or the product exists under another type (code wrong_product_type, with href).
Response 404
{
"error": {
"code": "wrong_product_type",
"message": "013-1-2 is an lsa product, not a maps product.",
"href": "/api/v1/lsa/013-1-2"
}
}
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · A dependency (database, Twilio, GHL or Local Viking) is unavailable (code upstream_unavailable), or the Local Viking session needs reconnecting (code local_viking_session), or Local Viking did not answer in time (code timed_out: the change may still go through, so read the profile before retrying).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "The live check could not run."
}
}
Get a client
GET/api/v1/clients/{clientNumber}
One client with every product (Maps, LSA, 5SOS) and its href. status is clients.client_status. managedService false on an Active client means Long-Term Warranty. monthsActive is computed when the request is served.
Access
Needs an API key with clients:read. clients:contacts adds primaryContact.
Parameters
Name
In
Type
Description
clientNumberrequired
path
string, pattern ^\d{1,6}$
The client number. Leading zeros are accepted ('13' and '013' are the same client).
404 · Not found (code not_found), or the product exists under another type (code wrong_product_type, with href).
Response 404
{
"error": {
"code": "wrong_product_type",
"message": "013-1-2 is an lsa product, not a maps product.",
"href": "/api/v1/lsa/013-1-2"
}
}
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · A dependency (database, Twilio, GHL or Local Viking) is unavailable (code upstream_unavailable), or the Local Viking session needs reconnecting (code local_viking_session), or Local Viking did not answer in time (code timed_out: the change may still go through, so read the profile before retrying).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "The live check could not run."
}
}
List a client's Maps products
GET/api/v1/maps
Every Maps product of one client, each with its cached trackingNumberCheck. trackingNumberCheck is the latest cached result of the tracking-number check (null when the product has never been checked). result is pass | fail | warn | unknown | not_applicable; it is never pass without evidence. numbersChangedSinceCheck is true when the product's tracking or forwarding number was edited after the check ran; a pass is then returned as unknown (the old verdict is about a different number), and ?live=1 re-checks the current numbers. stale is true when the result is older than the cache window. Phone numbers inside checks are masked to the last 4 digits. The check only reads and reports: nothing in this API changes a number.
404 · Not found (code not_found), or the product exists under another type (code wrong_product_type, with href).
Response 404
{
"error": {
"code": "wrong_product_type",
"message": "013-1-2 is an lsa product, not a maps product.",
"href": "/api/v1/lsa/013-1-2"
}
}
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · A dependency (database, Twilio, GHL or Local Viking) is unavailable (code upstream_unavailable), or the Local Viking session needs reconnecting (code local_viking_session), or Local Viking did not answer in time (code timed_out: the change may still go through, so read the profile before retrying).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "The live check could not run."
}
}
Get a Maps product
GET/api/v1/maps/{productId}
One Maps product. businessName is the campaign business name, falling back to the client name. dates.end is unreliable in the source data. trackingNumberCheck is the latest cached result of the tracking-number check (null when the product has never been checked). result is pass | fail | warn | unknown | not_applicable; it is never pass without evidence. numbersChangedSinceCheck is true when the product's tracking or forwarding number was edited after the check ran; a pass is then returned as unknown (the old verdict is about a different number), and ?live=1 re-checks the current numbers. stale is true when the result is older than the cache window. Phone numbers inside checks are masked to the last 4 digits. The check only reads and reports: nothing in this API changes a number.
Access
Needs an API key with maps:read. tracking:check allows ?live=1.
Parameters
Name
In
Type
Description
productIdrequired
path
string, pattern ^\d{1,4}-\d{1,2}-\d{1,2}$
The product id, e.g. 1498-1-1. The client part is normalised to at least 3 digits ('13-1-1' is read as '013-1-1'). The product's client is always taken from the product row, never parsed from this id.
live
query
one of 0 | 1
1 = re-run the tracking-number check now (Twilio + GHL, a few seconds) instead of returning the cached result. Needs the tracking:check scope; 10 per minute per key.
404 · Not found (code not_found), or the product exists under another type (code wrong_product_type, with href).
Response 404
{
"error": {
"code": "wrong_product_type",
"message": "013-1-2 is an lsa product, not a maps product.",
"href": "/api/v1/lsa/013-1-2"
}
}
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · A dependency (database, Twilio, GHL or Local Viking) is unavailable (code upstream_unavailable), or the Local Viking session needs reconnecting (code local_viking_session), or Local Viking did not answer in time (code timed_out: the change may still go through, so read the profile before retrying).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "The live check could not run."
}
}
List a Maps product's Local Viking geogrid scans
GET/api/v1/maps/{productId}/scans
The product's geogrid scans from the Local Viking mirror, newest first, in a date range. only=top3 keeps scans with at least one top 3 node; only=newTop3 keeps scans where a node reached the top 3 for the first time (judged against every earlier scan, not just the range). Scans run weekly, mostly on Mondays. Top 3 is read from the grid (a node ranked 1 to 3), not from Local Viking's stored flags, which are unreliable. A new top 3 node is one in the top 3 for the first time since the first scan of the same keyword and grid layout (that first scan is the baseline: baseline true, newTop3Nodes 0). ranks is the grid row by row; null is not ranked.
Access
Needs an API key with maps:read.
Parameters
Name
In
Type
Description
productIdrequired
path
string, pattern ^\d{1,4}-\d{1,2}-\d{1,2}$
The Maps product.
since
query
string
First scan date, inclusive (YYYY-MM-DD). Default: 90 days before until.
until
query
string
Last scan date, exclusive (YYYY-MM-DD). Default: tomorrow.
keyword
query
string
Only this search term (exact, any case).
only
query
one of top3 | newTop3
top3 or newTop3.
limit
query
integer
Page size.
cursor
query
string
meta.nextCursor from the previous page (null on the last page).
404 · No such Maps product (not_found), it has no Maps place id (no_place_id), or, for four-month, no access verification date (no_access_verification).
Response 404
{
"error": {
"code": "no_place_id",
"message": "013-1-1 has no Maps place id, so it has no scans."
}
}
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · A dependency (database, Twilio, GHL or Local Viking) is unavailable (code upstream_unavailable), or the Local Viking session needs reconnecting (code local_viking_session), or Local Viking did not answer in time (code timed_out: the change may still go through, so read the profile before retrying).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "The live check could not run."
}
}
Get a Maps product's 4-month guarantee scans
GET/api/v1/maps/{productId}/scans/four-month
The 4-month guarantee window: from the access verification date to 4 months later, inclusive (complete false while it is still running). For each keyword and grid layout scanned in the window: the first scan (baselineScan) and the last (fourMonthScan, the 4-month scan; null when there was only one). firstNewTop3 is the guarantee's test, as the 4-month guarantee service runs it: the earliest node to go from outside the top 3 at baseline into the top 3 for the first time (null if none). It does not drop branded or city-named keywords; use keyword to check one. guaranteeMet is the value stored on the product. Top 3 is read from the grid (a node ranked 1 to 3), not from Local Viking's stored flags, which are unreliable. A new top 3 node is one in the top 3 for the first time since the first scan of the same keyword and grid layout (that first scan is the baseline: baseline true, newTop3Nodes 0). ranks is the grid row by row; null is not ranked.
404 · No such Maps product (not_found), it has no Maps place id (no_place_id), or, for four-month, no access verification date (no_access_verification).
Response 404
{
"error": {
"code": "no_place_id",
"message": "013-1-1 has no Maps place id, so it has no scans."
}
}
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · A dependency (database, Twilio, GHL or Local Viking) is unavailable (code upstream_unavailable), or the Local Viking session needs reconnecting (code local_viking_session), or Local Viking did not answer in time (code timed_out: the change may still go through, so read the profile before retrying).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "The live check could not run."
}
}
Get a Maps product's Competitor Check
GET/api/v1/maps/{productId}/competitors
The competitors from the product's latest Local Viking scan with at least reviewFloor Google reviews (never one of our own clients), each checked three ways: a keyword-stuffed name, an address that is residential, a PO box or a virtual mailbox (USPS), and possible fake five-star reviews. Each check is a line marked ❌ (a violation), ➖ (the check could not be made) or ✔️ (clean); a competitor's dot is red with any ❌, else yellow with any ➖, else green, the Power Audit's colours. Worst first, then most reviews. The product's dot is its worst competitor's (green with none), yellow when its newest scan is too old to grade (grade Stale), null when Local Viking holds no scan (grade No Data). The results are those of the last Check all (lastRunAt).
404 · No such Maps product (not_found), not Active or Paused (not_listed), or not checked yet (not_checked).
Response 404
{
"error": {
"code": "not_checked",
"message": "Maps product 013-1-1 has not been through Competitor Check yet (Admin → Competitor Check → Check all)."
}
}
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · A dependency (database, Twilio, GHL or Local Viking) is unavailable (code upstream_unavailable), or the Local Viking session needs reconnecting (code local_viking_session), or Local Viking did not answer in time (code timed_out: the change may still go through, so read the profile before retrying).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "The live check could not run."
}
}
Get an LSA product
GET/api/v1/lsa/{productId}
One LSA product (client.gga). lifecycle is account_status, the real lifecycle field. lsaType and fulfillmentTier are the stored GGA values. There is no tierSold for LSA: the source field is never filled, so fulfillmentTier is the tier. trackingNumberCheck is the latest cached result of the tracking-number check (null when the product has never been checked). result is pass | fail | warn | unknown | not_applicable; it is never pass without evidence. numbersChangedSinceCheck is true when the product's tracking or forwarding number was edited after the check ran; a pass is then returned as unknown (the old verdict is about a different number), and ?live=1 re-checks the current numbers. stale is true when the result is older than the cache window. Phone numbers inside checks are masked to the last 4 digits. The check only reads and reports: nothing in this API changes a number.
Access
Needs an API key with lsa:read. tracking:check allows ?live=1.
Parameters
Name
In
Type
Description
productIdrequired
path
string, pattern ^\d{1,4}-\d{1,2}-\d{1,2}$
The product id, e.g. 1498-1-1. The client part is normalised to at least 3 digits ('13-1-1' is read as '013-1-1'). The product's client is always taken from the product row, never parsed from this id.
live
query
one of 0 | 1
1 = re-run the tracking-number check now (Twilio + GHL, a few seconds) instead of returning the cached result. Needs the tracking:check scope; 10 per minute per key.
404 · Not found (code not_found), or the product exists under another type (code wrong_product_type, with href).
Response 404
{
"error": {
"code": "wrong_product_type",
"message": "013-1-2 is an lsa product, not a maps product.",
"href": "/api/v1/lsa/013-1-2"
}
}
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · A dependency (database, Twilio, GHL or Local Viking) is unavailable (code upstream_unavailable), or the Local Viking session needs reconnecting (code local_viking_session), or Local Viking did not answer in time (code timed_out: the change may still go through, so read the profile before retrying).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "The live check could not run."
}
}
Get an LSA product (alias)
GET/api/v1/gga/{productId}
Alias of /lsa/{productId}: the same response.
Access
Needs an API key with lsa:read. tracking:check allows ?live=1.
Parameters
Name
In
Type
Description
productIdrequired
path
string, pattern ^\d{1,4}-\d{1,2}-\d{1,2}$
The product id, e.g. 1498-1-1. The client part is normalised to at least 3 digits ('13-1-1' is read as '013-1-1'). The product's client is always taken from the product row, never parsed from this id.
live
query
one of 0 | 1
1 = re-run the tracking-number check now (Twilio + GHL, a few seconds) instead of returning the cached result. Needs the tracking:check scope; 10 per minute per key.
404 · Not found (code not_found), or the product exists under another type (code wrong_product_type, with href).
Response 404
{
"error": {
"code": "wrong_product_type",
"message": "013-1-2 is an lsa product, not a maps product.",
"href": "/api/v1/lsa/013-1-2"
}
}
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · A dependency (database, Twilio, GHL or Local Viking) is unavailable (code upstream_unavailable), or the Local Viking session needs reconnecting (code local_viking_session), or Local Viking did not answer in time (code timed_out: the change may still go through, so read the profile before retrying).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "The live check could not run."
}
}
List a client's 5SOS products
GET/api/v1/5sos
Every 5SOS product of one client, each with its review locations.
404 · Not found (code not_found), or the product exists under another type (code wrong_product_type, with href).
Response 404
{
"error": {
"code": "wrong_product_type",
"message": "013-1-2 is an lsa product, not a maps product.",
"href": "/api/v1/lsa/013-1-2"
}
}
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · A dependency (database, Twilio, GHL or Local Viking) is unavailable (code upstream_unavailable), or the Local Viking session needs reconnecting (code local_viking_session), or Local Viking did not answer in time (code timed_out: the change may still go through, so read the profile before retrying).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "The live check could not run."
}
}
Get a 5SOS product
GET/api/v1/5sos/{productId}
One 5SOS product with its review locations. setupStatus is the real setup pipeline (onboarding_status is not returned: it is the same on every row).
Access
Needs an API key with 5sos:read.
Parameters
Name
In
Type
Description
productIdrequired
path
string, pattern ^\d{1,4}-\d{1,2}-\d{1,2}$
The product id, e.g. 1498-1-1. The client part is normalised to at least 3 digits ('13-1-1' is read as '013-1-1'). The product's client is always taken from the product row, never parsed from this id.
404 · Not found (code not_found), or the product exists under another type (code wrong_product_type, with href).
Response 404
{
"error": {
"code": "wrong_product_type",
"message": "013-1-2 is an lsa product, not a maps product.",
"href": "/api/v1/lsa/013-1-2"
}
}
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · A dependency (database, Twilio, GHL or Local Viking) is unavailable (code upstream_unavailable), or the Local Viking session needs reconnecting (code local_viking_session), or Local Viking did not answer in time (code timed_out: the change may still go through, so read the profile before retrying).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "The live check could not run."
}
}
List the tracking-number audit
GET/api/v1/tracking-checks
Every live (Active or Paused) Maps and LSA product with its tracking-number audit. meta.byStatus counts the main statuses of the rows returned. The tracking-number audit (Admin → Product Audits → Tracking Number), one row per product. status is the main status (pass | fail | warn | unknown | not_applicable, or unchecked when never checked). subStatuses has one entry per check, in setup order: tn_on_file (tracking number saved, +1 format), tn_unique (no other live product uses it), fwd_on_file (Forwarding Number saved), twilio_number_present (in our Twilio), twilio_routing (spam screen for Maps; straight to GHL for LSA), twilio_fallback (calls fall back to GHL if the spam screen fails), twilio_sms (texts go straight to GHL), ghl_number_present (in the product's GHL sub-account), ghl_forwarding (GHL forwards to the Forwarding Number), recent_calls (real calls rang it); each is pass | fail | warn | unknown | skipped | not_applicable. trackingNumberCheck carries the same with labels and details. Results are the last check (weekday sweep, or Re-check); numbers are masked.
Access
Needs an API key with tracking:read.
Parameters
Name
In
Type
Description
type
query
one of maps | lsa
Only Maps or only LSA products.
status
query
one of pass | fail | warn | unknown | not_applicable | unchecked
Only products with this main status. unchecked = never checked yet.
404 · Not found (code not_found), or the product exists under another type (code wrong_product_type, with href).
Response 404
{
"error": {
"code": "wrong_product_type",
"message": "013-1-2 is an lsa product, not a maps product.",
"href": "/api/v1/lsa/013-1-2"
}
}
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · A dependency (database, Twilio, GHL or Local Viking) is unavailable (code upstream_unavailable), or the Local Viking session needs reconnecting (code local_viking_session), or Local Viking did not answer in time (code timed_out: the change may still go through, so read the profile before retrying).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "The live check could not run."
}
}
Get one product's tracking-number audit
GET/api/v1/tracking-checks/{productId}
The tracking-number audit of one Maps or LSA product (any product status). The tracking-number audit (Admin → Product Audits → Tracking Number), one row per product. status is the main status (pass | fail | warn | unknown | not_applicable, or unchecked when never checked). subStatuses has one entry per check, in setup order: tn_on_file (tracking number saved, +1 format), tn_unique (no other live product uses it), fwd_on_file (Forwarding Number saved), twilio_number_present (in our Twilio), twilio_routing (spam screen for Maps; straight to GHL for LSA), twilio_fallback (calls fall back to GHL if the spam screen fails), twilio_sms (texts go straight to GHL), ghl_number_present (in the product's GHL sub-account), ghl_forwarding (GHL forwards to the Forwarding Number), recent_calls (real calls rang it); each is pass | fail | warn | unknown | skipped | not_applicable. trackingNumberCheck carries the same with labels and details. Results are the last check (weekday sweep, or Re-check); numbers are masked.
Access
Needs an API key with tracking:read.
Parameters
Name
In
Type
Description
productIdrequired
path
string, pattern ^\d{1,4}-\d{1,2}-\d{1,2}$
The product id, e.g. 1498-1-1. The client part is normalised to at least 3 digits ('13-1-1' is read as '013-1-1'). The product's client is always taken from the product row, never parsed from this id.
404 · Not found (code not_found), or the product exists under another type (code wrong_product_type, with href).
Response 404
{
"error": {
"code": "wrong_product_type",
"message": "013-1-2 is an lsa product, not a maps product.",
"href": "/api/v1/lsa/013-1-2"
}
}
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · A dependency (database, Twilio, GHL or Local Viking) is unavailable (code upstream_unavailable), or the Local Viking session needs reconnecting (code local_viking_session), or Local Viking did not answer in time (code timed_out: the change may still go through, so read the profile before retrying).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "The live check could not run."
}
}
Get a Maps product's Google Business Profile
GET/api/v1/gbp/{productId}
Every profile field and every photo and video, in one call. The product's Google Business Profile as Local Viking holds it, found by the product's Google place id. location and each media item are Local Viking's own records with Local Viking's field names, and a PATCH takes the same names and shapes (for service_area, only places.place_infos). owner is added to each photo: business (uploaded by the business, can be deleted) or customer (uploaded by a customer; Google lets a business report it but not delete it). The editable fields (the ones PATCH takes) are read from Local Viking's own editor through the signed-in session, so they are current right after an edit; Local Viking's public copy can lag behind one. When the session is unavailable they come from the public copy, special_hours and opening_date are null, and meta.warnings says so.
Access
Needs an API key with gbp:read or gbp:write.
Parameters
Name
In
Type
Description
productIdrequired
path
string, pattern ^\d{1,4}-\d{1,2}-\d{1,2}$
The product id, e.g. 1498-1-1. The client part is normalised to at least 3 digits ('13-1-1' is read as '013-1-1'). The product's client is always taken from the product row, never parsed from this id.
404 · Not found (code not_found), or the product exists under another type (code wrong_product_type, with href).
Response 404
{
"error": {
"code": "wrong_product_type",
"message": "013-1-2 is an lsa product, not a maps product.",
"href": "/api/v1/lsa/013-1-2"
}
}
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · A dependency (database, Twilio, GHL or Local Viking) is unavailable (code upstream_unavailable), or the Local Viking session needs reconnecting (code local_viking_session), or Local Viking did not answer in time (code timed_out: the change may still go through, so read the profile before retrying).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "The live check could not run."
}
}
Edit a Maps product's Google Business Profile
PATCH/api/v1/gbp/{productId}
Send only the fields to change, with the names and shapes the GET returns: name, description, phone_number, additional_phone_numbers, website_url, labels, primary_category, additional_categories (category names, as the GET shows them), regular_hours, special_hours, service_area (places.place_infos only), opening_date. Nested objects take only the keys shown in the examples. Every lookup (current values, category ids) happens before the first change, so an invalid or unreadable edit changes nothing. Each section is then saved the way Local Viking's own profile editor saves it, one after another; if one is refused or Local Viking stops answering, the call stops and applied lists the sections already saved. A call answers within 50 s; when Local Viking runs out of time (503) the change in progress may still go through, so read the profile before retrying. Only Active and Paused products can be edited. The address cannot be changed here (an address change can send the profile back to Google verification): change it in Local Viking. Edits reach Google within minutes; Google may take longer to show them and may review a name or category change. Every call is logged with the key that made it.
Access
Needs an API key with gbp:write.
Parameters
Name
In
Type
Description
productIdrequired
path
string, pattern ^\d{1,4}-\d{1,2}-\d{1,2}$
The product id, e.g. 1498-1-1. The client part is normalised to at least 3 digits ('13-1-1' is read as '013-1-1'). The product's client is always taken from the product row, never parsed from this id.
Body
The fields to change (any of the editable fields; at least one). JSON, Content-Type: application/json.
Request
curl -X PATCH -H "Authorization: Bearer $CIM_API_KEY" -H "Content-Type: application/json" -d '{"description":"Example Roofing Co repairs and replaces roofs across Springfield and Chatham.","regular_hours":{"periods":[{"open_day":"MONDAY","close_day":"MONDAY","open_time":"07:00","close_time":"17:00"}]}}' "https://admin.contractorindependence.com/api/v1/gbp/013-1-1"
404 · Not found (code not_found), or the product exists under another type (code wrong_product_type, with href).
Response 404
{
"error": {
"code": "wrong_product_type",
"message": "013-1-2 is an lsa product, not a maps product.",
"href": "/api/v1/lsa/013-1-2"
}
}
409 · Not possible on this profile: the photo was uploaded by a customer (code customer_photo: Google lets a business delete only its own photos; report it with takedownUrl), the product is Inactive (code product_not_live: its profile can be read, not changed), its Local Viking record is disabled (code local_viking_disabled), or the same photo was added in the last 10 minutes (code duplicate_photo).
Response 409
{
"error": {
"code": "customer_photo",
"message": "zx12ab35 was uploaded by a customer: Google does not let a business delete it.",
"takedownUrl": "https://www.google.com/local/content/rap/report?postId=example"
}
}
413 · The body or the photo is too large (code too_large). Bodies over 5 MB are refused at the edge with a plain 413 page before reaching the API.
Response 413
{
"error": {
"code": "too_large",
"message": "Photos are limited to 15 MB."
}
}
415 · The body is not JSON (code unsupported_media_type). Send Content-Type: application/json.
Response 415
{
"error": {
"code": "unsupported_media_type",
"message": "Send the body as JSON with Content-Type: application/json."
}
}
422 · The edit is invalid (code invalid_edit, nothing changed), or Local Viking or Google refused a section (code local_viking_rejected). Once any section has been saved, every error (422 or 503) carries `applied`: the fields already live.
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · A dependency (database, Twilio, GHL or Local Viking) is unavailable (code upstream_unavailable), or the Local Viking session needs reconnecting (code local_viking_session), or Local Viking did not answer in time (code timed_out: the change may still go through, so read the profile before retrying).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "The live check could not run."
}
}
Add a photo to a Maps product's Google Business Profile
POST/api/v1/gbp/{productId}/media
Uploads one JPEG or PNG (base64) to the profile through Local Viking, the way Local Viking's own upload does it. Request bodies are limited to 5 MB at the edge, so send photos up to about 3.5 MB (base64 grows by a third; a JPEG at most 2048 px on its long side is plenty for Google). category is one of additional, cover, profile, logo, exterior, interior, product, at_work, teams. The photo goes live on Google after Google processes it, usually within minutes. mediaId is the new photo's Local Viking id when Local Viking returns one, else null. A cover must be 16:9 or Google refuses it. Every call is logged with the key that made it.
Access
Needs an API key with gbp:write.
Parameters
Name
In
Type
Description
productIdrequired
path
string, pattern ^\d{1,4}-\d{1,2}-\d{1,2}$
The product id, e.g. 1498-1-1. The client part is normalised to at least 3 digits ('13-1-1' is read as '013-1-1'). The product's client is always taken from the product row, never parsed from this id.
Body
The photo (base64 of the file bytes) and its category. JSON, Content-Type: application/json.
Request
curl -X POST -H "Authorization: Bearer $CIM_API_KEY" -H "Content-Type: application/json" -d '{"category":"additional","imageBase64":"<base64 of a JPEG or PNG>","description":"Crew on a re-roof in Springfield"}' "https://admin.contractorindependence.com/api/v1/gbp/013-1-1/media"
404 · Not found (code not_found), or the product exists under another type (code wrong_product_type, with href).
Response 404
{
"error": {
"code": "wrong_product_type",
"message": "013-1-2 is an lsa product, not a maps product.",
"href": "/api/v1/lsa/013-1-2"
}
}
409 · Not possible on this profile: the photo was uploaded by a customer (code customer_photo: Google lets a business delete only its own photos; report it with takedownUrl), the product is Inactive (code product_not_live: its profile can be read, not changed), its Local Viking record is disabled (code local_viking_disabled), or the same photo was added in the last 10 minutes (code duplicate_photo).
Response 409
{
"error": {
"code": "customer_photo",
"message": "zx12ab35 was uploaded by a customer: Google does not let a business delete it.",
"takedownUrl": "https://www.google.com/local/content/rap/report?postId=example"
}
}
413 · The body or the photo is too large (code too_large). Bodies over 5 MB are refused at the edge with a plain 413 page before reaching the API.
Response 413
{
"error": {
"code": "too_large",
"message": "Photos are limited to 15 MB."
}
}
415 · The body is not JSON (code unsupported_media_type). Send Content-Type: application/json.
Response 415
{
"error": {
"code": "unsupported_media_type",
"message": "Send the body as JSON with Content-Type: application/json."
}
}
422 · The edit is invalid (code invalid_edit, nothing changed), or Local Viking or Google refused a section (code local_viking_rejected). Once any section has been saved, every error (422 or 503) carries `applied`: the fields already live.
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · A dependency (database, Twilio, GHL or Local Viking) is unavailable (code upstream_unavailable), or the Local Viking session needs reconnecting (code local_viking_session), or Local Viking did not answer in time (code timed_out: the change may still go through, so read the profile before retrying).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "The live check could not run."
}
}
Delete a business photo from a Maps product's Google Business Profile
DELETE/api/v1/gbp/{productId}/media/{mediaId}
Deletes one photo or video the business uploaded (owner = business). Before deleting, the record and the image are saved on CIM's server, so it can be put back. A customer's photo cannot be deleted by the business: the call returns 409 customer_photo with the Google report link. Removal from the public profile follows within minutes. Every call is logged with the key that made it.
Access
Needs an API key with gbp:write.
Parameters
Name
In
Type
Description
productIdrequired
path
string, pattern ^\d{1,4}-\d{1,2}-\d{1,2}$
The product id, e.g. 1498-1-1. The client part is normalised to at least 3 digits ('13-1-1' is read as '013-1-1'). The product's client is always taken from the product row, never parsed from this id.
mediaIdrequired
path
string, pattern ^[a-z0-9]{6,12}$
The photo's id: media[].id from GET /gbp/{productId}.
404 · Not found (code not_found), or the product exists under another type (code wrong_product_type, with href).
Response 404
{
"error": {
"code": "wrong_product_type",
"message": "013-1-2 is an lsa product, not a maps product.",
"href": "/api/v1/lsa/013-1-2"
}
}
409 · Not possible on this profile: the photo was uploaded by a customer (code customer_photo: Google lets a business delete only its own photos; report it with takedownUrl), the product is Inactive (code product_not_live: its profile can be read, not changed), its Local Viking record is disabled (code local_viking_disabled), or the same photo was added in the last 10 minutes (code duplicate_photo).
Response 409
{
"error": {
"code": "customer_photo",
"message": "zx12ab35 was uploaded by a customer: Google does not let a business delete it.",
"takedownUrl": "https://www.google.com/local/content/rap/report?postId=example"
}
}
413 · The body or the photo is too large (code too_large). Bodies over 5 MB are refused at the edge with a plain 413 page before reaching the API.
Response 413
{
"error": {
"code": "too_large",
"message": "Photos are limited to 15 MB."
}
}
415 · The body is not JSON (code unsupported_media_type). Send Content-Type: application/json.
Response 415
{
"error": {
"code": "unsupported_media_type",
"message": "Send the body as JSON with Content-Type: application/json."
}
}
422 · The edit is invalid (code invalid_edit, nothing changed), or Local Viking or Google refused a section (code local_viking_rejected). Once any section has been saved, every error (422 or 503) carries `applied`: the fields already live.
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · A dependency (database, Twilio, GHL or Local Viking) is unavailable (code upstream_unavailable), or the Local Viking session needs reconnecting (code local_viking_session), or Local Viking did not answer in time (code timed_out: the change may still go through, so read the profile before retrying).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "The live check could not run."
}
}
List the GBP photo checks
GET/api/v1/gbp-photos
Every product GBP Photo Verification lists, with its cover and search photo grades. meta.byStatus counts the overall statuses of the rows returned. One row per product the GBP Photo Verification page lists: every Active or Paused Maps product with a Google place id, except the tiers it leaves out (Long-Term Warranty) and products Local Viking has no active connection to (Admin → Local Viking). The same rows and grades the page shows. overall.status is the photo status: green = cover photo Good and search photo Good; red = cover photo Bad and search photo Bad (no cover photo counts as Bad); orange = any other pair of grades (a mix, OK and OK included); unchecked = never checked, a check is running now, the last check failed, or either photo isn't graded yet. overall.cover and overall.search say whether each piece is done (ok) and, when not, what is missing (why). overall.checkedAt is when the latest check finished; stale is true when that is longer ago than the stale window (meta.staleHours, 24 by default) or there is none. While a check runs (anyone's: the page, Check all, the API), the row is unchecked (why: 'Checking now.'), checkedAt is null and cover and search are null until it finishes: keep the previous result rather than writing that over it. cover.verdict is ideal (Good) | okay (OK) | not (Bad) | none (no cover photo) | unscreened (not graded); search.grade is green (Good) | yellow (OK) | red (Bad); url is the photo itself. When the business isn't in the mobile results, or its card shows no image, the check grades photo #1 instead (the profile's cover photo, else its newest photo, else Google's first photo), and search.url and search.mediaId are that photo (mediaId null for Google's first photo). cover and search are null until a check has finished. idealSearchPhoto: showing (the search photo is already Good) | on_profile (a photo that would grade Good is on the profile) | none (no photo on the profile would) | unknown. manualPass is staff signing the current photo off as good enough (who and when); it changes no grade. localViking is the check's Local Viking connection (null on checks made before it was recorded).
Access
Needs an API key with gbp:read or gbp:check.
Parameters
Name
In
Type
Description
clientNumber
query
string, pattern ^\d{1,6}$
Only this client's products.
status
query
one of green | orange | red | unchecked
Only products with this overall photo status: green = both photos Good, red = both Bad, orange = a mix. unchecked = never checked, checking now, the last check failed, or a photo not graded yet.
stale
query
one of 0 | 1
1 = only products whose latest check is older than the stale window (or that have none): the ones to refresh. 0 = only fresh ones.
404 · Not found (code not_found), or the product exists under another type (code wrong_product_type, with href).
Response 404
{
"error": {
"code": "wrong_product_type",
"message": "013-1-2 is an lsa product, not a maps product.",
"href": "/api/v1/lsa/013-1-2"
}
}
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · A dependency (database, Twilio, GHL or Local Viking) is unavailable (code upstream_unavailable), or the Local Viking session needs reconnecting (code local_viking_session), or Local Viking did not answer in time (code timed_out: the change may still go through, so read the profile before retrying).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "The live check could not run."
}
}
Get one product's GBP photo check
GET/api/v1/gbp-photos/{productId}
One product's cover and search photo grades. A product the page doesn't list is 404 not_listed with the reason (Inactive, no place id, Long-Term Warranty tier, no Local Viking connection). One row per product the GBP Photo Verification page lists: every Active or Paused Maps product with a Google place id, except the tiers it leaves out (Long-Term Warranty) and products Local Viking has no active connection to (Admin → Local Viking). The same rows and grades the page shows. overall.status is the photo status: green = cover photo Good and search photo Good; red = cover photo Bad and search photo Bad (no cover photo counts as Bad); orange = any other pair of grades (a mix, OK and OK included); unchecked = never checked, a check is running now, the last check failed, or either photo isn't graded yet. overall.cover and overall.search say whether each piece is done (ok) and, when not, what is missing (why). overall.checkedAt is when the latest check finished; stale is true when that is longer ago than the stale window (meta.staleHours, 24 by default) or there is none. While a check runs (anyone's: the page, Check all, the API), the row is unchecked (why: 'Checking now.'), checkedAt is null and cover and search are null until it finishes: keep the previous result rather than writing that over it. cover.verdict is ideal (Good) | okay (OK) | not (Bad) | none (no cover photo) | unscreened (not graded); search.grade is green (Good) | yellow (OK) | red (Bad); url is the photo itself. When the business isn't in the mobile results, or its card shows no image, the check grades photo #1 instead (the profile's cover photo, else its newest photo, else Google's first photo), and search.url and search.mediaId are that photo (mediaId null for Google's first photo). cover and search are null until a check has finished. idealSearchPhoto: showing (the search photo is already Good) | on_profile (a photo that would grade Good is on the profile) | none (no photo on the profile would) | unknown. manualPass is staff signing the current photo off as good enough (who and when); it changes no grade. localViking is the check's Local Viking connection (null on checks made before it was recorded).
Access
Needs an API key with gbp:read or gbp:check.
Parameters
Name
In
Type
Description
productIdrequired
path
string, pattern ^\d{1,4}-\d{1,2}-\d{1,2}$
The product id, e.g. 1498-1-1. The client part is normalised to at least 3 digits ('13-1-1' is read as '013-1-1'). The product's client is always taken from the product row, never parsed from this id.
404 · Not found (code not_found), or the product exists under another type (code wrong_product_type, with href).
Response 404
{
"error": {
"code": "wrong_product_type",
"message": "013-1-2 is an lsa product, not a maps product.",
"href": "/api/v1/lsa/013-1-2"
}
}
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · A dependency (database, Twilio, GHL or Local Viking) is unavailable (code upstream_unavailable), or the Local Viking session needs reconnecting (code local_viking_session), or Local Viking did not answer in time (code timed_out: the change may still go through, so read the profile before retrying).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "The live check could not run."
}
}
Run a fresh GBP photo check of one product
POST/api/v1/gbp-photos/{productId}/check
Runs a fresh photo check of one product, or says why it didn't, so a Power Audit run can refresh a product without paying twice. Each check is a paid AI call, so: a check that finished within the last freshMinutes (60 by default) is returned as it is (200, fresh: true, result = that product as GET /gbp-photos/{productId} returns it); a check already running is never doubled, and none starts while an Auto-cycle or a change is under way on the product's Google profile (202, started: false, running: true); otherwise a check starts (202, started: true, checkId). A check usually takes 15 to 40 seconds, sometimes up to 90, and up to 3 minutes for a profile with many photos never checked before (each photo is graded as the cover once): poll GET /gbp-photos/{productId} every 5 to 10 seconds until latestCheck.status is no longer running (done or failed), then read overall. While a check runs, overall is unchecked (why: 'Checking now.') with no checkedAt: keep the previous result until it finishes. A key starts at most dailyChecks.limit checks a day (UTC; 40 by default); past it the call is 429 daily_limit_reached until 00:00 UTC, and reads still work. Only products GET /gbp-photos lists can be checked. No body.
Access
Needs an API key with gbp:check.
Parameters
Name
In
Type
Description
productIdrequired
path
string, pattern ^\d{1,4}-\d{1,2}-\d{1,2}$
The product id, e.g. 1498-1-1. The client part is normalised to at least 3 digits ('13-1-1' is read as '013-1-1'). The product's client is always taken from the product row, never parsed from this id.
Request
curl -X POST -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/gbp-photos/013-1-1/check"
Response 200 · success
{
"data": {
"productId": "013-1-1",
"started": false,
"fresh": true,
"running": false,
"checkId": 412,
"message": "Checked 12 minutes ago, within the last 60 minutes: no new check started (each one is a paid AI call). result is that check.",
"result": {
"productId": "013-1-1",
"clientNumber": 13,
"businessName": "Example Roofing Co",
"productStatus": "Active",
"overall": {
"status": "orange",
"cover": {
"ok": true,
"why": null
},
"search": {
"ok": false,
"why": "Search photo is OK, not Good: A crew on a roof, but no sign or logo."
},
"checkedAt": "2026-10-01T12:20:04.000Z",
"stale": false
},
"cover": {
"verdict": "ideal",
"reason": "The team in branded shirts in front of the wrapped truck; the name is easy to read.",
"url": "https://lh3.googleusercontent.com/example-cover=s0",
"mediaId": "cov12ab3"
},
"search": {
"grade": "yellow",
"reason": "A crew on a roof, but no sign or logo.",
"url": "https://lh3.googleusercontent.com/example-search=s0",
"mediaId": "zx12ab34"
},
"idealSearchPhoto": "on_profile",
"photos": {
"total": 48,
"business": 41,
"customer": 7,
"screened": 48,
"ideal": 2
},
"manualPass": {
"cover": null,
"search": {
"by": "[email protected]",
"at": "2026-10-01T15:02:11.000Z"
}
},
"latestCheck": {
"id": 412,
"status": "done",
"startedAt": "2026-10-01T12:19:31.000Z",
"finishedAt": "2026-10-01T12:20:04.000Z",
"error": null
},
"localViking": {
"connected": true,
"why": null
},
"href": "/api/v1/gbp-photos/013-1-1",
"productHref": "/api/v1/maps/013-1-1",
"clientHref": "/api/v1/clients/13"
},
"dailyChecks": {
"started": 2,
"limit": 40
},
"freshMinutes": 60,
"href": "/api/v1/gbp-photos/013-1-1"
},
"meta": {
"apiVersion": "v1",
"requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
"generatedAt": "2026-10-01T14:00:00.000Z"
}
}
202: A check started (started: true, checkId), or one was already running (started: false, running: true). Poll GET /gbp-photos/{productId}.
Response 202 · accepted
{
"data": {
"productId": "013-1-1",
"started": true,
"fresh": false,
"running": true,
"checkId": 413,
"message": "Check 413 started. It usually takes 15 to 40 seconds, sometimes up to 90 (up to 3 minutes for a profile with many photos never checked before). Poll GET /api/v1/gbp-photos/013-1-1 until latestCheck.status is no longer running.",
"result": null,
"dailyChecks": {
"started": 3,
"limit": 40
},
"freshMinutes": 60,
"href": "/api/v1/gbp-photos/013-1-1"
},
"meta": {
"apiVersion": "v1",
"requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
"generatedAt": "2026-10-01T14:00:00.000Z"
}
}
Errors
400 · Bad request: an id or query parameter is malformed (code invalid_id or invalid_parameter).
Response 400
{
"error": {
"code": "invalid_id",
"message": "productId must look like 1498-1-1"
}
}
401 · No key, an unknown key, a revoked key or an expired key (code unauthorized).
Response 401
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}
403 · The key lacks the scope this call needs (code forbidden_scope).
429 · This key has started its daily number of photo checks (code daily_limit_reached; Retry-After = seconds to 00:00 UTC; reads still work), or the per-key request limit (code rate_limited).
Response 429
{
"error": {
"code": "daily_limit_reached",
"message": "This key has started 40 photo checks today (UTC), its daily limit: each one is a paid AI call. Read the latest results with GET /api/v1/gbp-photos/013-1-1, or check again after 00:00 UTC."
}
}
503 · Photo checks are not set up on the server (code not_configured), or a dependency is unavailable (code upstream_unavailable).
Response 503
{
"error": {
"code": "not_configured",
"message": "Photo checks are not set up on the server."
}
}
List 5SOS contacts
GET/api/v1/5sos/contacts
5SOS contacts (one homeowner job each) of one client, newest first: clientNumber or productId is required unless the X-Phone header is sent. Page with cursor = the previous page's meta.nextCursor (null on the last page).
stage is the contact's latest contact disposition (initial when it has none). campaign.joined lists the CIM campaigns the contact joined, by the same test as the campaign filter (a Bulk Upload Priority contact is bulk_priority, not bulk_upload); campaign.label is the campaign the client's 5SOS page shows (Bulk Upload Priority shows as Bulk Upload). rep is the home-visit inspector. humanLinkOpens counts opens of the review link by people (link scanners are left out). productId is the client's 5SOS product.
homeowner (name, phone, email, address) comes only with include=homeowner, which needs 5sos:personal. So does the X-Phone header: it finds the contacts with that phone across every client (or one client, with clientNumber or productId), in one page of at most 50 (meta.truncated is true when there were more).
Access
Needs an API key with 5sos:activity. 5sos:personal allows include=homeowner (adds homeowner to each contact) and the X-Phone lookup.
Parameters
Name
In
Type
Description
clientNumber
query
string, pattern ^\d{1,6}$
Only this client's 5SOS data. Give clientNumber or productId, not both.
productId
query
string, pattern ^\d{1,4}-\d{1,2}-\d{1,2}$
Only this 5SOS product's data, read as its client (each client has one 5SOS product). Give clientNumber or productId, not both.
stage
query
string
Only contacts at these stages, a comma list of initial, activeConvo, callback, rp, waitingJobComplete, rg, lost, dnd, alreadyReviewed, escalation. The stage is the contact's latest contact disposition in CIM's event log; a contact with none yet is initial.
Only these CIM campaigns, a comma list of post_appt, post_job, bulk_upload, bulk_priority.
hvFrom
query
string, pattern ^\d{4}-\d{2}-\d{2}$
Only contacts whose home visit was on or after this day (YYYY-MM-DD, the day as the client reported it). hvFrom and hvTo are at most 366 days apart.
hvTo
query
string, pattern ^\d{4}-\d{2}-\d{2}$
Only contacts whose home visit was on or before this day (YYYY-MM-DD, the day as the client reported it). hvFrom and hvTo are at most 366 days apart.
limit
query
integer
Page size.
cursor
query
string, pattern ^\d{1,15}$
meta.nextCursor from the previous page (the last contactId; null on the last page). Not with X-Phone.
include
query
string, pattern ^(homeowner)(,(homeowner))*$
Personal data to add, a comma list of homeowner; each needs the 5sos:personal scope (403 without it). homeowner: the homeowner's name, phone, email and address, and their side of each call and SMS.
X-Phone
header
string, pattern ^\+1\d{10}$
Find contacts by the homeowner's phone, +1 and 10 digits (sent as a header so it stays out of URLs and logs). Needs the 5sos:personal scope.
400 · Bad request: an id or query parameter is malformed or out of range (code invalid_id or invalid_parameter).
Response 400
{
"error": {
"code": "invalid_parameter",
"message": "since and until are at most 31 days apart"
}
}
401 · No key, an unknown key, a revoked key or an expired key (code unauthorized).
Response 401
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}
403 · The key lacks the scope this call needs (code forbidden_scope), or asked for personal data (include=homeowner, include=text, the X-Phone header) without the 5sos:personal scope. Checked before anything is read.
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · The database or CallTools is unavailable, or the API's share of CallTools is used up for the minute (code upstream_unavailable, with Retry-After), or CallTools is not set up on the server (code not_configured).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "CallTools is not answering right now. Try again shortly."
}
}
Get a 5SOS contact
GET/api/v1/5sos/contacts/{contactId}
One contact: the list's fields, plus stage.since (when its latest contact disposition was logged), counts and the last call and SMS, its unique RP and RG events, the other contacts at the same client with the same phone (ids only, at most 20; samePhoneTruncated when there are more), and live, its state in CallTools now.
counts, lastCallAt and lastSmsAt come from CIM's mirror of CallTools, which starts on the coverage days: older activity is not counted. In uniqueEvents, agent is the email on the event and creditedAgent the CallTools user on the event, as the calls name its rep (null when the event carries none, or that user has never dialled).
live is read from CallTools (live=0 skips it): meta.live is ok, skipped or unavailable (CallTools down, or the API's share of it used up: live is null and the call still answers; with ok, live is null only when CallTools has no such contact). live.stageMismatch is true when CallTools' contact disposition differs from stage, which can lag it. homeowner only with include=homeowner (5sos:personal).
Access
Needs an API key with 5sos:activity. 5sos:personal allows include=homeowner (adds homeowner).
Parameters
Name
In
Type
Description
contactIdrequired
path
string, pattern ^\d{1,15}$
The 5SOS contact: its CallTools contact id. A contact is one homeowner job for one client.
live
query
one of 0 | 1
1 (the default) adds live, the contact's state in CallTools now; 0 skips CallTools.
include
query
string, pattern ^(homeowner)(,(homeowner))*$
Personal data to add, a comma list of homeowner; each needs the 5sos:personal scope (403 without it). homeowner: the homeowner's name, phone, email and address, and their side of each call and SMS.
400 · Bad request: an id or query parameter is malformed or out of range (code invalid_id or invalid_parameter).
Response 400
{
"error": {
"code": "invalid_parameter",
"message": "since and until are at most 31 days apart"
}
}
401 · No key, an unknown key, a revoked key or an expired key (code unauthorized).
Response 401
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}
403 · The key lacks the scope this call needs (code forbidden_scope), or asked for personal data (include=homeowner, include=text, the X-Phone header) without the 5sos:personal scope. Checked before anything is read.
404 · CIM has no contacts row for this id (code not_found). Its calls, SMS and events may still be on GET /5sos/contacts/{contactId}/timeline.
Response 404
{
"error": {
"code": "not_found",
"message": "No 5SOS contact 900000199 in CIM's contacts. Its calls, SMS and events may still be at /api/v1/5sos/contacts/900000199/timeline."
}
}
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · The database or CallTools is unavailable, or the API's share of CallTools is used up for the minute (code upstream_unavailable, with Retry-After), or CallTools is not set up on the server (code not_configured).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "CallTools is not answering right now. Try again shortly."
}
}
A 5SOS contact's timeline: every call, SMS and event
GET/api/v1/5sos/contacts/{contactId}/timeline
Everything that happened on one contact, in time order: calls, scored calls the calls mirror doesn't hold (kind scored_call), SMS, disposition events, unique RP and RG events, review-link opens (isBot marks link scanners), admin disposition updates and review-verification decisions. Works for any CallTools contact id, with or without a contacts row. Rows in the same second: an admin_update before the contact_disposition it caused, that before its unique event, otherwise by kind and then id. At most 500 rows of each kind's source (meta.truncated says when one was cut).
From CIM's MySQL mirror only. meta.coverage gives the first day each source holds; meta.freshness.callsAsOf when calls last synced (calls and SMS sync every 20 minutes; calls are scored hourly, so transcript.available false on a recent call can mean not yet scored). Known gaps: an inbound call CallTools never attached to a contact is on no timeline; automated SMS have agent null; MMS are not mirrored.
agent: the call's agent, the CallTools user of a person's SMS, the email on an event; on an admin_update the rep who made it, null when it was not a rep (a client user's Do Not Contact, say). A contact_disposition that names its stage without the id has the stage's id in detail.dispositionId. ourNumber is CIM's side of a call or SMS; homeownerNumber (include=homeowner) the other side; both are null when the direction is unknown. campaign is the CallTools dialer campaign (its name is null while CallTools is unavailable); events carry CIM's campaignToken. recording.available: a recording is on record (CallTools keeps audio about 60 days). text (include=text): the SMS body or the call's transcript (A and B are speaker labels, not names), with review links shown as jobreview.co/r/[link]. include needs 5sos:personal.
Access
Needs an API key with 5sos:activity. 5sos:personal allows include=homeowner (adds homeownerNumber) and include=text (adds text).
Parameters
Name
In
Type
Description
contactIdrequired
path
string, pattern ^\d{1,15}$
The 5SOS contact: its CallTools contact id. A contact is one homeowner job for one client.
kinds
query
string
Only these kinds, a comma list of call, scored_call, sms, call_disposition, contact_disposition, email_disposition, sms_disposition, unique, link_open, admin_update, review_decision. Default: all but sms_disposition (it repeats each SMS) and unique (it repeats the contact_disposition RP or RG it came with).
since
query
string
Only rows at or after this instant: an ISO date or date-time, in UTC unless it carries an offset (a bare date means 00:00 UTC). Default: no start.
until
query
string
Only rows before this instant: an ISO date or date-time, in UTC unless it carries an offset (a bare date means 00:00 UTC). Default: no end.
order
query
one of asc | desc
asc (the default): oldest first; desc: newest first (the exact reverse).
Personal data to add, a comma list of homeowner, text; each needs the 5sos:personal scope (403 without it). homeowner: the homeowner's name, phone, email and address, and their side of each call and SMS. text: message and transcript text (review links shown as jobreview.co/r/[link]).
400 · Bad request: an id or query parameter is malformed or out of range (code invalid_id or invalid_parameter).
Response 400
{
"error": {
"code": "invalid_parameter",
"message": "since and until are at most 31 days apart"
}
}
401 · No key, an unknown key, a revoked key or an expired key (code unauthorized).
Response 401
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}
403 · The key lacks the scope this call needs (code forbidden_scope), or asked for personal data (include=homeowner, include=text, the X-Phone header) without the 5sos:personal scope. Checked before anything is read.
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · The database or CallTools is unavailable, or the API's share of CallTools is used up for the minute (code upstream_unavailable, with Retry-After), or CallTools is not set up on the server (code not_configured).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "CallTools is not answering right now. Try again shortly."
}
}
List 5SOS calls
GET/api/v1/5sos/calls
Calls, newest first, in a window of at most 31 days. From CIM's copy of the CallTools log, synced every 20 minutes: the newest minutes may not be here yet, and nothing older than the copy (a contact's timeline gives each source's first day in meta.coverage). ourNumber is our caller ID: the number we dialled from, or the one the homeowner rang. recording.available means CallTools recorded the call; it keeps audio about 60 days, after which the recording answers 410. transcript.available is false on a call not scored yet: the scorer runs hourly and covers outbound calls by active reps with a recording of 15 s or more. callDisposition is the rep's CallTools call disposition; campaign.name comes from CallTools and is null while CallTools is unavailable. An inbound call CallTools never attached to a contact has no contact or client. Report dials count call disposition events (GET /5sos/events), so they differ from this list.
Access
Needs an API key with 5sos:activity. 5sos:personal allows include=homeowner (homeownerNumber) and include=text (the words).
Parameters
Name
In
Type
Description
clientNumber
query
string, pattern ^\d{1,6}$
Only this client's 5SOS data. Give clientNumber or productId, not both.
productId
query
string, pattern ^\d{1,4}-\d{1,2}-\d{1,2}$
Only this 5SOS product's data, read as its client (each client has one 5SOS product). Give clientNumber or productId, not both.
contactId
query
string, pattern ^\d{1,15}$
Only this contact's (its CallTools contact id).
agent
query
string, pattern ^[a-z0-9._-]{1,64}$
Only this agent's: the email local part, e.g. sam.r.
since
query
string
Start of the window, inclusive: an ISO date or date-time, in UTC unless it carries an offset. A bare date means 00:00 UTC, so 2026-10-01 starts at 8 pm New York time on 30 September. Default: 7 days before until. At most 31 days before until.
until
query
string
End of the window, exclusive: an ISO date or date-time, in UTC unless it carries an offset (a bare date means 00:00 UTC). Default: now.
direction
query
one of in | out
in (from the homeowner) or out (from us).
minBillsec
query
integer
Only calls billed at least this many seconds (0 to 3600).
campaignId
query
string, pattern ^\d{1,15}$
Only this CallTools dialer campaign (GET /5sos/campaigns lists them).
transcribed
query
one of 0 | 1
1: only calls with a transcript; 0: only calls without.
limit
query
integer
Page size.
cursor
query
string, pattern ^\d{1,10}_\d{1,20}$
meta.nextCursor from the previous page (null on the last page).
Personal data to add, a comma list of homeowner, text; each needs the 5sos:personal scope (403 without it). homeowner: the homeowner's name, phone, email and address, and their side of each call and SMS. text: message and transcript text (review links shown as jobreview.co/r/[link]).
{
"data": [
{
"id": 2300000001,
"contactId": 900000101,
"clientNumber": 13,
"productId": "013-2-1",
"startedAt": "2026-09-30T15:00:00.000Z",
"endedAt": "2026-09-30T15:02:10.000Z",
"direction": "out",
"durationSec": 130,
"billsec": 115,
"agent": {
"key": "sam.r",
"name": "Sam R"
},
"ourNumber": "+12175550101",
"homeownerNumber": "+12175550123",
"systemDisposition": "Answered",
"callDisposition": {
"id": 900001,
"name": "Answered"
},
"campaign": {
"id": 700001,
"name": "5SOS (Example) Post Appointment"
},
"recording": {
"available": true,
"href": "/api/v1/5sos/calls/2300000001/recording"
},
"transcript": {
"available": true,
"chars": 96,
"truncated": false,
"href": "/api/v1/5sos/calls/2300000001",
"text": "A: Hi, is this Alex? It's Sam from Example Roofing about your inspection.\nB: Yes, that's me.",
"turns": [
{
"speaker": "A",
"text": "Hi, is this Alex? It's Sam from Example Roofing about your inspection."
},
{
"speaker": "B",
"text": "Yes, that's me."
}
]
},
"scored": true,
"href": "/api/v1/5sos/calls/2300000001"
}
],
"meta": {
"apiVersion": "v1",
"requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
"generatedAt": "2026-10-01T14:00:00.000Z",
"limit": 50,
"nextCursor": "1790780400_2300000001",
"since": "2026-09-24T14:00:00.000Z",
"until": "2026-10-01T14:00:00.000Z"
}
}
Errors
400 · Bad request: an id or query parameter is malformed or out of range (code invalid_id or invalid_parameter).
Response 400
{
"error": {
"code": "invalid_parameter",
"message": "since and until are at most 31 days apart"
}
}
401 · No key, an unknown key, a revoked key or an expired key (code unauthorized).
Response 401
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}
403 · The key lacks the scope this call needs (code forbidden_scope), or asked for personal data (include=homeowner, include=text, the X-Phone header) without the 5sos:personal scope. Checked before anything is read.
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · The database or CallTools is unavailable, or the API's share of CallTools is used up for the minute (code upstream_unavailable, with Retry-After), or CallTools is not set up on the server (code not_configured).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "CallTools is not answering right now. Try again shortly."
}
}
Get a 5SOS call
GET/api/v1/5sos/calls/{callId}
One call with its scoring. A scored call older than the calls mirror has inMirror false and only what scoring kept (contact, agent, start, transcript, analysis). analysis is null on a call not scored. include=text adds the transcript (cut at 12,000 characters: truncated) as text and turns, and the scorer's words (bestQuote and its speaker and lever, weakness, deviations, techniqueNote). The turn speakers A, B are voices, not people. outcomeClass is the scorer's reason a call ended without a Review Promised. A transcript is never made on request.
Access
Needs an API key with 5sos:activity. 5sos:personal allows include=homeowner (homeownerNumber) and include=text (the words).
Personal data to add, a comma list of homeowner, text; each needs the 5sos:personal scope (403 without it). homeowner: the homeowner's name, phone, email and address, and their side of each call and SMS. text: message and transcript text (review links shown as jobreview.co/r/[link]).
{
"data": {
"id": 2300000001,
"contactId": 900000101,
"clientNumber": 13,
"productId": "013-2-1",
"startedAt": "2026-09-30T15:00:00.000Z",
"endedAt": "2026-09-30T15:02:10.000Z",
"direction": "out",
"durationSec": 130,
"billsec": 115,
"agent": {
"key": "sam.r",
"name": "Sam R"
},
"ourNumber": "+12175550101",
"homeownerNumber": "+12175550123",
"systemDisposition": "Answered",
"callDisposition": {
"id": 900001,
"name": "Answered"
},
"campaign": {
"id": 700001,
"name": "5SOS (Example) Post Appointment"
},
"recording": {
"available": true,
"href": "/api/v1/5sos/calls/2300000001/recording"
},
"transcript": {
"available": true,
"chars": 96,
"truncated": false,
"href": "/api/v1/5sos/calls/2300000001",
"text": "A: Hi, is this Alex? It's Sam from Example Roofing about your inspection.\nB: Yes, that's me.",
"turns": [
{
"speaker": "A",
"text": "Hi, is this Alex? It's Sam from Example Roofing about your inspection."
},
{
"speaker": "B",
"text": "Yes, that's me."
}
]
},
"scored": true,
"href": "/api/v1/5sos/calls/2300000001",
"inMirror": true,
"analysis": {
"causedRp": true,
"rightPerson": true,
"askMade": true,
"askType": "direct",
"promiseStrength": "firm",
"liveWalk": false,
"linkPromised": true,
"timePinned": true,
"rapport": true,
"customerSentiment": "positive",
"outcomeClass": null,
"technique": 4.5,
"talkRatio": 0.55,
"introScore": 4,
"qaScore": 4.5,
"transitionScore": 4,
"bestQuote": "It only takes a minute, and it helps a small local business a lot.",
"bestQuoteSpeaker": "rep",
"bestQuoteLever": "reciprocity",
"weakness": "Did not confirm when the homeowner would post.",
"deviations": "Skipped the recap.",
"techniqueNote": "Good warm open."
}
},
"meta": {
"apiVersion": "v1",
"requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
"generatedAt": "2026-10-01T14:00:00.000Z"
}
}
Errors
400 · Bad request: an id or query parameter is malformed or out of range (code invalid_id or invalid_parameter).
Response 400
{
"error": {
"code": "invalid_parameter",
"message": "since and until are at most 31 days apart"
}
}
401 · No key, an unknown key, a revoked key or an expired key (code unauthorized).
Response 401
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}
403 · The key lacks the scope this call needs (code forbidden_scope), or asked for personal data (include=homeowner, include=text, the X-Phone header) without the 5sos:personal scope. Checked before anything is read.
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · The database or CallTools is unavailable, or the API's share of CallTools is used up for the minute (code upstream_unavailable, with Retry-After), or CallTools is not set up on the server (code not_configured).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "CallTools is not answering right now. Try again shortly."
}
}
Play a 5SOS call's recording
GET/api/v1/5sos/calls/{callId}/recording
The call's recording as audio/wav, streamed from CallTools through CIM (no link to CallTools is ever returned). CallTools keeps audio about 60 days: an older one answers 410. At most 3 recordings stream at once across the API. Each play counts as a CallTools request.
Returns audio/wav: The recording (Content-Disposition names it call-<id>.wav).
Errors
400 · Bad request: an id or query parameter is malformed or out of range (code invalid_id or invalid_parameter).
Response 400
{
"error": {
"code": "invalid_parameter",
"message": "since and until are at most 31 days apart"
}
}
401 · No key, an unknown key, a revoked key or an expired key (code unauthorized).
Response 401
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}
403 · The key lacks the scope this call needs (code forbidden_scope), or asked for personal data (include=homeowner, include=text, the X-Phone header) without the 5sos:personal scope. Checked before anything is read.
404 · No such call in the calls mirror (code not_found), or the call has no recording (code no_recording).
Response 404
{
"error": {
"code": "no_recording",
"message": "Call 2300000002 has no recording."
}
}
410 · CallTools no longer has the recording (code recording_expired): it keeps call audio about 60 days.
Response 410
{
"error": {
"code": "recording_expired",
"message": "CallTools no longer has this recording."
}
}
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · 3 recordings are already streaming across the API (code busy, with Retry-After), CallTools is unavailable or did not send the file (code upstream_unavailable), or CallTools is not set up on the server (code not_configured).
Response 503
{
"error": {
"code": "busy",
"message": "3 recordings are streaming already. Try again shortly."
}
}
List 5SOS SMS
GET/api/v1/5sos/sms
Texts, newest first, in a window of at most 31 days. From CIM's copy of the CallTools log, synced every 20 minutes: the newest minutes may not be here yet, and nothing older than the copy (a contact's timeline gives each source's first day in meta.coverage). agent is the rep who sent it; an automated text has agent null. Picture messages (MMS) are not copied. chars counts the message; include=text returns it. Report "SMS sent" counts SMS disposition events (GET /5sos/events), so it differs from this list.
Access
Needs an API key with 5sos:activity. 5sos:personal allows include=homeowner (homeownerNumber) and include=text (the words).
Parameters
Name
In
Type
Description
clientNumber
query
string, pattern ^\d{1,6}$
Only this client's 5SOS data. Give clientNumber or productId, not both.
productId
query
string, pattern ^\d{1,4}-\d{1,2}-\d{1,2}$
Only this 5SOS product's data, read as its client (each client has one 5SOS product). Give clientNumber or productId, not both.
contactId
query
string, pattern ^\d{1,15}$
Only this contact's (its CallTools contact id).
since
query
string
Start of the window, inclusive: an ISO date or date-time, in UTC unless it carries an offset. A bare date means 00:00 UTC, so 2026-10-01 starts at 8 pm New York time on 30 September. Default: 7 days before until. At most 31 days before until.
until
query
string
End of the window, exclusive: an ISO date or date-time, in UTC unless it carries an offset (a bare date means 00:00 UTC). Default: now.
direction
query
one of in | out
in (from the homeowner) or out (from us).
campaignId
query
string, pattern ^\d{1,15}$
Only this CallTools dialer campaign (GET /5sos/campaigns lists them).
limit
query
integer
Page size.
cursor
query
string, pattern ^\d{1,10}_\d{1,20}$
meta.nextCursor from the previous page (null on the last page).
Personal data to add, a comma list of homeowner, text; each needs the 5sos:personal scope (403 without it). homeowner: the homeowner's name, phone, email and address, and their side of each call and SMS. text: message and transcript text (review links shown as jobreview.co/r/[link]).
400 · Bad request: an id or query parameter is malformed or out of range (code invalid_id or invalid_parameter).
Response 400
{
"error": {
"code": "invalid_parameter",
"message": "since and until are at most 31 days apart"
}
}
401 · No key, an unknown key, a revoked key or an expired key (code unauthorized).
Response 401
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}
403 · The key lacks the scope this call needs (code forbidden_scope), or asked for personal data (include=homeowner, include=text, the X-Phone header) without the 5sos:personal scope. Checked before anything is read.
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · The database or CallTools is unavailable, or the API's share of CallTools is used up for the minute (code upstream_unavailable, with Retry-After), or CallTools is not set up on the server (code not_configured).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "CallTools is not answering right now. Try again shortly."
}
}
List 5SOS disposition events
GET/api/v1/5sos/events
Every disposition event, newest first, in a window of at most 31 days: contact stage changes, call, SMS and email dispositions, written as they happen. agent is the email on the event (it can be the rep who owned the Review Promised before); the leaderboard credits the CallTools user on the event where there is one (creditedAgent on GET /5sos/unique-events). A contact event that names its stage without the id (some Review Promised events) has the stage's id in disposition.id, as the reports read it. liveOnCall is null when the event did not record it. campaign selects CIM campaigns as the reports do: an event without a campaign stamp counts by its contact's campaign. campaignId is the CallTools dialer campaign: contact events carry none (400 with type=contact) and some SMS events lack one.
Access
Needs an API key with 5sos:activity.
Parameters
Name
In
Type
Description
clientNumber
query
string, pattern ^\d{1,6}$
Only this client's 5SOS data. Give clientNumber or productId, not both.
productId
query
string, pattern ^\d{1,4}-\d{1,2}-\d{1,2}$
Only this 5SOS product's data, read as its client (each client has one 5SOS product). Give clientNumber or productId, not both.
contactId
query
string, pattern ^\d{1,15}$
Only this contact's (its CallTools contact id).
agent
query
string, pattern ^[a-z0-9._-]{1,64}$
Only this agent's: the email local part, e.g. sam.r.
Only these kinds, a comma list of contact (stage changes), call, sms, email.
dispositionId
query
string, pattern ^\d{1,15}$
Only events to this CallTools disposition, e.g. 31627 (Review Promised). A stage's id also finds the contact events that name the stage without its id.
Only these CIM campaigns, a comma list of post_appt, post_job, bulk_upload, bulk_priority.
campaignId
query
string, pattern ^\d{1,15}$
Only events stamped with this CallTools dialer campaign. Contact events carry none, so not with type=contact (400).
since
query
string
Start of the window, inclusive: an ISO date or date-time, in UTC unless it carries an offset. A bare date means 00:00 UTC, so 2026-10-01 starts at 8 pm New York time on 30 September. Default: 7 days before until. At most 31 days before until.
until
query
string
End of the window, exclusive: an ISO date or date-time, in UTC unless it carries an offset (a bare date means 00:00 UTC). Default: now.
limit
query
integer
Page size.
cursor
query
string, pattern ^\d{1,10}_\d{1,20}$
meta.nextCursor from the previous page (null on the last page).
400 · Bad request: an id or query parameter is malformed or out of range (code invalid_id or invalid_parameter).
Response 400
{
"error": {
"code": "invalid_parameter",
"message": "since and until are at most 31 days apart"
}
}
401 · No key, an unknown key, a revoked key or an expired key (code unauthorized).
Response 401
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}
403 · The key lacks the scope this call needs (code forbidden_scope), or asked for personal data (include=homeowner, include=text, the X-Phone header) without the 5sos:personal scope. Checked before anything is read.
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · The database or CallTools is unavailable, or the API's share of CallTools is used up for the minute (code upstream_unavailable, with Retry-After), or CallTools is not set up on the server (code not_configured).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "CallTools is not answering right now. Try again shortly."
}
}
List 5SOS unique RP and RG events
GET/api/v1/5sos/unique-events
The first Review Promised (rp) and the first Review Generated (rg) of each contact, newest first, in a window of at most 366 days: what the reports count as RPs and RGs. agent is the email on the event; creditedAgent is the CallTools user on the event, as the calls name its rep: null when that user has never dialled, and null when the event carries no CallTools user (about 8% of them), where the leaderboard credits the email's agent instead. No billing.
Access
Needs an API key with 5sos:activity.
Parameters
Name
In
Type
Description
clientNumber
query
string, pattern ^\d{1,6}$
Only this client's 5SOS data. Give clientNumber or productId, not both.
productId
query
string, pattern ^\d{1,4}-\d{1,2}-\d{1,2}$
Only this 5SOS product's data, read as its client (each client has one 5SOS product). Give clientNumber or productId, not both.
contactId
query
string, pattern ^\d{1,15}$
Only this contact's (its CallTools contact id).
disposition
query
one of rp | rg
rp (Review Promised) or rg (Review Generated). Default: both.
since
query
string
Start of the window, inclusive: an ISO date or date-time, in UTC unless it carries an offset. A bare date means 00:00 UTC, so 2026-10-01 starts at 8 pm New York time on 30 September. Default: 7 days before until. At most 366 days before until.
until
query
string
End of the window, exclusive: an ISO date or date-time, in UTC unless it carries an offset (a bare date means 00:00 UTC). Default: now.
limit
query
integer
Page size.
cursor
query
string, pattern ^\d{1,10}_\d{1,20}$
meta.nextCursor from the previous page (null on the last page).
400 · Bad request: an id or query parameter is malformed or out of range (code invalid_id or invalid_parameter).
Response 400
{
"error": {
"code": "invalid_parameter",
"message": "since and until are at most 31 days apart"
}
}
401 · No key, an unknown key, a revoked key or an expired key (code unauthorized).
Response 401
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}
403 · The key lacks the scope this call needs (code forbidden_scope), or asked for personal data (include=homeowner, include=text, the X-Phone header) without the 5sos:personal scope. Checked before anything is read.
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · The database or CallTools is unavailable, or the API's share of CallTools is used up for the minute (code upstream_unavailable, with Retry-After), or CallTools is not set up on the server (code not_configured).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "CallTools is not answering right now. Try again shortly."
}
}
List the CallTools dialer campaigns, with their calls and SMS
GET/api/v1/5sos/campaigns
Every CallTools dialer campaign (cached 5 minutes): its name, whether it is on, its live filter (the dial queue it calls from) and whether it records calls. activity counts the call and SMS mirror over the last days: callsOut and callsIn are mirrored calls (both directions, every outcome; the Inputs & Outcomes report counts call dispositions instead, so its dials differ a little), contactsDialled is the contacts with at least one outbound call, smsOut and smsIn are mirrored texts. Automated texts carry no campaign and are not counted, nor is anything on a campaign CallTools no longer lists. These are CallTools campaigns; the CIM campaigns (post_appt, post_job, bulk_upload, bulk_priority) are a contact's campaign token on the contact and event routes. Read from CallTools live (never written to), through a cache, within a per-minute share of CallTools for the whole API: when CallTools is down or that share is used up the call answers 503 upstream_unavailable with Retry-After.
Access
Needs an API key with 5sos:activity.
Parameters
Name
In
Type
Description
days
query
integer
The activity counts cover the last this many days, up to now.
400 · Bad request: an id or query parameter is malformed or out of range (code invalid_id or invalid_parameter).
Response 400
{
"error": {
"code": "invalid_parameter",
"message": "since and until are at most 31 days apart"
}
}
401 · No key, an unknown key, a revoked key or an expired key (code unauthorized).
Response 401
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}
403 · The key lacks the scope this call needs (code forbidden_scope), or asked for personal data (include=homeowner, include=text, the X-Phone header) without the 5sos:personal scope. Checked before anything is read.
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · The database or CallTools is unavailable, or the API's share of CallTools is used up for the minute (code upstream_unavailable, with Retry-After), or CallTools is not set up on the server (code not_configured).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "CallTools is not answering right now. Try again shortly."
}
}
List the CallTools live filters (dial queues)
GET/api/v1/5sos/live-filters
Every CallTools live filter, every page (cached 60 seconds). A live filter is a saved contact query, and it is the dial queue of the campaigns in campaignIds; active false means CallTools has paused it. dialableNow is the contacts it holds that can be called now (inside their local dial window); total also counts those waiting for their window to open. conditions is the query (operators and fields as CallTools names them); tags and call dispositions are CallTools ids; orderBy is the contact field CallTools sorts the queue by. The admin 5SOS pages still read only the first page of live filters (the newest 20 or 50), so this list can hold filters they don't show. Read from CallTools live (never written to), through a cache, within a per-minute share of CallTools for the whole API: when CallTools is down or that share is used up the call answers 503 upstream_unavailable with Retry-After.
400 · Bad request: an id or query parameter is malformed or out of range (code invalid_id or invalid_parameter).
Response 400
{
"error": {
"code": "invalid_parameter",
"message": "since and until are at most 31 days apart"
}
}
401 · No key, an unknown key, a revoked key or an expired key (code unauthorized).
Response 401
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}
403 · The key lacks the scope this call needs (code forbidden_scope), or asked for personal data (include=homeowner, include=text, the X-Phone header) without the 5sos:personal scope. Checked before anything is read.
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · The database or CallTools is unavailable, or the API's share of CallTools is used up for the minute (code upstream_unavailable, with Retry-After), or CallTools is not set up on the server (code not_configured).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "CallTools is not answering right now. Try again shortly."
}
}
The contacts a live filter holds right now that can be called (its dialableNow: CallTools cannot list the ones waiting for their dial window), one page at a time, each with the mirror's client, 5SOS product and current stage. inMirror false means CIM holds no record of the contact (its other fields are null). include=homeowner adds the homeowner from CIM's records. meta.count is CallTools' count now. A cursor goes stale as the queue moves: past the end it is 400, so start again without one. If CallTools ignores the filter (it then answers with the whole account) the call answers 503 rather than list the wrong contacts. Read from CallTools live (never written to), through a cache, within a per-minute share of CallTools for the whole API: when CallTools is down or that share is used up the call answers 503 upstream_unavailable with Retry-After.
Access
Needs an API key with 5sos:activity. 5sos:personal allows include=homeowner.
Parameters
Name
In
Type
Description
liveFilterIdrequired
path
string, pattern ^\d{1,15}$
The CallTools live filter (GET /5sos/live-filters lists them).
limit
query
integer
Page size.
cursor
query
string, pattern ^p\d{1,5}s\d{1,3}$
meta.nextCursor from the previous page (null on the last page). It keeps the page size it was made with.
include
query
string, pattern ^(homeowner)(,(homeowner))*$
Personal data to add, a comma list of homeowner; each needs the 5sos:personal scope (403 without it). homeowner: the homeowner's name, phone, email and address, and their side of each call and SMS.
400 · Bad request: an id or query parameter is malformed or out of range (code invalid_id or invalid_parameter).
Response 400
{
"error": {
"code": "invalid_parameter",
"message": "since and until are at most 31 days apart"
}
}
401 · No key, an unknown key, a revoked key or an expired key (code unauthorized).
Response 401
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}
403 · The key lacks the scope this call needs (code forbidden_scope), or asked for personal data (include=homeowner, include=text, the X-Phone header) without the 5sos:personal scope. Checked before anything is read.
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · The database or CallTools is unavailable, or the API's share of CallTools is used up for the minute (code upstream_unavailable, with Retry-After), or CallTools is not set up on the server (code not_configured).
Response 503
{
"error": {
"code": "upstream_unavailable",
"message": "CallTools is not answering right now. Try again shortly."
}
}
Inputs & Outcomes: the funnel by day or week
GET/api/v1/5sos/reports/inputs-outcomes
The Inputs & Outcomes table: one row per day (a range of 21 days or less) or per Monday week, newest first, then overall, and the clients and agents the page's filters offer. A range over 182 days keeps its last 182, as on the page. Sources: hv = contacts by HV date; hvComplete = those HVs with a finished outcome; calls = dials, each call_disposition event (the calls of /5sos/calls are the call mirror and count differently); sms = SMS sent, each sms_disposition 'sent' event (not the texts of /5sos/sms); c = conversations, distinct contacts with an answered call or an inbound SMS; rp and rg = unique Review Promised and Review Generated events, by event day; uniqueDials = distinct contacts dialled. Rates: hvDials = share of the HVs dialled, dialsC = c/calls, hvC = c/hv, cRp = rp/c, rpRg = rg/rp, hvR = rg/hv (Performance's hvr divides by finished contacts instead), hvDone = hvComplete/hv. groupBy=client answers rows instead, one per client (with its 5SOS products), each equal to the overall row with that client picked. The same numbers as Admin → 5SOS Reporting: the page's own code runs, with the page's parameter names, read only. Days are New York days. Rates are fractions from 0 to 1, null when the denominator is 0. One report runs at a time across the API: while another runs the call answers 503 busy with Retry-After.
Access
Needs an API key with 5sos:activity.
Parameters
Name
In
Type
Description
dayStart
query
string, pattern ^\d{4}-\d{2}-\d{2}$
First day, New York time (YYYY-MM-DD). Default: 29 days before dayEnd, the page's last 30 days. dayStart to dayEnd is at most 366 days.
dayEnd
query
string, pattern ^\d{4}-\d{2}-\d{2}$
Last day, included, New York time (YYYY-MM-DD). Default: today.
agentEmail
query
string
Only these agents (the page's User filter): a comma list of their emails or email local parts, e.g. sam.r. Both email domains of one agent count as the same agent.
clientNumber
query
string, pattern ^\d{1,6}$
Only this client (the page's Client filter).
bookView
query
one of active | all
active (default): the team rates leave out the unfinished contacts of Inactive and Paused clients, as the page's Active book toggle does. all: everyone. Ignored when clientNumber is given.
400 · Bad request: an id or query parameter is malformed or out of range (code invalid_id or invalid_parameter).
Response 400
{
"error": {
"code": "invalid_parameter",
"message": "since and until are at most 31 days apart"
}
}
401 · No key, an unknown key, a revoked key or an expired key (code unauthorized).
Response 401
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}
403 · The key lacks the scope this call needs (code forbidden_scope), or asked for personal data (include=homeowner, include=text, the X-Phone header) without the 5sos:personal scope. Checked before anything is read.
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · Another report is running (code busy, with Retry-After: one report runs at a time across the API), or the database is unavailable (code upstream_unavailable), or reports are not set up on the server (code not_configured).
Response 503
{
"error": {
"code": "busy",
"message": "Another report is running. Retry in a few seconds."
}
}
Inputs & Outcomes: the rows behind one cell
GET/api/v1/5sos/reports/inputs-outcomes-drilldown
The contacts or events behind one Inputs & Outcomes cell (the page's drill-down): metric is the column, weekStart the row's day or Monday (bucketType day or week; none = the overall row). numerator and denominator are the cell's counts; rows stop at 5,000 (truncated). id is the event's id, or the contact's where a row is a contact; agentName is the agent's email; inspectorName is the client's home-visit rep. include=homeowner adds contact, the homeowner's name. The same numbers as Admin → 5SOS Reporting: the page's own code runs, with the page's parameter names, read only. Days are New York days. Rates are fractions from 0 to 1, null when the denominator is 0. One report runs at a time across the API: while another runs the call answers 503 busy with Retry-After.
Access
Needs an API key with 5sos:activity. 5sos:personal allows include=homeowner (adds contact, the homeowner's name, to each row).
Parameters
Name
In
Type
Description
metricrequired
query
one of hv | hvComplete | calls | sms | c | rp | rg | uniqueDials | hvDials | dialsC | hvC | cRp | rpRg | hvR | hvDone
The column.
weekStart
query
string, pattern ^\d{4}-\d{2}-\d{2}$
The row: its day, or its Monday. None = the overall row.
bucketType
query
one of day | week
Whether weekStart is a day or a Monday week.
dayStart
query
string, pattern ^\d{4}-\d{2}-\d{2}$
First day, New York time (YYYY-MM-DD). Default: 29 days before dayEnd, the page's last 30 days. dayStart to dayEnd is at most 366 days.
dayEnd
query
string, pattern ^\d{4}-\d{2}-\d{2}$
Last day, included, New York time (YYYY-MM-DD). Default: today.
agentEmail
query
string
Only these agents (the page's User filter): a comma list of their emails or email local parts, e.g. sam.r. Both email domains of one agent count as the same agent.
clientNumber
query
string, pattern ^\d{1,6}$
Only this client (the page's Client filter).
bookView
query
one of active | all
active (default): the team rates leave out the unfinished contacts of Inactive and Paused clients, as the page's Active book toggle does. all: everyone. Ignored when clientNumber is given.
400 · Bad request: an id or query parameter is malformed or out of range (code invalid_id or invalid_parameter).
Response 400
{
"error": {
"code": "invalid_parameter",
"message": "since and until are at most 31 days apart"
}
}
401 · No key, an unknown key, a revoked key or an expired key (code unauthorized).
Response 401
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}
403 · The key lacks the scope this call needs (code forbidden_scope), or asked for personal data (include=homeowner, include=text, the X-Phone header) without the 5sos:personal scope. Checked before anything is read.
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · Another report is running (code busy, with Retry-After: one report runs at a time across the API), or the database is unavailable (code upstream_unavailable), or reports are not set up on the server (code not_configured).
Response 503
{
"error": {
"code": "busy",
"message": "Another report is running. Retry in a few seconds."
}
}
Conversion: HV cohorts and how far each got
GET/api/v1/5sos/reports/conversion
The Conversion table: the contacts whose HV falls in the range, grouped into cohorts (grain), with every stage counted (c conversation, rp, rg, lost, and the live pipeline columns) and the cohort rates, plus overall, the two recent-cohort tiles and the colour targets. The ...Idx columns are an index that freezes once the cohort locks (lockDay: a week plus maturationDays after the cohort starts; maturationDays is a reporting_settings value); the ...Live columns keep moving. With grain=client each cohort is a client (cohort = its number, label = its name, products = its 5SOS products). roRgLive = rg/hv over the cohort. The same numbers as Admin → 5SOS Reporting: the page's own code runs, with the page's parameter names, read only. Days are New York days. Rates are fractions from 0 to 1, null when the denominator is 0. One report runs at a time across the API: while another runs the call answers 503 busy with Retry-After.
Access
Needs an API key with 5sos:activity.
Parameters
Name
In
Type
Description
dayStart
query
string, pattern ^\d{4}-\d{2}-\d{2}$
First day, New York time (YYYY-MM-DD). Default: 29 days before dayEnd, the page's last 30 days. dayStart to dayEnd is at most 366 days.
dayEnd
query
string, pattern ^\d{4}-\d{2}-\d{2}$
Last day, included, New York time (YYYY-MM-DD). Default: today.
agentEmail
query
string
Only these agents (the page's User filter): a comma list of their emails or email local parts, e.g. sam.r. Both email domains of one agent count as the same agent.
clientNumber
query
string, pattern ^\d{1,6}$
Only this client (the page's Client filter).
bookView
query
one of active | all
active (default): the team rates leave out the unfinished contacts of Inactive and Paused clients, as the page's Active book toggle does. all: everyone. Ignored when clientNumber is given.
400 · Bad request: an id or query parameter is malformed or out of range (code invalid_id or invalid_parameter).
Response 400
{
"error": {
"code": "invalid_parameter",
"message": "since and until are at most 31 days apart"
}
}
401 · No key, an unknown key, a revoked key or an expired key (code unauthorized).
Response 401
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}
403 · The key lacks the scope this call needs (code forbidden_scope), or asked for personal data (include=homeowner, include=text, the X-Phone header) without the 5sos:personal scope. Checked before anything is read.
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · Another report is running (code busy, with Retry-After: one report runs at a time across the API), or the database is unavailable (code upstream_unavailable), or reports are not set up on the server (code not_configured).
Response 503
{
"error": {
"code": "busy",
"message": "Another report is running. Retry in a few seconds."
}
}
Conversion: the contacts behind one cell
GET/api/v1/5sos/reports/conversion-drilldown
The contacts of one Conversion cell (the page's drill-down) with all their stage dates: metric is the stage, cohort the cohort (none = overall). rgInIdx is 1 when the review counts in the frozen index. Rows stop at 5,000 (truncated). include=homeowner adds contact, the homeowner's name. The same numbers as Admin → 5SOS Reporting: the page's own code runs, with the page's parameter names, read only. Days are New York days. Rates are fractions from 0 to 1, null when the denominator is 0. One report runs at a time across the API: while another runs the call answers 503 busy with Retry-After.
Access
Needs an API key with 5sos:activity. 5sos:personal allows include=homeowner (adds contact, the homeowner's name, to each row).
Parameters
Name
In
Type
Description
metricrequired
query
one of hv | c | rp | rg | lost | nonComplete | liveComplete | liveIncomplete | liveIoLt4 | liveIoGe4 | liveRp | liveCallback | liveActive
The stage (the live... ones are the pipeline columns).
cohort
query
string
The cohort: its Monday or first of the month (YYYY-MM-DD), or with grain=client the client number. None = the overall column.
grain
query
one of week | month | client
Cohorts by HV week (Monday, default), HV month, or client.
dayStart
query
string, pattern ^\d{4}-\d{2}-\d{2}$
First day, New York time (YYYY-MM-DD). Default: 29 days before dayEnd, the page's last 30 days. dayStart to dayEnd is at most 366 days.
dayEnd
query
string, pattern ^\d{4}-\d{2}-\d{2}$
Last day, included, New York time (YYYY-MM-DD). Default: today.
agentEmail
query
string
Only these agents (the page's User filter): a comma list of their emails or email local parts, e.g. sam.r. Both email domains of one agent count as the same agent.
clientNumber
query
string, pattern ^\d{1,6}$
Only this client (the page's Client filter).
bookView
query
one of active | all
active (default): the team rates leave out the unfinished contacts of Inactive and Paused clients, as the page's Active book toggle does. all: everyone. Ignored when clientNumber is given.
400 · Bad request: an id or query parameter is malformed or out of range (code invalid_id or invalid_parameter).
Response 400
{
"error": {
"code": "invalid_parameter",
"message": "since and until are at most 31 days apart"
}
}
401 · No key, an unknown key, a revoked key or an expired key (code unauthorized).
Response 401
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}
403 · The key lacks the scope this call needs (code forbidden_scope), or asked for personal data (include=homeowner, include=text, the X-Phone header) without the 5sos:personal scope. Checked before anything is read.
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · Another report is running (code busy, with Retry-After: one report runs at a time across the API), or the database is unavailable (code upstream_unavailable), or reports are not set up on the server (code not_configured).
Response 503
{
"error": {
"code": "busy",
"message": "Another report is running. Retry in a few seconds."
}
}
Conversion by agent: the weekly cohorts split by rep
GET/api/v1/5sos/reports/conversion-agents
The Team Scoreboard's per-rep cohort trend: the same weekly cohorts as Conversion, with each stage event credited to the agent who logged it (series, one row per agent and cohort). c, rp and rg are that agent's; the cohort sizes in cohorts are everyone's, so an agent's share of hv reads as a share of the team. A contact worked by two agents counts for both. The same numbers as Admin → 5SOS Reporting: the page's own code runs, with the page's parameter names, read only. Days are New York days. Rates are fractions from 0 to 1, null when the denominator is 0. One report runs at a time across the API: while another runs the call answers 503 busy with Retry-After.
Access
Needs an API key with 5sos:activity.
Parameters
Name
In
Type
Description
dayStart
query
string, pattern ^\d{4}-\d{2}-\d{2}$
First day, New York time (YYYY-MM-DD). Default: 29 days before dayEnd, the page's last 30 days. dayStart to dayEnd is at most 366 days.
dayEnd
query
string, pattern ^\d{4}-\d{2}-\d{2}$
Last day, included, New York time (YYYY-MM-DD). Default: today.
clientNumber
query
string, pattern ^\d{1,6}$
Only this client (the page's Client filter).
bookView
query
one of active | all
active (default): the team rates leave out the unfinished contacts of Inactive and Paused clients, as the page's Active book toggle does. all: everyone. Ignored when clientNumber is given.
400 · Bad request: an id or query parameter is malformed or out of range (code invalid_id or invalid_parameter).
Response 400
{
"error": {
"code": "invalid_parameter",
"message": "since and until are at most 31 days apart"
}
}
401 · No key, an unknown key, a revoked key or an expired key (code unauthorized).
Response 401
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}
403 · The key lacks the scope this call needs (code forbidden_scope), or asked for personal data (include=homeowner, include=text, the X-Phone header) without the 5sos:personal scope. Checked before anything is read.
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · Another report is running (code busy, with Retry-After: one report runs at a time across the API), or the database is unavailable (code upstream_unavailable), or reports are not set up on the server (code not_configured).
Response 503
{
"error": {
"code": "busy",
"message": "Another report is running. Retry in a few seconds."
}
}
Product Performance: the cards
GET/api/v1/5sos/reports/performance
The Product Performance cards, over the contacts whose HV falls in the range: counts, then each card as value (with num and den where it is a rate). hvc = conversations/hv; crp = rp/conversations; rprg = rg/rp; hvr = rg/terminal, where terminal is the contacts that finished (Review Generated or Lost), so it differs from Inputs & Outcomes' hvR (rg/hv); s2ca = dialled by the end of the next business day; cahv = initial-outreach dials per HV; sqr = share with 4 or more initial-outreach dials or a finished outcome; complaint = CIM-caused Lost over conversations; openRate = contacts who opened a review link over those sent one (from 2026-08-24). The same numbers as Admin → 5SOS Reporting: the page's own code runs, with the page's parameter names, read only. Days are New York days. Rates are fractions from 0 to 1, null when the denominator is 0. One report runs at a time across the API: while another runs the call answers 503 busy with Retry-After.
Access
Needs an API key with 5sos:activity.
Parameters
Name
In
Type
Description
dayStart
query
string, pattern ^\d{4}-\d{2}-\d{2}$
First day, New York time (YYYY-MM-DD). Default: 29 days before dayEnd, the page's last 30 days. dayStart to dayEnd is at most 366 days.
dayEnd
query
string, pattern ^\d{4}-\d{2}-\d{2}$
Last day, included, New York time (YYYY-MM-DD). Default: today.
agentEmail
query
string
Only these agents (the page's User filter): a comma list of their emails or email local parts, e.g. sam.r. Both email domains of one agent count as the same agent.
clientNumber
query
string, pattern ^\d{1,6}$
Only this client (the page's Client filter).
bookView
query
one of active | all
active (default): the team rates leave out the unfinished contacts of Inactive and Paused clients, as the page's Active book toggle does. all: everyone. Ignored when clientNumber is given.
400 · Bad request: an id or query parameter is malformed or out of range (code invalid_id or invalid_parameter).
Response 400
{
"error": {
"code": "invalid_parameter",
"message": "since and until are at most 31 days apart"
}
}
401 · No key, an unknown key, a revoked key or an expired key (code unauthorized).
Response 401
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}
403 · The key lacks the scope this call needs (code forbidden_scope), or asked for personal data (include=homeowner, include=text, the X-Phone header) without the 5sos:personal scope. Checked before anything is read.
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · Another report is running (code busy, with Retry-After: one report runs at a time across the API), or the database is unavailable (code upstream_unavailable), or reports are not set up on the server (code not_configured).
Response 503
{
"error": {
"code": "busy",
"message": "Another report is running. Retry in a few seconds."
}
}
Scoreboard: each agent's numbers
GET/api/v1/5sos/reports/scoreboard
The Stats and Scoreboard tabs: overall (filtered by agentEmail) and one row per agent (never filtered by agent), by event day. rg and rp are unique Review Generated and Review Promised events; attempts every dial; conv distinct contacts reached (an answered call or an inbound SMS); caIO and caRP the dials before and after the contact's first Review Promised; activeHours dialling time with gaps capped at 10 minutes; rlr24 Review Promised contacts whose Review Generated came within 24 hours of the promise. crg = rpRgIdx/conv (promises in the range that converted within maturationDays); cac = conv/attempts; crp = rp/conv; rprg = rg/rp; rprgIdx = rpRgIdx/rpIdxDen; caPerHr = attempts/activeHours. The same numbers as Admin → 5SOS Reporting: the page's own code runs, with the page's parameter names, read only. Days are New York days. Rates are fractions from 0 to 1, null when the denominator is 0. One report runs at a time across the API: while another runs the call answers 503 busy with Retry-After.
Access
Needs an API key with 5sos:activity.
Parameters
Name
In
Type
Description
dayStart
query
string, pattern ^\d{4}-\d{2}-\d{2}$
First day, New York time (YYYY-MM-DD). Default: 29 days before dayEnd, the page's last 30 days. dayStart to dayEnd is at most 366 days.
dayEnd
query
string, pattern ^\d{4}-\d{2}-\d{2}$
Last day, included, New York time (YYYY-MM-DD). Default: today.
agentEmail
query
string
Only these agents (the page's User filter): a comma list of their emails or email local parts, e.g. sam.r. Both email domains of one agent count as the same agent.
clientNumber
query
string, pattern ^\d{1,6}$
Only this client (the page's Client filter).
bookView
query
one of active | all
active (default): the team rates leave out the unfinished contacts of Inactive and Paused clients, as the page's Active book toggle does. all: everyone. Ignored when clientNumber is given.
400 · Bad request: an id or query parameter is malformed or out of range (code invalid_id or invalid_parameter).
Response 400
{
"error": {
"code": "invalid_parameter",
"message": "since and until are at most 31 days apart"
}
}
401 · No key, an unknown key, a revoked key or an expired key (code unauthorized).
Response 401
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}
403 · The key lacks the scope this call needs (code forbidden_scope), or asked for personal data (include=homeowner, include=text, the X-Phone header) without the 5sos:personal scope. Checked before anything is read.
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · Another report is running (code busy, with Retry-After: one report runs at a time across the API), or the database is unavailable (code upstream_unavailable), or reports are not set up on the server (code not_configured).
Response 503
{
"error": {
"code": "busy",
"message": "Another report is running. Retry in a few seconds."
}
}
Lost: why contacts were lost
GET/api/v1/5sos/reports/lost
The Lost tab: distinct contacts given the Lost disposition in the range (lostAllTime here means in the range) and in the last 30 days, and the count per lost reason. It always reads the disposition log in MySQL, as the page does once any date or filter is set, so it equals the page's filtered view; the page's unfiltered view reads CallTools' current state instead and shows a few more (the log under-captures it slightly). totalHvs is every contact in the mirror (the page's HV total; the client's, when one is picked). The same numbers as Admin → 5SOS Reporting: the page's own code runs, with the page's parameter names, read only. Days are New York days. Rates are fractions from 0 to 1, null when the denominator is 0. One report runs at a time across the API: while another runs the call answers 503 busy with Retry-After.
Access
Needs an API key with 5sos:activity.
Parameters
Name
In
Type
Description
dayStart
query
string, pattern ^\d{4}-\d{2}-\d{2}$
First day, New York time (YYYY-MM-DD). Default: 29 days before dayEnd, the page's last 30 days. dayStart to dayEnd is at most 366 days.
dayEnd
query
string, pattern ^\d{4}-\d{2}-\d{2}$
Last day, included, New York time (YYYY-MM-DD). Default: today.
agentEmail
query
string
Only these agents (the page's User filter): a comma list of their emails or email local parts, e.g. sam.r. Both email domains of one agent count as the same agent.
400 · Bad request: an id or query parameter is malformed or out of range (code invalid_id or invalid_parameter).
Response 400
{
"error": {
"code": "invalid_parameter",
"message": "since and until are at most 31 days apart"
}
}
401 · No key, an unknown key, a revoked key or an expired key (code unauthorized).
Response 401
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}
403 · The key lacks the scope this call needs (code forbidden_scope), or asked for personal data (include=homeowner, include=text, the X-Phone header) without the 5sos:personal scope. Checked before anything is read.
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · Another report is running (code busy, with Retry-After: one report runs at a time across the API), or the database is unavailable (code upstream_unavailable), or reports are not set up on the server (code not_configured).
Response 503
{
"error": {
"code": "busy",
"message": "Another report is running. Retry in a few seconds."
}
}
One client's 5SOS report: home visits and reviews
GET/api/v1/5sos/reports/client/{clientNumber}
One client's 5SOS page (the report the client sees, and admins see on the product profile), over all time: homeVisits, reviewsPromised (Review Promised contacts who have not gone on to Review Generated: a promise drops off once fulfilled) and reviewsGenerated, and the three lists behind them, newest first, each with the client's home-visit rep, the campaign and the day. Each list stops at 1,000 rows (meta.truncated); the counts are the full numbers. include=homeowner adds each row's name and phone, and reviewLink (the confirmed review link, read from CallTools) on rgList. isFiveSosClient is false for a client that never had 5SOS. The same numbers as Admin → 5SOS Reporting: the page's own code runs, with the page's parameter names, read only. Days are New York days. Rates are fractions from 0 to 1, null when the denominator is 0. One report runs at a time across the API: while another runs the call answers 503 busy with Retry-After.
Access
Needs an API key with 5sos:activity. 5sos:personal allows include=homeowner (adds name, phone and reviewLink to the lists).
Parameters
Name
In
Type
Description
clientNumberrequired
path
string, pattern ^\d{1,6}$
The client number. Leading zeros are accepted ('13' and '013' are the same client).
include
query
string, pattern ^(homeowner)(,(homeowner))*$
homeowner: add each row's name and phone, and the review link on Review Generated rows. Needs the 5sos:personal scope (403 without it).
400 · Bad request: an id or query parameter is malformed or out of range (code invalid_id or invalid_parameter).
Response 400
{
"error": {
"code": "invalid_parameter",
"message": "since and until are at most 31 days apart"
}
}
401 · No key, an unknown key, a revoked key or an expired key (code unauthorized).
Response 401
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}
403 · The key lacks the scope this call needs (code forbidden_scope), or asked for personal data (include=homeowner, include=text, the X-Phone header) without the 5sos:personal scope. Checked before anything is read.
429 · Rate limited (code rate_limited). Retry-After says when to try again.
Response 429
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Retry in 12 s."
}
}
503 · Another report is running (code busy, with Retry-After: one report runs at a time across the API), or the database is unavailable (code upstream_unavailable), or reports are not set up on the server (code not_configured).
Response 503
{
"error": {
"code": "busy",
"message": "Another report is running. Retry in a few seconds."
}
}