CIM API v1

https://admin.contractorindependence.com/api/v1 OpenAPI JSONAI version (Markdown)

CIM API v1

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.

Base URL
https://admin.contractorindependence.com/api/v1
AI-readable docs
https://admin.contractorindependence.com/api/docs.md

Authentication

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.

Bearer header
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/clients?limit=1"
X-API-Key header
curl -H "X-API-Key: $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/clients?limit=1"

Responses & errors

Success: { "data": …, "meta": { "apiVersion", "requestId", "generatedAt" } }. Lists add counts or a nextCursor to meta. Errors: { "error": { "code", "message" } } with these HTTP statuses:

StatusMeaning
400Bad request: an id or query parameter is malformed (code invalid_id or invalid_parameter).
401No key, an unknown key, a revoked key or an expired key (code unauthorized).
403The key lacks the scope this call needs (code forbidden_scope).
404Not found (code not_found), or the product exists under another type (code wrong_product_type, with href).
409Not 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).
410CallTools no longer has the recording (code recording_expired): it keeps call audio about 60 days.
413The 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.
415The body is not JSON (code unsupported_media_type). Send Content-Type: application/json.
422The 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.
429Rate limited (code rate_limited). Retry-After says when to try again.
503A 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."
  }
}

Rate limits

LimitValue
Per key120 requests per minute
Live tracking checks (?live=1)10 per minute per key
GBP edits (PATCH, POST, DELETE /gbp)20 per minute per key
GBP photo checks (POST /gbp-photos/{productId}/check)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 recordings3 streams at once across the API (503 while full)
Failed authentication20 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

ScopeAllows
clients:readRead clients: status, team, dates and the product list.
clients:contactsAdds the client's primary contact (name, email, phone) to client responses. Opt-in: personal data. Needs clients:read as well.
maps:readRead Maps products, including the cached tracking-number check.
lsa:readRead LSA (stored as GGA) products, including the cached tracking-number check.
5sos:readRead 5SOS products and their review locations.
tracking:checkAllows ?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:readRead the tracking-number audit (main status and every sub-status) for all live Maps and LSA products: /tracking-checks.
gbp:readRead 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:writeEdit 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:checkRun 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:activityRead 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:personalAdds 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:recordingsPlay 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

NameInTypeDescription
statusqueryone of Active | Paused | InactiveOnly clients with this status (client_status).
qquerystringSearch: part of the client name, or an exact client number.
limitqueryintegerPage size.
cursorquerystring, pattern ^\d{1,6}$meta.nextCursor from the previous page.
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/clients?status=Active&limit=50"
Response 200 · success
{
  "data": [
    {
      "clientNumber": 13,
      "name": "Example Roofing Co",
      "status": "Active",
      "managedService": true,
      "startDate": "2024-03-01",
      "monthsActive": 31,
      "journeyStage": "Growth",
      "healthStatus": "Healthy",
      "niche": "Roofing",
      "team": {
        "csm": {
          "name": "Casey Example",
          "email": "[email protected]"
        },
        "activationSpecialist": "Jordan Example",
        "linkSpecialist": null,
        "adsManager": null
      },
      "primaryContact": {
        "name": "Pat Example",
        "email": "[email protected]",
        "phone": "+12175550142"
      },
      "href": "/api/v1/clients/13",
      "liveProductCounts": {
        "maps": 1,
        "lsa": 1,
        "5sos": 1
      }
    }
  ],
  "meta": {
    "apiVersion": "v1",
    "requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
    "generatedAt": "2026-10-01T14:00:00.000Z",
    "limit": 50,
    "nextCursor": "12"
  }
}
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).
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "This key needs the maps:read scope."
  }
}
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

NameInTypeDescription
clientNumber requiredpathstring, pattern ^\d{1,6}$The client number. Leading zeros are accepted ('13' and '013' are the same client).
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/clients/13"
Response 200 · success
{
  "data": {
    "clientNumber": 13,
    "name": "Example Roofing Co",
    "status": "Active",
    "managedService": true,
    "startDate": "2024-03-01",
    "monthsActive": 31,
    "journeyStage": "Growth",
    "healthStatus": "Healthy",
    "niche": "Roofing",
    "team": {
      "csm": {
        "name": "Casey Example",
        "email": "[email protected]"
      },
      "activationSpecialist": "Jordan Example",
      "linkSpecialist": null,
      "adsManager": null
    },
    "primaryContact": {
      "name": "Pat Example",
      "email": "[email protected]",
      "phone": "+12175550142"
    },
    "href": "/api/v1/clients/13",
    "products": [
      {
        "productId": "013-1-1",
        "type": "maps",
        "status": "Active",
        "businessName": "Example Roofing Co",
        "location": "Springfield",
        "tier": "Tier 4",
        "longTermWarranty": false,
        "href": "/api/v1/maps/013-1-1"
      },
      {
        "productId": "013-1-2",
        "type": "lsa",
        "status": "Active",
        "businessName": "Example Roofing Co",
        "location": "Springfield",
        "tier": "GGA $500/m",
        "longTermWarranty": false,
        "href": "/api/v1/lsa/013-1-2"
      },
      {
        "productId": "013-2-1",
        "type": "5sos",
        "status": "Active",
        "businessName": null,
        "location": "Springfield, IL",
        "tier": "PPR",
        "longTermWarranty": false,
        "href": "/api/v1/5sos/013-2-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).
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "This key needs the maps:read scope."
  }
}
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.

Access

Needs an API key with maps:read.

Parameters

NameInTypeDescription
clientNumber requiredquerystring, pattern ^\d{1,6}$The client whose products to list.
statusqueryone of Active | Paused | InactiveOnly products with this product status.
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/maps?clientNumber=13"
Response 200 · success
{
  "data": [
    {
      "productId": "013-1-1",
      "type": "maps",
      "clientNumber": 13,
      "clientName": "Example Roofing Co",
      "businessName": "Example Roofing Co",
      "status": "Active",
      "fulfillmentTier": "Tier 4",
      "tierSold": "Tier 4",
      "longTermWarranty": false,
      "location": {
        "label": "Springfield",
        "address": "100 Example St, Springfield, IL",
        "zip": "62701",
        "serviceArea": "Yes"
      },
      "dates": {
        "start": "2024-03-01",
        "end": null,
        "statusChangedAt": "2024-03-01"
      },
      "gbp": {
        "placeId": "ChIJexampleplaceid0001",
        "verificationStatus": "Verified",
        "type": "SAB",
        "publicUrl": "https://maps.google.com/?cid=1234567890",
        "reviewLink": "https://g.page/r/example/review",
        "profileHealth": "Healthy"
      },
      "websiteUrl": "https://example-roofing.example.test",
      "ghl": {
        "locationId": "ExampleLocation00001"
      },
      "phone": {
        "trackingNumber": "+12175550123",
        "optedOutOfTrackingNumber": false,
        "forwardingNumber": "+12175550188",
        "officialBusinessNumber": "+12175550188",
        "callIngestEnabled": true
      },
      "trackingNumberCheck": {
        "result": "fail",
        "verifiedBy": null,
        "summary": "GHL forwards ***0123 to nothing: calls go to GHL voicemail, not the Forwarding Number ***0188.",
        "numbersChangedSinceCheck": false,
        "checkedAt": "2026-10-01T12:20:04.000Z",
        "ageSeconds": 6236,
        "stale": false,
        "freshness": "cached",
        "refreshError": null,
        "checks": [
          {
            "id": "tn_on_file",
            "label": "Tracking number on file",
            "status": "pass",
            "expected": null,
            "actual": "***0123",
            "detail": "E.164 tracking number on file."
          },
          {
            "id": "ghl_forwarding",
            "label": "GHL forwards to the Forwarding Number",
            "status": "fail",
            "expected": "***0188",
            "actual": null,
            "detail": "GHL has no forwarding number for this tracking number."
          }
        ]
      },
      "href": "/api/v1/maps/013-1-1",
      "clientHref": "/api/v1/clients/13"
    }
  ],
  "meta": {
    "apiVersion": "v1",
    "requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
    "generatedAt": "2026-10-01T14:00:00.000Z",
    "count": 1
  }
}
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).
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "This key needs the maps:read scope."
  }
}
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

NameInTypeDescription
productId requiredpathstring, 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.
livequeryone of 0 | 11 = 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.
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/maps/013-1-1"
Response 200 · success
{
  "data": {
    "productId": "013-1-1",
    "type": "maps",
    "clientNumber": 13,
    "clientName": "Example Roofing Co",
    "businessName": "Example Roofing Co",
    "status": "Active",
    "fulfillmentTier": "Tier 4",
    "tierSold": "Tier 4",
    "longTermWarranty": false,
    "location": {
      "label": "Springfield",
      "address": "100 Example St, Springfield, IL",
      "zip": "62701",
      "serviceArea": "Yes"
    },
    "dates": {
      "start": "2024-03-01",
      "end": null,
      "statusChangedAt": "2024-03-01"
    },
    "gbp": {
      "placeId": "ChIJexampleplaceid0001",
      "verificationStatus": "Verified",
      "type": "SAB",
      "publicUrl": "https://maps.google.com/?cid=1234567890",
      "reviewLink": "https://g.page/r/example/review",
      "profileHealth": "Healthy"
    },
    "websiteUrl": "https://example-roofing.example.test",
    "ghl": {
      "locationId": "ExampleLocation00001"
    },
    "phone": {
      "trackingNumber": "+12175550123",
      "optedOutOfTrackingNumber": false,
      "forwardingNumber": "+12175550188",
      "officialBusinessNumber": "+12175550188",
      "callIngestEnabled": true
    },
    "trackingNumberCheck": {
      "result": "fail",
      "verifiedBy": null,
      "summary": "GHL forwards ***0123 to nothing: calls go to GHL voicemail, not the Forwarding Number ***0188.",
      "numbersChangedSinceCheck": false,
      "checkedAt": "2026-10-01T12:20:04.000Z",
      "ageSeconds": 6236,
      "stale": false,
      "freshness": "cached",
      "refreshError": null,
      "checks": [
        {
          "id": "tn_on_file",
          "label": "Tracking number on file",
          "status": "pass",
          "expected": null,
          "actual": "***0123",
          "detail": "E.164 tracking number on file."
        },
        {
          "id": "ghl_forwarding",
          "label": "GHL forwards to the Forwarding Number",
          "status": "fail",
          "expected": "***0188",
          "actual": null,
          "detail": "GHL has no forwarding number for this tracking number."
        }
      ]
    },
    "href": "/api/v1/maps/013-1-1",
    "clientHref": "/api/v1/clients/13"
  },
  "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).
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "This key needs the maps:read scope."
  }
}
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

NameInTypeDescription
productId requiredpathstring, pattern ^\d{1,4}-\d{1,2}-\d{1,2}$The Maps product.
sincequerystringFirst scan date, inclusive (YYYY-MM-DD). Default: 90 days before until.
untilquerystringLast scan date, exclusive (YYYY-MM-DD). Default: tomorrow.
keywordquerystringOnly this search term (exact, any case).
onlyqueryone of top3 | newTop3top3 or newTop3.
limitqueryintegerPage size.
cursorquerystringmeta.nextCursor from the previous page (null on the last page).
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/maps/013-1-1/scans?since=2026-07-01&only=newTop3"
Response 200 · success
{
  "data": [
    {
      "id": "ex4mpl3a",
      "date": "2026-10-05",
      "keyword": "roofing contractors",
      "grid": {
        "size": 3,
        "pointDistance": 1,
        "measure": "miles",
        "centerLat": 40.0001,
        "centerLng": -75.0001
      },
      "nodes": 9,
      "rankedNodes": 8,
      "top3Nodes": 3,
      "newTop3Nodes": 1,
      "baseline": false,
      "ranks": [
        [
          4,
          3,
          5
        ],
        [
          2,
          1,
          null
        ],
        [
          6,
          7,
          9
        ]
      ],
      "image": "https://example.test/geogrid/ex4mpl3a.png"
    }
  ],
  "meta": {
    "apiVersion": "v1",
    "requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
    "generatedAt": "2026-10-01T14:00:00.000Z",
    "productId": "013-1-1",
    "placeId": "ChIJexampleexampleexample",
    "since": "2026-07-01",
    "until": "2026-10-13",
    "keyword": null,
    "only": "newTop3",
    "count": 1,
    "nextCursor": null
  }
}
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).
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "This key needs the maps:read scope."
  }
}
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.

Access

Needs an API key with maps:read.

Parameters

NameInTypeDescription
productId requiredpathstring, pattern ^\d{1,4}-\d{1,2}-\d{1,2}$The Maps product.
keywordquerystringOnly this search term (exact, any case).
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/maps/013-1-1/scans/four-month"
Response 200 · success
{
  "data": {
    "productId": "013-1-1",
    "placeId": "ChIJexampleexampleexample",
    "window": {
      "start": "2026-06-05",
      "end": "2026-10-05",
      "complete": true
    },
    "guaranteeMet": "Yes",
    "firstNewTop3": {
      "keyword": "roofing contractors",
      "node": {
        "index": 1,
        "row": 1,
        "col": 2
      },
      "baseline": {
        "scanId": "ex4mpl30",
        "date": "2026-06-08",
        "rank": 5,
        "image": "https://example.test/geogrid/ex4mpl30.png"
      },
      "improved": {
        "scanId": "ex4mpl3a",
        "date": "2026-10-05",
        "rank": 3,
        "image": "https://example.test/geogrid/ex4mpl3a.png"
      }
    },
    "keywords": [
      {
        "keyword": "roofing contractors",
        "scansInWindow": 18,
        "newTop3Nodes": 1,
        "baselineScan": {
          "id": "ex4mpl30",
          "date": "2026-06-08",
          "keyword": "roofing contractors",
          "grid": {
            "size": 3,
            "pointDistance": 1,
            "measure": "miles",
            "centerLat": 40.0001,
            "centerLng": -75.0001
          },
          "nodes": 9,
          "rankedNodes": 8,
          "top3Nodes": 2,
          "newTop3Nodes": 0,
          "baseline": true,
          "ranks": [
            [
              4,
              5,
              5
            ],
            [
              2,
              1,
              null
            ],
            [
              6,
              7,
              9
            ]
          ],
          "image": "https://example.test/geogrid/ex4mpl3a.png"
        },
        "fourMonthScan": {
          "id": "ex4mpl3a",
          "date": "2026-10-05",
          "keyword": "roofing contractors",
          "grid": {
            "size": 3,
            "pointDistance": 1,
            "measure": "miles",
            "centerLat": 40.0001,
            "centerLng": -75.0001
          },
          "nodes": 9,
          "rankedNodes": 8,
          "top3Nodes": 3,
          "newTop3Nodes": 1,
          "baseline": false,
          "ranks": [
            [
              4,
              3,
              5
            ],
            [
              2,
              1,
              null
            ],
            [
              6,
              7,
              9
            ]
          ],
          "image": "https://example.test/geogrid/ex4mpl3a.png"
        }
      }
    ]
  },
  "meta": {
    "apiVersion": "v1",
    "requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
    "generatedAt": "2026-10-01T14:00:00.000Z",
    "keyword": null,
    "scansInWindow": 18
  }
}
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).
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "This key needs the maps:read scope."
  }
}
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).

Access

Needs an API key with maps:read.

Parameters

NameInTypeDescription
productId requiredpathstring, pattern ^\d{1,4}-\d{1,2}-\d{1,2}$The Maps product.
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/maps/013-1-1/competitors"
Response 200 · success
{
  "data": {
    "productId": "013-1-1",
    "businessName": "Example Roofing",
    "dot": "red",
    "grade": "Issues Found",
    "note": null,
    "lastRunAt": "2026-10-12T18:33:00Z",
    "scan": {
      "id": "ex4mpl3a",
      "date": "2026-10-06",
      "ageDays": 6
    },
    "reviewFloor": 40,
    "counts": {
      "red": 1,
      "yellow": 0,
      "green": 1
    },
    "competitors": [
      {
        "placeId": "ChIJexampleexampleexample1",
        "name": "Best Roofer Near Me Example City",
        "address": "12 Oak Ln, Example City, TX 75000",
        "reviews": 212,
        "rating": 4.9,
        "mapsUrl": "https://maps.google.com/?cid=1234567890",
        "dot": "red",
        "lines": [
          {
            "check": "name",
            "mark": "❌",
            "text": "Name: keyword-stuffed (a search phrase and a city added to the name)"
          },
          {
            "check": "address",
            "mark": "❌",
            "text": "Address: residential (USPS), shown on the profile — 12 Oak Ln, Example City, TX 75000"
          },
          {
            "check": "reviews",
            "mark": "✔️",
            "text": "Reviews: no burst or one-off pattern in the 100 read"
          }
        ]
      },
      {
        "placeId": "ChIJexampleexampleexample2",
        "name": "Smith Roofing",
        "address": "400 Main St, Example City, TX 75000",
        "reviews": 88,
        "rating": 4.7,
        "mapsUrl": "https://maps.google.com/?cid=2345678901",
        "dot": "green",
        "lines": [
          {
            "check": "name",
            "mark": "✔️",
            "text": "Name: no keyword padding"
          },
          {
            "check": "address",
            "mark": "✔️",
            "text": "Address: a business address (USPS: not residential, a PO box or a mailbox)"
          },
          {
            "check": "reviews",
            "mark": "✔️",
            "text": "Reviews: no burst or one-off pattern in the 100 read"
          }
        ]
      }
    ]
  },
  "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).
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "This key needs the maps:read scope."
  }
}
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

NameInTypeDescription
productId requiredpathstring, 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.
livequeryone of 0 | 11 = 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.
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/lsa/013-1-2"
Response 200 · success
{
  "data": {
    "productId": "013-1-2",
    "type": "lsa",
    "clientNumber": 13,
    "clientName": "Example Roofing Co",
    "businessName": "Example Roofing Co",
    "status": "Active",
    "lifecycle": "Account Live",
    "adsStatus": "Enabled",
    "performance": "Good",
    "lsaType": "New GGA",
    "fulfillmentTier": "GGA $500/m",
    "longTermWarranty": false,
    "location": {
      "label": "Springfield",
      "address": "100 Example St, Springfield, IL"
    },
    "dates": {
      "onboarding": "2024-04-02",
      "ads": "2024-04-20",
      "end": null,
      "statusChangedAt": "2024-04-02",
      "licenseExpiry": "2027-01-31",
      "insuranceExpiry": "2027-03-31"
    },
    "budgetLimit": 1500,
    "gbpLink": "https://maps.google.com/?cid=1234567890",
    "phone": {
      "trackingNumber": "+12175550124",
      "optedOutOfTrackingNumber": false,
      "forwardingNumber": "+12175550188"
    },
    "trackingNumberCheck": {
      "result": "pass",
      "verifiedBy": "calls",
      "summary": "Recent calls rang the Forwarding Number.",
      "numbersChangedSinceCheck": false,
      "checkedAt": "2026-10-01T12:20:04.000Z",
      "ageSeconds": 6236,
      "stale": false,
      "freshness": "cached",
      "refreshError": null,
      "checks": [
        {
          "id": "tn_on_file",
          "label": "Tracking number on file",
          "status": "pass",
          "expected": null,
          "actual": "***0123",
          "detail": "E.164 tracking number on file."
        },
        {
          "id": "ghl_forwarding",
          "label": "GHL forwards to the Forwarding Number",
          "status": "fail",
          "expected": "***0188",
          "actual": null,
          "detail": "GHL has no forwarding number for this tracking number."
        }
      ]
    },
    "href": "/api/v1/lsa/013-1-2",
    "clientHref": "/api/v1/clients/13"
  },
  "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).
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "This key needs the maps:read scope."
  }
}
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

NameInTypeDescription
productId requiredpathstring, 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.
livequeryone of 0 | 11 = 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.
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/gga/013-1-2"
Response 200 · success
{
  "data": {
    "productId": "013-1-2",
    "type": "lsa",
    "clientNumber": 13,
    "clientName": "Example Roofing Co",
    "businessName": "Example Roofing Co",
    "status": "Active",
    "lifecycle": "Account Live",
    "adsStatus": "Enabled",
    "performance": "Good",
    "lsaType": "New GGA",
    "fulfillmentTier": "GGA $500/m",
    "longTermWarranty": false,
    "location": {
      "label": "Springfield",
      "address": "100 Example St, Springfield, IL"
    },
    "dates": {
      "onboarding": "2024-04-02",
      "ads": "2024-04-20",
      "end": null,
      "statusChangedAt": "2024-04-02",
      "licenseExpiry": "2027-01-31",
      "insuranceExpiry": "2027-03-31"
    },
    "budgetLimit": 1500,
    "gbpLink": "https://maps.google.com/?cid=1234567890",
    "phone": {
      "trackingNumber": "+12175550124",
      "optedOutOfTrackingNumber": false,
      "forwardingNumber": "+12175550188"
    },
    "trackingNumberCheck": {
      "result": "pass",
      "verifiedBy": "calls",
      "summary": "Recent calls rang the Forwarding Number.",
      "numbersChangedSinceCheck": false,
      "checkedAt": "2026-10-01T12:20:04.000Z",
      "ageSeconds": 6236,
      "stale": false,
      "freshness": "cached",
      "refreshError": null,
      "checks": [
        {
          "id": "tn_on_file",
          "label": "Tracking number on file",
          "status": "pass",
          "expected": null,
          "actual": "***0123",
          "detail": "E.164 tracking number on file."
        },
        {
          "id": "ghl_forwarding",
          "label": "GHL forwards to the Forwarding Number",
          "status": "fail",
          "expected": "***0188",
          "actual": null,
          "detail": "GHL has no forwarding number for this tracking number."
        }
      ]
    },
    "href": "/api/v1/lsa/013-1-2",
    "clientHref": "/api/v1/clients/13"
  },
  "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).
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "This key needs the maps:read scope."
  }
}
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.

Access

Needs an API key with 5sos:read.

Parameters

NameInTypeDescription
clientNumber requiredquerystring, pattern ^\d{1,6}$The client whose products to list.
statusqueryone of Active | Paused | InactiveOnly products with this product status.
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/5sos?clientNumber=13"
Response 200 · success
{
  "data": [
    {
      "productId": "013-2-1",
      "type": "5sos",
      "clientNumber": 13,
      "clientName": "Example Roofing Co",
      "status": "Active",
      "tier": "PPR",
      "setupStatus": "Completed",
      "setupTier": 2,
      "triggerMethod": "Manual",
      "crm": {
        "name": "JobNimbus",
        "connected": true,
        "integrationDate": "2025-02-10"
      },
      "location": "Springfield, IL",
      "startDate": "2025-02-01",
      "dialingStartDate": "2025-02-15",
      "billingCadence": "Monthly",
      "feePerReview": 50,
      "monthlyReviewCap": 40,
      "timezone": "America/Chicago",
      "activationSpecialist": "Jordan Example",
      "assignedTo": "[email protected]",
      "campaigns": {
        "postAppointment": true,
        "jobComplete": true
      },
      "audits": {
        "day7": {
          "status": "Completed",
          "result": "Pass"
        },
        "day30": {
          "status": "Completed"
        }
      },
      "locations": [
        {
          "id": 101,
          "name": "Springfield",
          "gbpLink": "https://maps.google.com/?cid=1234567890",
          "reviewLink": "https://g.page/r/example/review",
          "mapsPlaceId": "ChIJexampleplaceid0001",
          "allocationPct": 100,
          "isActive": true
        }
      ],
      "href": "/api/v1/5sos/013-2-1",
      "clientHref": "/api/v1/clients/13"
    }
  ],
  "meta": {
    "apiVersion": "v1",
    "requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
    "generatedAt": "2026-10-01T14:00:00.000Z",
    "count": 1
  }
}
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).
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "This key needs the maps:read scope."
  }
}
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

NameInTypeDescription
productId requiredpathstring, 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 -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/5sos/013-2-1"
Response 200 · success
{
  "data": {
    "productId": "013-2-1",
    "type": "5sos",
    "clientNumber": 13,
    "clientName": "Example Roofing Co",
    "status": "Active",
    "tier": "PPR",
    "setupStatus": "Completed",
    "setupTier": 2,
    "triggerMethod": "Manual",
    "crm": {
      "name": "JobNimbus",
      "connected": true,
      "integrationDate": "2025-02-10"
    },
    "location": "Springfield, IL",
    "startDate": "2025-02-01",
    "dialingStartDate": "2025-02-15",
    "billingCadence": "Monthly",
    "feePerReview": 50,
    "monthlyReviewCap": 40,
    "timezone": "America/Chicago",
    "activationSpecialist": "Jordan Example",
    "assignedTo": "[email protected]",
    "campaigns": {
      "postAppointment": true,
      "jobComplete": true
    },
    "audits": {
      "day7": {
        "status": "Completed",
        "result": "Pass"
      },
      "day30": {
        "status": "Completed"
      }
    },
    "locations": [
      {
        "id": 101,
        "name": "Springfield",
        "gbpLink": "https://maps.google.com/?cid=1234567890",
        "reviewLink": "https://g.page/r/example/review",
        "mapsPlaceId": "ChIJexampleplaceid0001",
        "allocationPct": 100,
        "isActive": true
      }
    ],
    "href": "/api/v1/5sos/013-2-1",
    "clientHref": "/api/v1/clients/13"
  },
  "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).
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "This key needs the maps:read scope."
  }
}
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

NameInTypeDescription
typequeryone of maps | lsaOnly Maps or only LSA products.
statusqueryone of pass | fail | warn | unknown | not_applicable | uncheckedOnly products with this main status. unchecked = never checked yet.
clientNumberquerystring, pattern ^\d{1,6}$Only this client's products.
checkqueryone of tn_on_file | tn_unique | fwd_on_file | twilio_number_present | twilio_routing | twilio_fallback | twilio_sms | ghl_number_present | ghl_forwarding | recent_callsWith checkStatus: only products whose sub-status for this check matches, e.g. check=fwd_on_file&checkStatus=fail.
checkStatusqueryone of pass | fail | warn | unknown | skipped | not_applicableThe sub-status to match for check.
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/tracking-checks?status=fail"
Response 200 · success
{
  "data": [
    {
      "productId": "013-1-1",
      "type": "maps",
      "clientNumber": 13,
      "businessName": "Example Roofing",
      "productStatus": "Active",
      "status": "fail",
      "subStatuses": {
        "tn_on_file": "pass",
        "tn_unique": "pass",
        "fwd_on_file": "pass",
        "twilio_number_present": "pass",
        "twilio_routing": "pass",
        "twilio_fallback": "pass",
        "twilio_sms": "pass",
        "ghl_number_present": "pass",
        "ghl_forwarding": "fail",
        "recent_calls": "skipped"
      },
      "trackingNumberCheck": {
        "result": "fail",
        "verifiedBy": null,
        "summary": "GHL forwards ***0123 to nothing: calls go to GHL voicemail, not the Forwarding Number ***0188.",
        "numbersChangedSinceCheck": false,
        "checkedAt": "2026-10-01T12:20:04.000Z",
        "ageSeconds": 6236,
        "stale": false,
        "freshness": "cached",
        "refreshError": null,
        "checks": [
          {
            "id": "tn_on_file",
            "label": "Tracking number on file",
            "status": "pass",
            "expected": null,
            "actual": "***0123",
            "detail": "E.164 tracking number on file."
          },
          {
            "id": "ghl_forwarding",
            "label": "GHL forwards to the Forwarding Number",
            "status": "fail",
            "expected": "***0188",
            "actual": null,
            "detail": "GHL has no forwarding number for this tracking number."
          }
        ]
      },
      "href": "/api/v1/tracking-checks/013-1-1",
      "productHref": "/api/v1/maps/013-1-1",
      "clientHref": "/api/v1/clients/13"
    }
  ],
  "meta": {
    "apiVersion": "v1",
    "requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
    "generatedAt": "2026-10-01T14:00:00.000Z",
    "count": 1,
    "byStatus": {
      "pass": 0,
      "fail": 1,
      "warn": 0,
      "unknown": 0,
      "not_applicable": 0,
      "unchecked": 0
    }
  }
}
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).
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "This key needs the maps:read scope."
  }
}
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

NameInTypeDescription
productId requiredpathstring, 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 -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/tracking-checks/013-1-1"
Response 200 · success
{
  "data": {
    "productId": "013-1-1",
    "type": "maps",
    "clientNumber": 13,
    "businessName": "Example Roofing",
    "productStatus": "Active",
    "status": "fail",
    "subStatuses": {
      "tn_on_file": "pass",
      "tn_unique": "pass",
      "fwd_on_file": "pass",
      "twilio_number_present": "pass",
      "twilio_routing": "pass",
      "twilio_fallback": "pass",
      "twilio_sms": "pass",
      "ghl_number_present": "pass",
      "ghl_forwarding": "fail",
      "recent_calls": "skipped"
    },
    "trackingNumberCheck": {
      "result": "fail",
      "verifiedBy": null,
      "summary": "GHL forwards ***0123 to nothing: calls go to GHL voicemail, not the Forwarding Number ***0188.",
      "numbersChangedSinceCheck": false,
      "checkedAt": "2026-10-01T12:20:04.000Z",
      "ageSeconds": 6236,
      "stale": false,
      "freshness": "cached",
      "refreshError": null,
      "checks": [
        {
          "id": "tn_on_file",
          "label": "Tracking number on file",
          "status": "pass",
          "expected": null,
          "actual": "***0123",
          "detail": "E.164 tracking number on file."
        },
        {
          "id": "ghl_forwarding",
          "label": "GHL forwards to the Forwarding Number",
          "status": "fail",
          "expected": "***0188",
          "actual": null,
          "detail": "GHL has no forwarding number for this tracking number."
        }
      ]
    },
    "href": "/api/v1/tracking-checks/013-1-1",
    "productHref": "/api/v1/maps/013-1-1",
    "clientHref": "/api/v1/clients/13"
  },
  "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).
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "This key needs the maps:read scope."
  }
}
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

NameInTypeDescription
productId requiredpathstring, 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 -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/gbp/013-1-1"
Response 200 · success
{
  "data": {
    "productId": "013-1-1",
    "clientNumber": 13,
    "businessName": "Example Roofing Co",
    "placeId": "ChIJexampleplaceid0001",
    "localViking": {
      "locationId": "ab12cd34",
      "gbpId": "1234567890123456789"
    },
    "location": {
      "id": "ab12cd34",
      "gbp_id": "1234567890123456789",
      "name": "Example Roofing Co",
      "enabled": true,
      "place_id": "ChIJexampleplaceid0001",
      "store_code": "13",
      "labels": [
        "Example Roofing Co - Springfield IL"
      ],
      "address": "100 Example St, Springfield, IL 62701, USA",
      "city": "Springfield",
      "administrative_area": "Illinois",
      "postal_code": "62701",
      "country": "United States",
      "lat": 39.7817,
      "lng": -89.6501,
      "phone_number": "(217) 555-0123",
      "additional_phone_numbers": [
        "(217) 555-0188"
      ],
      "website_url": "https://example-roofing.example.test/",
      "primary_category": "Roofing contractor",
      "additional_categories": [
        "Gutter cleaning service"
      ],
      "description": "Example Roofing Co repairs and replaces roofs across Springfield.",
      "regular_hours": {
        "periods": [
          {
            "open_day": "MONDAY",
            "close_day": "MONDAY",
            "open_time": "08:00",
            "close_time": "17:00"
          }
        ]
      },
      "special_hours": {
        "special_hour_periods": [
          {
            "start_date": {
              "year": 2026,
              "month": 12,
              "day": 25
            },
            "is_closed": true,
            "open_time": "00:00",
            "close_time": "00:00"
          }
        ]
      },
      "opening_date": {
        "year": 2015,
        "month": 4,
        "day": 1
      },
      "service_area": {
        "business_type": "CUSTOMER_AND_BUSINESS_LOCATION",
        "places": {
          "place_infos": [
            {
              "name": "Chatham, IL, USA",
              "place_id": "ChIJexampleplaceid0002"
            }
          ]
        }
      },
      "gmb_url": "https://maps.google.com/maps?cid=1234567890",
      "states": [
        "can_delete"
      ],
      "note": null,
      "created_at": "2024-03-01T15:00:00.000Z"
    },
    "media": [
      {
        "id": "zx12ab34",
        "remote_id": "AF1QipExampleMediaId0001",
        "google_url": "https://lh3.googleusercontent.com/example-photo=s0",
        "category": "additional",
        "owner": "business",
        "media_format": "photo",
        "state": "live",
        "scheduled_at": null,
        "sync_errors": {},
        "remote_created_at": "2026-09-29T15:33:41.000Z"
      }
    ],
    "href": "/api/v1/gbp/013-1-1",
    "productHref": "/api/v1/maps/013-1-1"
  },
  "meta": {
    "apiVersion": "v1",
    "requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
    "generatedAt": "2026-10-01T14:00:00.000Z",
    "mediaCount": 1,
    "warnings": []
  }
}
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).
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "This key needs the maps:read scope."
  }
}
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

NameInTypeDescription
productId requiredpathstring, 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"
Response 200 · success
{
  "data": {
    "productId": "013-1-1",
    "updated": [
      "description",
      "regular_hours"
    ],
    "location": {
      "id": "ab12cd34",
      "gbp_id": "1234567890123456789",
      "name": "Example Roofing Co",
      "enabled": true,
      "place_id": "ChIJexampleplaceid0001",
      "store_code": "13",
      "labels": [
        "Example Roofing Co - Springfield IL"
      ],
      "address": "100 Example St, Springfield, IL 62701, USA",
      "city": "Springfield",
      "administrative_area": "Illinois",
      "postal_code": "62701",
      "country": "United States",
      "lat": 39.7817,
      "lng": -89.6501,
      "phone_number": "(217) 555-0123",
      "additional_phone_numbers": [
        "(217) 555-0188"
      ],
      "website_url": "https://example-roofing.example.test/",
      "primary_category": "Roofing contractor",
      "additional_categories": [
        "Gutter cleaning service"
      ],
      "description": "Example Roofing Co repairs and replaces roofs across Springfield.",
      "regular_hours": {
        "periods": [
          {
            "open_day": "MONDAY",
            "close_day": "MONDAY",
            "open_time": "08:00",
            "close_time": "17:00"
          }
        ]
      },
      "special_hours": {
        "special_hour_periods": [
          {
            "start_date": {
              "year": 2026,
              "month": 12,
              "day": 25
            },
            "is_closed": true,
            "open_time": "00:00",
            "close_time": "00:00"
          }
        ]
      },
      "opening_date": {
        "year": 2015,
        "month": 4,
        "day": 1
      },
      "service_area": {
        "business_type": "CUSTOMER_AND_BUSINESS_LOCATION",
        "places": {
          "place_infos": [
            {
              "name": "Chatham, IL, USA",
              "place_id": "ChIJexampleplaceid0002"
            }
          ]
        }
      },
      "gmb_url": "https://maps.google.com/maps?cid=1234567890",
      "states": [
        "can_delete"
      ],
      "note": null,
      "created_at": "2024-03-01T15:00:00.000Z"
    },
    "href": "/api/v1/gbp/013-1-1"
  },
  "meta": {
    "apiVersion": "v1",
    "requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
    "generatedAt": "2026-10-01T14:00:00.000Z",
    "warnings": []
  }
}
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).
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "This key needs the maps:read scope."
  }
}
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.
Response 422
{
  "error": {
    "code": "local_viking_rejected",
    "message": "Local Viking refused the categories change: Category not found.",
    "applied": [
      "description"
    ]
  }
}
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

NameInTypeDescription
productId requiredpathstring, 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"
Response 200 · success
{
  "data": {
    "productId": "013-1-1",
    "added": true,
    "category": "additional",
    "message": "Media has been successfully created.",
    "mediaId": "zx12ab34"
  },
  "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).
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "This key needs the maps:read scope."
  }
}
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.
Response 422
{
  "error": {
    "code": "local_viking_rejected",
    "message": "Local Viking refused the categories change: Category not found.",
    "applied": [
      "description"
    ]
  }
}
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

NameInTypeDescription
productId requiredpathstring, 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.
mediaId requiredpathstring, pattern ^[a-z0-9]{6,12}$The photo's id: media[].id from GET /gbp/{productId}.
Request
curl -X DELETE -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/gbp/013-1-1/media/zx12ab34"
Response 200 · success
{
  "data": {
    "productId": "013-1-1",
    "deleted": "zx12ab34",
    "backedUp": true
  },
  "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).
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "This key needs the maps:read scope."
  }
}
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.
Response 422
{
  "error": {
    "code": "local_viking_rejected",
    "message": "Local Viking refused the categories change: Category not found.",
    "applied": [
      "description"
    ]
  }
}
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

NameInTypeDescription
clientNumberquerystring, pattern ^\d{1,6}$Only this client's products.
statusqueryone of green | orange | red | uncheckedOnly 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.
stalequeryone of 0 | 11 = only products whose latest check is older than the stale window (or that have none): the ones to refresh. 0 = only fresh ones.
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/gbp-photos?status=orange"
Response 200 · success
{
  "data": [
    {
      "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"
    }
  ],
  "meta": {
    "apiVersion": "v1",
    "requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
    "generatedAt": "2026-10-01T14:00:00.000Z",
    "count": 1,
    "byStatus": {
      "green": 0,
      "orange": 1,
      "red": 0,
      "unchecked": 0
    },
    "staleHours": 24
  }
}
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).
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "This key needs the maps:read scope."
  }
}
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

NameInTypeDescription
productId requiredpathstring, 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 -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/gbp-photos/013-1-1"
Response 200 · success
{
  "data": {
    "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"
  },
  "meta": {
    "apiVersion": "v1",
    "requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
    "generatedAt": "2026-10-01T14:00:00.000Z",
    "staleHours": 24
  }
}
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).
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "This key needs the maps:read scope."
  }
}
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

NameInTypeDescription
productId requiredpathstring, 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).
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "This key needs the maps:read scope."
  }
}
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 · The product is not Active or Paused (code product_not_live): only live products are checked.
Response 409
{
  "error": {
    "code": "product_not_live",
    "message": "Maps product 013-1-9 is Inactive."
  }
}
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

NameInTypeDescription
clientNumberquerystring, pattern ^\d{1,6}$Only this client's 5SOS data. Give clientNumber or productId, not both.
productIdquerystring, 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.
stagequerystringOnly 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.
campaignquerystring, pattern ^(post_appt|post_job|bulk_upload|bulk_priority)(,(post_appt|post_job|bulk_upload|bulk_priority))*$Only these CIM campaigns, a comma list of post_appt, post_job, bulk_upload, bulk_priority.
hvFromquerystring, 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.
hvToquerystring, 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.
limitqueryintegerPage size.
cursorquerystring, pattern ^\d{1,15}$meta.nextCursor from the previous page (the last contactId; null on the last page). Not with X-Phone.
includequerystring, 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-Phoneheaderstring, 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.
Request
curl -H "Authorization: Bearer $CIM_API_KEY" -H "X-Phone: +12175550123" "https://admin.contractorindependence.com/api/v1/5sos/contacts?clientNumber=13&stage=rp&limit=50"
Response 200 · success
{
  "data": [
    {
      "contactId": 900000101,
      "clientNumber": 13,
      "productId": "013-2-1",
      "subLocationId": 88001,
      "hvDate": "2026-07-29",
      "stage": {
        "id": 31627,
        "key": "rp",
        "name": "Review Promised"
      },
      "campaign": {
        "joined": [
          "post_appt"
        ],
        "label": "Post-Appointment"
      },
      "rep": "Sam Inspector",
      "humanLinkOpens": 2,
      "lastHumanLinkOpenAt": "2026-09-16T14:02:11.000Z",
      "href": "/api/v1/5sos/contacts/900000101",
      "homeowner": {
        "firstName": "Alex",
        "lastName": "Sample",
        "phone": "+12175550123",
        "email": "[email protected]",
        "address": "1 Example St",
        "city": "Springfield",
        "state": "IL",
        "zip": "62701"
      }
    }
  ],
  "meta": {
    "apiVersion": "v1",
    "requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
    "generatedAt": "2026-10-01T14:00:00.000Z",
    "limit": 50,
    "nextCursor": "900000101",
    "truncated": false
  }
}
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.
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "include=homeowner needs the 5sos:personal scope."
  }
}
404 · Not found (code not_found): no such contact, call, live filter or 5SOS product.
Response 404
{
  "error": {
    "code": "not_found",
    "message": "No 5SOS product 013-2-9."
  }
}
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

NameInTypeDescription
contactId requiredpathstring, pattern ^\d{1,15}$The 5SOS contact: its CallTools contact id. A contact is one homeowner job for one client.
livequeryone of 0 | 11 (the default) adds live, the contact's state in CallTools now; 0 skips CallTools.
includequerystring, 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.
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/5sos/contacts/900000101"
Response 200 · success
{
  "data": {
    "contactId": 900000101,
    "clientNumber": 13,
    "productId": "013-2-1",
    "subLocationId": 88001,
    "hvDate": "2026-07-29",
    "stage": {
      "id": 31627,
      "key": "rp",
      "name": "Review Promised",
      "since": "2026-09-14T22:52:10.000Z"
    },
    "campaign": {
      "joined": [
        "post_appt"
      ],
      "label": "Post-Appointment"
    },
    "rep": "Sam Inspector",
    "humanLinkOpens": 2,
    "lastHumanLinkOpenAt": "2026-09-16T14:02:11.000Z",
    "href": "/api/v1/5sos/contacts/900000101",
    "homeowner": {
      "firstName": "Alex",
      "lastName": "Sample",
      "phone": "+12175550123",
      "email": "[email protected]",
      "address": "1 Example St",
      "city": "Springfield",
      "state": "IL",
      "zip": "62701"
    },
    "counts": {
      "calls": 6,
      "callsInbound": 1,
      "smsOut": 4,
      "smsIn": 2,
      "humanLinkOpens": 2
    },
    "lastCallAt": "2026-09-14T22:48:03.000Z",
    "lastSmsAt": "2026-09-15T13:01:40.000Z",
    "uniqueEvents": [
      {
        "disposition": "rp",
        "at": "2026-09-14T22:52:10.000Z",
        "agent": {
          "key": "sam.r",
          "name": "Sam R"
        },
        "creditedAgent": {
          "key": "sam.r",
          "name": "Sam R"
        }
      }
    ],
    "samePhoneContactIds": [
      900000087
    ],
    "samePhoneTruncated": false,
    "coverage": {
      "callsFrom": "2026-06-26",
      "smsFrom": "2026-05-01",
      "transcriptsFrom": "2026-06-22",
      "analysisFrom": "2026-05-11",
      "eventsFrom": "2026-01-04",
      "uniqueFrom": "2026-02-04",
      "clicksFrom": "2026-08-24"
    },
    "live": {
      "fetchedAt": "2026-10-01T14:00:00.000Z",
      "ready": true,
      "doNotContact": false,
      "suppressUntil": null,
      "contactDisposition": {
        "id": 31627,
        "name": "Review Promised"
      },
      "callDisposition": {
        "id": 900001,
        "name": "Answered"
      },
      "crmStage": "Home Visit",
      "campaignType": "post_appt",
      "campaignOff": false,
      "automateSms": [
        "rp1"
      ],
      "tags": [
        "io"
      ],
      "callsOutbound": 5,
      "callsInbound": 1,
      "callsHuman": 2,
      "callsMachine": 3,
      "lastCallAt": "2026-09-14T22:48:03.000Z",
      "lostReason": null,
      "stageMismatch": false
    }
  },
  "meta": {
    "apiVersion": "v1",
    "requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
    "generatedAt": "2026-10-01T14:00:00.000Z",
    "live": "ok"
  }
}
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.
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "include=homeowner needs the 5sos:personal scope."
  }
}
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

NameInTypeDescription
contactId requiredpathstring, pattern ^\d{1,15}$The 5SOS contact: its CallTools contact id. A contact is one homeowner job for one client.
kindsquerystringOnly 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).
sincequerystringOnly 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.
untilquerystringOnly 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.
orderqueryone of asc | descasc (the default): oldest first; desc: newest first (the exact reverse).
includequerystring, pattern ^(homeowner|text)(,(homeowner|text))*$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]).
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/5sos/contacts/900000101/timeline"
Response 200 · success
{
  "data": [
    {
      "at": "2026-09-14T22:48:03.000Z",
      "kind": "call",
      "id": 2300000001,
      "direction": "out",
      "agent": {
        "key": "sam.r",
        "name": "Sam R"
      },
      "ourNumber": "+12175550101",
      "homeownerNumber": "+12175550123",
      "campaign": {
        "id": 900064,
        "name": "Example Post Appointment"
      },
      "label": "Answered",
      "detail": {
        "durationSec": 130,
        "billsec": 115,
        "systemDisposition": "ANSWERED",
        "callDisposition": {
          "id": 900001,
          "name": "Answered"
        },
        "scored": true,
        "recording": {
          "available": true,
          "href": "/api/v1/5sos/calls/2300000001/recording"
        },
        "transcript": {
          "available": true,
          "chars": 1786,
          "truncated": false,
          "href": "/api/v1/5sos/calls/2300000001"
        }
      },
      "text": "A: Hi, is this Alex? It's Sam from Example Roofing Co. B: Yes, hi."
    },
    {
      "at": "2026-09-14T22:52:10.000Z",
      "kind": "contact_disposition",
      "id": 812345,
      "agent": {
        "key": "sam.r",
        "name": "Sam R"
      },
      "campaign": null,
      "label": "Review Promised",
      "detail": {
        "dispositionId": 31627,
        "prior": {
          "id": 31628,
          "name": "Active Convo"
        },
        "campaignToken": "post_appt",
        "liveOnCall": true,
        "lostReason": null
      }
    },
    {
      "at": "2026-09-14T23:01:40.000Z",
      "kind": "sms",
      "id": 98765,
      "direction": "in",
      "agent": null,
      "ourNumber": "+12175550101",
      "homeownerNumber": "+12175550123",
      "campaign": {
        "id": 900064,
        "name": "Example Post Appointment"
      },
      "label": "received",
      "detail": {
        "chars": 15
      },
      "text": "Thanks, will do"
    },
    {
      "at": "2026-09-15T13:02:11.000Z",
      "kind": "link_open",
      "id": 4001,
      "agent": null,
      "label": "link opened",
      "detail": {
        "isBot": false,
        "source": "sms"
      }
    },
    {
      "at": "2026-09-15T13:58:00.000Z",
      "kind": "admin_update",
      "id": 55,
      "agent": {
        "key": "sam.r",
        "name": "Sam R"
      },
      "label": "Review Generated",
      "detail": {
        "dispositionId": 31629,
        "webhookOk": true
      }
    },
    {
      "at": "2026-09-15T14:00:00.000Z",
      "kind": "review_decision",
      "id": 77,
      "agent": {
        "key": "ops.a",
        "name": null
      },
      "label": "confirmed",
      "detail": {
        "confidence": 100
      }
    }
  ],
  "meta": {
    "apiVersion": "v1",
    "requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
    "generatedAt": "2026-10-01T14:00:00.000Z",
    "count": 6,
    "truncated": false,
    "coverage": {
      "callsFrom": "2026-06-26",
      "smsFrom": "2026-05-01",
      "transcriptsFrom": "2026-06-22",
      "analysisFrom": "2026-05-11",
      "eventsFrom": "2026-01-04",
      "uniqueFrom": "2026-02-04",
      "clicksFrom": "2026-08-24"
    },
    "freshness": {
      "callsAsOf": "2026-10-01T13:40: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.
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "include=homeowner needs the 5sos:personal scope."
  }
}
404 · Not found (code not_found): no such contact, call, live filter or 5SOS product.
Response 404
{
  "error": {
    "code": "not_found",
    "message": "No 5SOS product 013-2-9."
  }
}
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

NameInTypeDescription
clientNumberquerystring, pattern ^\d{1,6}$Only this client's 5SOS data. Give clientNumber or productId, not both.
productIdquerystring, 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.
contactIdquerystring, pattern ^\d{1,15}$Only this contact's (its CallTools contact id).
agentquerystring, pattern ^[a-z0-9._-]{1,64}$Only this agent's: the email local part, e.g. sam.r.
sincequerystringStart 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.
untilquerystringEnd 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.
directionqueryone of in | outin (from the homeowner) or out (from us).
minBillsecqueryintegerOnly calls billed at least this many seconds (0 to 3600).
campaignIdquerystring, pattern ^\d{1,15}$Only this CallTools dialer campaign (GET /5sos/campaigns lists them).
transcribedqueryone of 0 | 11: only calls with a transcript; 0: only calls without.
limitqueryintegerPage size.
cursorquerystring, pattern ^\d{1,10}_\d{1,20}$meta.nextCursor from the previous page (null on the last page).
includequerystring, pattern ^(homeowner|text)(,(homeowner|text))*$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]).
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/5sos/calls?clientNumber=13&since=2026-09-24T14:00:00Z&until=2026-10-01T14:00:00Z"
Response 200 · success
{
  "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.
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "include=homeowner needs the 5sos:personal scope."
  }
}
404 · Not found (code not_found): no such contact, call, live filter or 5SOS product.
Response 404
{
  "error": {
    "code": "not_found",
    "message": "No 5SOS product 013-2-9."
  }
}
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).

Parameters

NameInTypeDescription
callId requiredpathstring, pattern ^\d{1,15}$The call's CallTools id.
includequerystring, pattern ^(homeowner|text)(,(homeowner|text))*$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]).
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/5sos/calls/2300000001"
Response 200 · success
{
  "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.
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "include=homeowner needs the 5sos:personal scope."
  }
}
404 · Not found (code not_found): no such contact, call, live filter or 5SOS product.
Response 404
{
  "error": {
    "code": "not_found",
    "message": "No 5SOS product 013-2-9."
  }
}
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.

Access

Needs an API key with 5sos:recordings.

Parameters

NameInTypeDescription
callId requiredpathstring, pattern ^\d{1,15}$The call's CallTools id.
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/5sos/calls/2300000001/recording"

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.
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "include=homeowner needs the 5sos:personal scope."
  }
}
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

NameInTypeDescription
clientNumberquerystring, pattern ^\d{1,6}$Only this client's 5SOS data. Give clientNumber or productId, not both.
productIdquerystring, 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.
contactIdquerystring, pattern ^\d{1,15}$Only this contact's (its CallTools contact id).
sincequerystringStart 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.
untilquerystringEnd 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.
directionqueryone of in | outin (from the homeowner) or out (from us).
campaignIdquerystring, pattern ^\d{1,15}$Only this CallTools dialer campaign (GET /5sos/campaigns lists them).
limitqueryintegerPage size.
cursorquerystring, pattern ^\d{1,10}_\d{1,20}$meta.nextCursor from the previous page (null on the last page).
includequerystring, pattern ^(homeowner|text)(,(homeowner|text))*$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]).
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/5sos/sms?clientNumber=13"
Response 200 · success
{
  "data": [
    {
      "id": 98765,
      "contactId": 900000101,
      "clientNumber": 13,
      "productId": "013-2-1",
      "at": "2026-09-30T15:05:00.000Z",
      "direction": "out",
      "agent": {
        "key": "sam.r",
        "name": "Sam R"
      },
      "ourNumber": "+12175550101",
      "homeownerNumber": "+12175550123",
      "campaign": {
        "id": 700001,
        "name": "5SOS (Example) Post Appointment"
      },
      "chars": 74,
      "text": "Hi Alex, thanks for your time today! Here is the link: jobreview.co/r/[link]"
    }
  ],
  "meta": {
    "apiVersion": "v1",
    "requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
    "generatedAt": "2026-10-01T14:00:00.000Z",
    "limit": 50,
    "nextCursor": "1790780700_98765",
    "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.
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "include=homeowner needs the 5sos:personal scope."
  }
}
404 · Not found (code not_found): no such contact, call, live filter or 5SOS product.
Response 404
{
  "error": {
    "code": "not_found",
    "message": "No 5SOS product 013-2-9."
  }
}
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

NameInTypeDescription
clientNumberquerystring, pattern ^\d{1,6}$Only this client's 5SOS data. Give clientNumber or productId, not both.
productIdquerystring, 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.
contactIdquerystring, pattern ^\d{1,15}$Only this contact's (its CallTools contact id).
agentquerystring, pattern ^[a-z0-9._-]{1,64}$Only this agent's: the email local part, e.g. sam.r.
typequerystring, pattern ^(contact|call|sms|email)(,(contact|call|sms|email))*$Only these kinds, a comma list of contact (stage changes), call, sms, email.
dispositionIdquerystring, 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.
campaignquerystring, pattern ^(post_appt|post_job|bulk_upload|bulk_priority)(,(post_appt|post_job|bulk_upload|bulk_priority))*$Only these CIM campaigns, a comma list of post_appt, post_job, bulk_upload, bulk_priority.
campaignIdquerystring, pattern ^\d{1,15}$Only events stamped with this CallTools dialer campaign. Contact events carry none, so not with type=contact (400).
sincequerystringStart 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.
untilquerystringEnd 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.
limitqueryintegerPage size.
cursorquerystring, pattern ^\d{1,10}_\d{1,20}$meta.nextCursor from the previous page (null on the last page).
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/5sos/events?clientNumber=13&type=contact"
Response 200 · success
{
  "data": [
    {
      "id": 812345,
      "contactId": 900000101,
      "clientNumber": 13,
      "productId": "013-2-1",
      "at": "2026-09-30T15:03:00.000Z",
      "type": "contact",
      "disposition": {
        "id": 31627,
        "name": "Review Promised"
      },
      "prior": {
        "id": 31628,
        "name": "Active Convo"
      },
      "agent": {
        "key": "sam.r",
        "name": "Sam R"
      },
      "campaignToken": "post_appt",
      "campaignId": null,
      "liveOnCall": true,
      "lostReason": null
    }
  ],
  "meta": {
    "apiVersion": "v1",
    "requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
    "generatedAt": "2026-10-01T14:00:00.000Z",
    "limit": 50,
    "nextCursor": "1790780580_812345",
    "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.
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "include=homeowner needs the 5sos:personal scope."
  }
}
404 · Not found (code not_found): no such contact, call, live filter or 5SOS product.
Response 404
{
  "error": {
    "code": "not_found",
    "message": "No 5SOS product 013-2-9."
  }
}
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

NameInTypeDescription
clientNumberquerystring, pattern ^\d{1,6}$Only this client's 5SOS data. Give clientNumber or productId, not both.
productIdquerystring, 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.
contactIdquerystring, pattern ^\d{1,15}$Only this contact's (its CallTools contact id).
dispositionqueryone of rp | rgrp (Review Promised) or rg (Review Generated). Default: both.
sincequerystringStart 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.
untilquerystringEnd 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.
limitqueryintegerPage size.
cursorquerystring, pattern ^\d{1,10}_\d{1,20}$meta.nextCursor from the previous page (null on the last page).
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/5sos/unique-events?clientNumber=13&since=2026-07-01"
Response 200 · success
{
  "data": [
    {
      "id": 5501,
      "contactId": 900000101,
      "clientNumber": 13,
      "productId": "013-2-1",
      "disposition": "rp",
      "at": "2026-09-30T15:03:00.000Z",
      "agent": {
        "key": "sam.r",
        "name": "Sam R"
      },
      "creditedAgent": {
        "key": "sam.r",
        "name": "Sam R"
      },
      "campaignToken": "post_appt"
    }
  ],
  "meta": {
    "apiVersion": "v1",
    "requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
    "generatedAt": "2026-10-01T14:00:00.000Z",
    "limit": 50,
    "nextCursor": "1790780580_5501",
    "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.
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "include=homeowner needs the 5sos:personal scope."
  }
}
404 · Not found (code not_found): no such contact, call, live filter or 5SOS product.
Response 404
{
  "error": {
    "code": "not_found",
    "message": "No 5SOS product 013-2-9."
  }
}
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

NameInTypeDescription
daysqueryintegerThe activity counts cover the last this many days, up to now.
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/5sos/campaigns?days=7"
Response 200 · success
{
  "data": [
    {
      "id": 990101,
      "name": "5SOS Example (IO) Post Appointment",
      "active": true,
      "liveFilterId": 990201,
      "callRecording": true,
      "activity": {
        "callsOut": 412,
        "callsIn": 37,
        "contactsDialled": 186,
        "smsOut": 240,
        "smsIn": 61
      },
      "liveFilterHref": "/api/v1/5sos/live-filters/990201/contacts"
    }
  ],
  "meta": {
    "apiVersion": "v1",
    "requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
    "generatedAt": "2026-10-01T14:00:00.000Z",
    "count": 1,
    "days": 7,
    "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.
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "include=homeowner needs the 5sos:personal scope."
  }
}
404 · Not found (code not_found): no such contact, call, live filter or 5SOS product.
Response 404
{
  "error": {
    "code": "not_found",
    "message": "No 5SOS product 013-2-9."
  }
}
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.

Access

Needs an API key with 5sos:activity.

Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/5sos/live-filters"
Response 200 · success
{
  "data": [
    {
      "id": 990201,
      "name": "5SOS Example (IO) Post Appointment",
      "description": "Post-Appointment homeowners still in initial outreach",
      "active": true,
      "dialableNow": 41,
      "total": 58,
      "lastQueryOn": "2026-10-01T13:58:12.000Z",
      "campaignIds": [
        990101
      ],
      "conditions": {
        "operator": "and",
        "childSets": [
          {
            "operator": "or",
            "childSets": [],
            "conditions": [
              {
                "operator": "less_than_or_equal",
                "valueLeft": "calls_outbound",
                "valueRight": 45
              }
            ]
          }
        ],
        "conditions": [
          {
            "operator": "equals",
            "valueLeft": "disposition_id",
            "valueRight": 31922
          }
        ]
      },
      "filterTags": [],
      "excludeTags": [
        "990301"
      ],
      "filterCallDispositions": [],
      "excludeCallDispositions": [],
      "localWindow": {
        "earliestStart": "09:00:00",
        "latestEnd": "20:00:00"
      },
      "suppressMinutes": {
        "dial": 240,
        "humanAnswer": 1440,
        "machineAnswer": 240,
        "inboundCall": 60
      },
      "respectSuppressUntil": true,
      "filterContactReady": true,
      "orderBy": null,
      "contactsHref": "/api/v1/5sos/live-filters/990201/contacts"
    }
  ],
  "meta": {
    "apiVersion": "v1",
    "requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
    "generatedAt": "2026-10-01T14:00:00.000Z",
    "count": 1
  }
}
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.
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "include=homeowner needs the 5sos:personal scope."
  }
}
404 · Not found (code not_found): no such contact, call, live filter or 5SOS product.
Response 404
{
  "error": {
    "code": "not_found",
    "message": "No 5SOS product 013-2-9."
  }
}
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 contacts in one live filter now

GET/api/v1/5sos/live-filters/{liveFilterId}/contacts

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

NameInTypeDescription
liveFilterId requiredpathstring, pattern ^\d{1,15}$The CallTools live filter (GET /5sos/live-filters lists them).
limitqueryintegerPage size.
cursorquerystring, 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.
includequerystring, 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.
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/5sos/live-filters/990201/contacts?limit=2"
Response 200 · success
{
  "data": [
    {
      "contactId": 900000101,
      "inMirror": true,
      "clientNumber": 13,
      "productId": "013-2-1",
      "stage": {
        "id": 31922,
        "key": "initial",
        "name": "Initial"
      }
    }
  ],
  "meta": {
    "apiVersion": "v1",
    "requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
    "generatedAt": "2026-10-01T14:00:00.000Z",
    "liveFilterId": 990201,
    "count": 41,
    "limit": 2,
    "nextCursor": "p2s2"
  }
}
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.
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "include=homeowner needs the 5sos:personal scope."
  }
}
404 · Not found (code not_found): no such contact, call, live filter or 5SOS product.
Response 404
{
  "error": {
    "code": "not_found",
    "message": "No 5SOS product 013-2-9."
  }
}
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

NameInTypeDescription
dayStartquerystring, 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.
dayEndquerystring, pattern ^\d{4}-\d{2}-\d{2}$Last day, included, New York time (YYYY-MM-DD). Default: today.
agentEmailquerystringOnly 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.
clientNumberquerystring, pattern ^\d{1,6}$Only this client (the page's Client filter).
bookViewqueryone of active | allactive (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.
campaignquerystring, pattern ^(post_appt|post_job|bulk_upload|bulk_priority)(,(post_appt|post_job|bulk_upload|bulk_priority))*$Only these CIM campaigns, a comma list of post_appt, post_job, bulk_upload, bulk_priority.
groupByqueryone of clientclient: one row per client instead of per day or week (the page's Performance by Client table).
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/5sos/reports/inputs-outcomes?dayStart=2026-09-02&dayEnd=2026-10-01"
Response 200 · success
{
  "data": {
    "windowStart": "2026-09-02",
    "windowEnd": "2026-10-02",
    "period": "last_12_weeks",
    "bucketType": "week",
    "weeks": [
      {
        "weekStart": "2026-09-28",
        "hv": 52,
        "hvComplete": 18,
        "c": 25,
        "rp": 10,
        "rg": 4,
        "calls": 118,
        "uniqueDials": 61,
        "sms": 140,
        "hvDials": 0.92,
        "dialsC": 0.21,
        "hvC": 0.48,
        "cRp": 0.39,
        "rpRg": 0.41,
        "hvR": 0.08,
        "hvDone": 0.35
      }
    ],
    "overall": {
      "hv": 212,
      "hvComplete": 74,
      "c": 102,
      "rp": 40,
      "rg": 17,
      "calls": 486,
      "sms": 590,
      "uniqueDials": 230,
      "hvDials": 0.92,
      "dialsC": 0.21,
      "hvC": 0.48,
      "cRp": 0.39,
      "rpRg": 0.41,
      "hvR": 0.08,
      "hvDone": 0.35
    },
    "clients": [
      {
        "clientNumber": "13",
        "clientName": "Example Roofing Co"
      }
    ],
    "agents": [
      {
        "agentEmail": "[email protected]",
        "agentName": "Sam R"
      }
    ]
  },
  "meta": {
    "apiVersion": "v1",
    "requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
    "generatedAt": "2026-10-01T14:00:00.000Z",
    "dayStart": "2026-09-02",
    "dayEnd": "2026-10-01"
  }
}
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.
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "include=homeowner needs the 5sos:personal scope."
  }
}
404 · Not found (code not_found): no such contact, call, live filter or 5SOS product.
Response 404
{
  "error": {
    "code": "not_found",
    "message": "No 5SOS product 013-2-9."
  }
}
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

NameInTypeDescription
metric requiredqueryone of hv | hvComplete | calls | sms | c | rp | rg | uniqueDials | hvDials | dialsC | hvC | cRp | rpRg | hvR | hvDoneThe column.
weekStartquerystring, pattern ^\d{4}-\d{2}-\d{2}$The row: its day, or its Monday. None = the overall row.
bucketTypequeryone of day | weekWhether weekStart is a day or a Monday week.
dayStartquerystring, 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.
dayEndquerystring, pattern ^\d{4}-\d{2}-\d{2}$Last day, included, New York time (YYYY-MM-DD). Default: today.
agentEmailquerystringOnly 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.
clientNumberquerystring, pattern ^\d{1,6}$Only this client (the page's Client filter).
bookViewqueryone of active | allactive (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.
campaignquerystring, pattern ^(post_appt|post_job|bulk_upload|bulk_priority)(,(post_appt|post_job|bulk_upload|bulk_priority))*$Only these CIM campaigns, a comma list of post_appt, post_job, bulk_upload, bulk_priority.
includequerystring, pattern ^(homeowner)(,(homeowner))*$homeowner: add the homeowner's name to each row. Needs the 5sos:personal scope (403 without it).
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/5sos/reports/inputs-outcomes-drilldown?metric=rg&dayStart=2026-09-02&dayEnd=2026-10-01"
Response 200 · success
{
  "data": {
    "metric": "rg",
    "weekStart": null,
    "rangeStart": "2026-09-02",
    "rangeEnd": "2026-10-02",
    "numerator": 17,
    "denominator": null,
    "rows": [
      {
        "id": "880001",
        "contactId": 900000101,
        "clientName": "Example Roofing Co",
        "inspectorName": "Jordan Inspector",
        "agentName": "[email protected]",
        "ts": "2026-09-30T15:12:04.000Z",
        "detail": "Review Generated"
      }
    ],
    "truncated": false
  },
  "meta": {
    "apiVersion": "v1",
    "requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
    "generatedAt": "2026-10-01T14:00:00.000Z",
    "dayStart": "2026-09-02",
    "dayEnd": "2026-10-01"
  }
}
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.
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "include=homeowner needs the 5sos:personal scope."
  }
}
404 · Not found (code not_found): no such contact, call, live filter or 5SOS product.
Response 404
{
  "error": {
    "code": "not_found",
    "message": "No 5SOS product 013-2-9."
  }
}
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

NameInTypeDescription
dayStartquerystring, 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.
dayEndquerystring, pattern ^\d{4}-\d{2}-\d{2}$Last day, included, New York time (YYYY-MM-DD). Default: today.
agentEmailquerystringOnly 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.
clientNumberquerystring, pattern ^\d{1,6}$Only this client (the page's Client filter).
bookViewqueryone of active | allactive (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.
campaignquerystring, pattern ^(post_appt|post_job|bulk_upload|bulk_priority)(,(post_appt|post_job|bulk_upload|bulk_priority))*$Only these CIM campaigns, a comma list of post_appt, post_job, bulk_upload, bulk_priority.
grainqueryone of week | month | clientCohorts by HV week (Monday, default), HV month, or client.
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/5sos/reports/conversion?grain=week&dayStart=2026-09-02&dayEnd=2026-10-01"
Response 200 · success
{
  "data": {
    "weeks": [
      {
        "cohort": "2026-09-07",
        "lockDay": "2026-09-28",
        "locked": true,
        "hv": 48,
        "hvIdx": 50,
        "calls": 196,
        "c": 23,
        "rp": 9,
        "rg": 4,
        "rgIdx": 3,
        "rgRpLive": 4,
        "rpIdxDen": 9,
        "cIdxDen": 24,
        "lost": 6,
        "nonComplete": 38,
        "within24": 41,
        "ioCalls": 160,
        "callsNet": 188,
        "cimLost": 1,
        "removed": 0,
        "rlr24": 5,
        "completed": 10,
        "pipeComplete": 31,
        "liveComplete": 31,
        "liveIncomplete": 17,
        "liveIoLt4": 12,
        "liveIoGe4": 9,
        "liveRp": 5,
        "liveCallback": 2,
        "liveActive": 1,
        "pipeCompleteRate": 0.65,
        "roRgLive": 0.08,
        "roRgIdx": 0.06,
        "hvC": 0.48,
        "cRp": 0.39,
        "rpRgLive": 0.44,
        "rpRgIdx": 0.33,
        "callsPerRo": 4.08,
        "cRLive": 0.17,
        "cRIdx": 0.13,
        "dialed24": 0.85,
        "caC": 0.12,
        "caIoPerRo": 3.33,
        "caPerRo": 3.92,
        "removedRate": 0,
        "complaintRate": 0.04,
        "rlr24Rate": 0.56
      }
    ],
    "overall": {
      "hv": 48,
      "hvIdx": 50,
      "calls": 196,
      "c": 23,
      "rp": 9,
      "rg": 4,
      "rgIdx": 3,
      "rgRpLive": 4,
      "rpIdxDen": 9,
      "cIdxDen": 24,
      "lost": 6,
      "nonComplete": 38,
      "within24": 41,
      "ioCalls": 160,
      "callsNet": 188,
      "cimLost": 1,
      "removed": 0,
      "rlr24": 5,
      "completed": 10,
      "pipeComplete": 31,
      "liveComplete": 31,
      "liveIncomplete": 17,
      "liveIoLt4": 12,
      "liveIoGe4": 9,
      "liveRp": 5,
      "liveCallback": 2,
      "liveActive": 1,
      "pipeCompleteRate": 0.65,
      "roRgLive": 0.08,
      "roRgIdx": 0.06,
      "hvC": 0.48,
      "cRp": 0.39,
      "rpRgLive": 0.44,
      "rpRgIdx": 0.33,
      "callsPerRo": 4.08,
      "cRLive": 0.17,
      "cRIdx": 0.13,
      "dialed24": 0.85,
      "caC": 0.12,
      "caIoPerRo": 3.33,
      "caPerRo": 3.92,
      "removedRate": 0,
      "complaintRate": 0.04,
      "rlr24Rate": 0.56,
      "locked": false,
      "lockDay": "2026-10-19"
    },
    "grain": "week",
    "maturationDays": 14,
    "settingsSource": "reporting_settings",
    "today": "2026-10-01",
    "tiles": {
      "wanted": 4,
      "gate": 0.95,
      "lookbackWeeks": 26,
      "matureScanWeeks": 10,
      "mature": {
        "cohorts": [
          "2026-08-31",
          "2026-09-07"
        ],
        "cohortCount": 2,
        "hv": 96,
        "hvIdx": 99,
        "rg": 9,
        "rgIdx": 7,
        "roRgLive": 0.09,
        "roRgIdx": 0.07,
        "pipeCompleteRate": 0.96,
        "locked": true
      },
      "offset2": {
        "cohorts": [
          "2026-08-31",
          "2026-09-07"
        ],
        "cohortCount": 2,
        "hv": 96,
        "hvIdx": 99,
        "rg": 9,
        "rgIdx": 7,
        "roRgLive": 0.09,
        "roRgIdx": 0.07,
        "pipeCompleteRate": 0.96,
        "locked": true
      }
    },
    "targets": {
      "calls_per_ro": {
        "good": 0.3,
        "near": 0.2,
        "higherIsBetter": true
      },
      "c_per_call": {
        "good": 0.3,
        "near": 0.2,
        "higherIsBetter": true
      },
      "rp_per_c": {
        "good": 0.3,
        "near": 0.2,
        "higherIsBetter": true
      },
      "rp_rg": {
        "good": 0.3,
        "near": 0.2,
        "higherIsBetter": true
      },
      "c_r": {
        "good": 0.3,
        "near": 0.2,
        "higherIsBetter": true
      },
      "ro_r": {
        "good": 0.3,
        "near": 0.2,
        "higherIsBetter": true
      },
      "complete": {
        "good": 0.3,
        "near": 0.2,
        "higherIsBetter": true
      },
      "incomplete": {
        "good": 0.3,
        "near": 0.2,
        "higherIsBetter": true
      },
      "removed": {
        "good": 0.3,
        "near": 0.2,
        "higherIsBetter": true
      },
      "complaint": {
        "good": 0.3,
        "near": 0.2,
        "higherIsBetter": true
      },
      "dialed24": {
        "good": 0.3,
        "near": 0.2,
        "higherIsBetter": true
      },
      "rlr24": {
        "good": 0.3,
        "near": 0.2,
        "higherIsBetter": true
      }
    }
  },
  "meta": {
    "apiVersion": "v1",
    "requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
    "generatedAt": "2026-10-01T14:00:00.000Z",
    "dayStart": "2026-09-02",
    "dayEnd": "2026-10-01"
  }
}
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.
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "include=homeowner needs the 5sos:personal scope."
  }
}
404 · Not found (code not_found): no such contact, call, live filter or 5SOS product.
Response 404
{
  "error": {
    "code": "not_found",
    "message": "No 5SOS product 013-2-9."
  }
}
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

NameInTypeDescription
metric requiredqueryone of hv | c | rp | rg | lost | nonComplete | liveComplete | liveIncomplete | liveIoLt4 | liveIoGe4 | liveRp | liveCallback | liveActiveThe stage (the live... ones are the pipeline columns).
cohortquerystringThe cohort: its Monday or first of the month (YYYY-MM-DD), or with grain=client the client number. None = the overall column.
grainqueryone of week | month | clientCohorts by HV week (Monday, default), HV month, or client.
dayStartquerystring, 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.
dayEndquerystring, pattern ^\d{4}-\d{2}-\d{2}$Last day, included, New York time (YYYY-MM-DD). Default: today.
agentEmailquerystringOnly 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.
clientNumberquerystring, pattern ^\d{1,6}$Only this client (the page's Client filter).
bookViewqueryone of active | allactive (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.
campaignquerystring, pattern ^(post_appt|post_job|bulk_upload|bulk_priority)(,(post_appt|post_job|bulk_upload|bulk_priority))*$Only these CIM campaigns, a comma list of post_appt, post_job, bulk_upload, bulk_priority.
includequerystring, pattern ^(homeowner)(,(homeowner))*$homeowner: add the homeowner's name to each row. Needs the 5sos:personal scope (403 without it).
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/5sos/reports/conversion-drilldown?metric=rg&cohort=2026-09-07&dayStart=2026-09-02&dayEnd=2026-10-01"
Response 200 · success
{
  "data": {
    "metric": "rg",
    "cohort": "2026-09-07",
    "grain": "week",
    "maturationDays": 14,
    "rows": [
      {
        "contactId": 900000101,
        "clientName": "Example Roofing Co",
        "hvDate": "2026-09-08",
        "cDate": "2026-09-09T16:02:11Z",
        "rpDate": "2026-09-09T16:05:40Z",
        "rgDate": "2026-09-12T13:20:00Z",
        "lostDate": null,
        "rgInIdx": 1
      }
    ],
    "truncated": false
  },
  "meta": {
    "apiVersion": "v1",
    "requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
    "generatedAt": "2026-10-01T14:00:00.000Z",
    "dayStart": "2026-09-02",
    "dayEnd": "2026-10-01"
  }
}
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.
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "include=homeowner needs the 5sos:personal scope."
  }
}
404 · Not found (code not_found): no such contact, call, live filter or 5SOS product.
Response 404
{
  "error": {
    "code": "not_found",
    "message": "No 5SOS product 013-2-9."
  }
}
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

NameInTypeDescription
dayStartquerystring, 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.
dayEndquerystring, pattern ^\d{4}-\d{2}-\d{2}$Last day, included, New York time (YYYY-MM-DD). Default: today.
clientNumberquerystring, pattern ^\d{1,6}$Only this client (the page's Client filter).
bookViewqueryone of active | allactive (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.
campaignquerystring, pattern ^(post_appt|post_job|bulk_upload|bulk_priority)(,(post_appt|post_job|bulk_upload|bulk_priority))*$Only these CIM campaigns, a comma list of post_appt, post_job, bulk_upload, bulk_priority.
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/5sos/reports/conversion-agents?dayStart=2026-09-02&dayEnd=2026-10-01"
Response 200 · success
{
  "data": {
    "cohorts": [
      {
        "cohort": "2026-09-07",
        "lockDay": "2026-09-28",
        "locked": true,
        "hv": 48,
        "hvIdx": 50
      }
    ],
    "series": [
      {
        "agent": "sam.r",
        "cohort": "2026-09-07",
        "c": 9,
        "rp": 4,
        "rg": 2,
        "rgIdx": 2,
        "rpIdxDen": 4
      }
    ],
    "maturationDays": 14,
    "settingsSource": "reporting_settings",
    "today": "2026-10-01"
  },
  "meta": {
    "apiVersion": "v1",
    "requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
    "generatedAt": "2026-10-01T14:00:00.000Z",
    "dayStart": "2026-09-02",
    "dayEnd": "2026-10-01"
  }
}
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.
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "include=homeowner needs the 5sos:personal scope."
  }
}
404 · Not found (code not_found): no such contact, call, live filter or 5SOS product.
Response 404
{
  "error": {
    "code": "not_found",
    "message": "No 5SOS product 013-2-9."
  }
}
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

NameInTypeDescription
dayStartquerystring, 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.
dayEndquerystring, pattern ^\d{4}-\d{2}-\d{2}$Last day, included, New York time (YYYY-MM-DD). Default: today.
agentEmailquerystringOnly 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.
clientNumberquerystring, pattern ^\d{1,6}$Only this client (the page's Client filter).
bookViewqueryone of active | allactive (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.
campaignquerystring, pattern ^(post_appt|post_job|bulk_upload|bulk_priority)(,(post_appt|post_job|bulk_upload|bulk_priority))*$Only these CIM campaigns, a comma list of post_appt, post_job, bulk_upload, bulk_priority.
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/5sos/reports/performance?clientNumber=13"
Response 200 · success
{
  "data": {
    "period": "last_12_weeks",
    "windowStart": "2026-09-02",
    "windowEnd": "2026-10-02",
    "counts": {
      "hv": 212,
      "within24": 180,
      "ioGte4": 140,
      "hvConv": 102,
      "hvRp": 40,
      "hvRg": 17,
      "hvLost": 31,
      "hvCimLost": 2,
      "terminal": 48
    },
    "metrics": {
      "hvv": {
        "value": 212
      },
      "hvc": {
        "value": 0.4811320754716981,
        "num": 102,
        "den": 212
      },
      "crp": {
        "value": 0.39215686274509803,
        "num": 40,
        "den": 102
      },
      "rprg": {
        "value": 0.425,
        "num": 17,
        "den": 40
      },
      "hvr": {
        "value": 0.3541666666666667,
        "num": 17,
        "den": 48
      },
      "s2ca": {
        "value": 0.8490566037735849,
        "num": 180,
        "den": 212
      },
      "cahv": {
        "value": 3.4
      },
      "cahvAll": {
        "value": 4.1
      },
      "sqr": {
        "value": 0.660377358490566,
        "num": 140,
        "den": 212
      },
      "sqrTerminal": {
        "value": 0.22641509433962265,
        "num": 48,
        "den": 212
      },
      "complaint": {
        "value": 0.0196078431372549,
        "num": 2,
        "den": 102
      },
      "openRate": {
        "value": 0.4066666666666667,
        "num": 61,
        "den": 150
      }
    },
    "fetchedAt": "2026-10-01T14:00:00.000Z"
  },
  "meta": {
    "apiVersion": "v1",
    "requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
    "generatedAt": "2026-10-01T14:00:00.000Z",
    "dayStart": "2026-09-02",
    "dayEnd": "2026-10-01"
  }
}
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.
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "include=homeowner needs the 5sos:personal scope."
  }
}
404 · Not found (code not_found): no such contact, call, live filter or 5SOS product.
Response 404
{
  "error": {
    "code": "not_found",
    "message": "No 5SOS product 013-2-9."
  }
}
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

NameInTypeDescription
dayStartquerystring, 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.
dayEndquerystring, pattern ^\d{4}-\d{2}-\d{2}$Last day, included, New York time (YYYY-MM-DD). Default: today.
agentEmailquerystringOnly 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.
clientNumberquerystring, pattern ^\d{1,6}$Only this client (the page's Client filter).
bookViewqueryone of active | allactive (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.
campaignquerystring, pattern ^(post_appt|post_job|bulk_upload|bulk_priority)(,(post_appt|post_job|bulk_upload|bulk_priority))*$Only these CIM campaigns, a comma list of post_appt, post_job, bulk_upload, bulk_priority.
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/5sos/reports/scoreboard?dayStart=2026-09-02&dayEnd=2026-10-01"
Response 200 · success
{
  "data": {
    "period": "last_12_weeks",
    "windowStart": "2026-09-02",
    "windowEnd": "2026-10-02",
    "overall": {
      "rg": 6,
      "rp": 14,
      "attempts": 410,
      "conv": 38,
      "caIO": 300,
      "caRP": 110,
      "activeHours": 30.7,
      "rlr24": 8,
      "rpRgIdx": 5,
      "rpIdxDen": 14,
      "crg": 0.13,
      "cac": 0.09,
      "crp": 0.37,
      "rprg": 0.43,
      "rprgIdx": 0.36,
      "rlr24Rate": 0.57,
      "caPerHr": 13.4
    },
    "agents": [
      {
        "agentLocal": "sam.r",
        "agentName": "Sam R",
        "rg": 6,
        "rp": 14,
        "attempts": 410,
        "conv": 38,
        "caIO": 300,
        "caRP": 110,
        "activeHours": 30.7,
        "rlr24": 8,
        "rpRgIdx": 5,
        "rpIdxDen": 14,
        "crg": 0.13,
        "cac": 0.09,
        "crp": 0.37,
        "rprg": 0.43,
        "rprgIdx": 0.36,
        "rlr24Rate": 0.57,
        "caPerHr": 13.4
      }
    ],
    "maturationDays": 14,
    "settingsSource": "reporting_settings",
    "fetchedAt": "2026-10-01T14:00:00.000Z"
  },
  "meta": {
    "apiVersion": "v1",
    "requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
    "generatedAt": "2026-10-01T14:00:00.000Z",
    "dayStart": "2026-09-02",
    "dayEnd": "2026-10-01"
  }
}
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.
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "include=homeowner needs the 5sos:personal scope."
  }
}
404 · Not found (code not_found): no such contact, call, live filter or 5SOS product.
Response 404
{
  "error": {
    "code": "not_found",
    "message": "No 5SOS product 013-2-9."
  }
}
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

NameInTypeDescription
dayStartquerystring, 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.
dayEndquerystring, pattern ^\d{4}-\d{2}-\d{2}$Last day, included, New York time (YYYY-MM-DD). Default: today.
agentEmailquerystringOnly 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.
clientNumberquerystring, pattern ^\d{1,6}$Only this client (the page's Client filter).
campaignquerystring, pattern ^(post_appt|post_job|bulk_upload|bulk_priority)(,(post_appt|post_job|bulk_upload|bulk_priority))*$Only these CIM campaigns, a comma list of post_appt, post_job, bulk_upload, bulk_priority.
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/5sos/reports/lost?dayStart=2026-09-02&dayEnd=2026-10-01"
Response 200 · success
{
  "data": {
    "windowNyStart": "2026-09-02",
    "rangeStart": "2026-09-02",
    "rangeEnd": "2026-10-01",
    "filtered": true,
    "source": "db_scoped",
    "lostAllTime": 31,
    "lost30d": 33,
    "reasonsAllTime": 6,
    "reasons30d": 6,
    "totalHvs": 4120,
    "breakdown": [
      {
        "reason": "initial_no_response",
        "allTime": 12,
        "last30d": 13,
        "pinned": true
      }
    ]
  },
  "meta": {
    "apiVersion": "v1",
    "requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
    "generatedAt": "2026-10-01T14:00:00.000Z",
    "dayStart": "2026-09-02",
    "dayEnd": "2026-10-01"
  }
}
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.
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "include=homeowner needs the 5sos:personal scope."
  }
}
404 · Not found (code not_found): no such contact, call, live filter or 5SOS product.
Response 404
{
  "error": {
    "code": "not_found",
    "message": "No 5SOS product 013-2-9."
  }
}
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

NameInTypeDescription
clientNumber requiredpathstring, pattern ^\d{1,6}$The client number. Leading zeros are accepted ('13' and '013' are the same client).
includequerystring, 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).
Request
curl -H "Authorization: Bearer $CIM_API_KEY" "https://admin.contractorindependence.com/api/v1/5sos/reports/client/13"
Response 200 · success
{
  "data": {
    "isFiveSosClient": true,
    "hasData": true,
    "homeVisits": 310,
    "reviewsPromised": 12,
    "reviewsGenerated": 21,
    "rgList": [
      {
        "rep": "Jordan Inspector",
        "campaign": "Post-Appointment",
        "date": "2026-09-30"
      }
    ],
    "rpList": [
      {
        "rep": "Jordan Inspector",
        "campaign": "Post-Appointment",
        "date": "2026-09-30"
      }
    ],
    "hvList": [
      {
        "rep": "Jordan Inspector",
        "campaign": "Post-Appointment",
        "date": "2026-09-30"
      }
    ]
  },
  "meta": {
    "apiVersion": "v1",
    "requestId": "3f2a6c1e-0d4b-4c9a-9a51-7b1d2e3f4a5b",
    "generatedAt": "2026-10-01T14:00:00.000Z",
    "truncated": false
  }
}
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.
Response 403
{
  "error": {
    "code": "forbidden_scope",
    "message": "include=homeowner needs the 5sos:personal scope."
  }
}
404 · Not found (code not_found): no such contact, call, live filter or 5SOS product.
Response 404
{
  "error": {
    "code": "not_found",
    "message": "No 5SOS product 013-2-9."
  }
}
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."
  }
}

This OpenAPI document

GET/api/v1/openapi.json

The machine-readable contract. No key needed.

Access

No key needed.

Request
curl "https://admin.contractorindependence.com/api/v1/openapi.json"

The OpenAPI 3.1 document (not wrapped in the data envelope).