API version: v1
The InsideLoop API gives you programmatic, read-only access to your workspace’s data — survey responses, technician insights, surveys, and your synced client/technician roster — to pull into BI tools, reporting pipelines, or your own dashboards. All endpoints live under /api/v1 on the same domain you use for the app, return JSON, and require an API key.
Create an API key under Settings → API keys. The full secret (it starts with sl_live_) is shown exactly once at creation — InsideLoop stores only a hash, so it can never be displayed again. Send it on every request as a bearer token:
Authorization: Bearer sl_live_...GET /api/v1/me (below) shows the full list for a given key.The public API cannot create, modify, or delete anything in your workspace. Every endpoint is a GET; other HTTP methods return 405. Writes only happen through the signed-in app.
Each key may make 60 requests per minute. Beyond that, requests return 429 with a Retry-After header (seconds until the window resets). Back off and retry rather than hammering — for bulk pulls, page with limit and offset at a steady pace.
Failures return a consistent JSON body with a machine-readable code:
{
"error": {
"code": "insufficient_scope",
"message": "This API key is missing the \"read:responses\" scope."
}
}| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_parameter | A query parameter is malformed or out of range. |
| 401 | unauthorized | Missing, malformed, unknown, revoked, or expired API key. |
| 403 | insufficient_scope | The key is valid but lacks the scope this endpoint needs. |
| 429 | rate_limited | Over 60 requests/minute on this key. Honor Retry-After. |
days + offset is fine for one-off pulls, but it’s the wrong tool for a recurring sync job: as new responses arrive, later pages shift underneath you, so an offset-based poll can silently skip or double-count rows. The responses and tech-insights endpoints instead support a since parameter (an ISO 8601 timestamp) built for polling:
since and the endpoint switches from its default newest-first order to oldest-first, returning only rows strictly after that timestamp.received_at (or created_at) of the last row in the response and use it as since on your next call — that row is never returned again, so nothing repeats.limit, you’re caught up; wait and poll again with the same since.since with offset or days — the moving cursor already tells the endpoint where to resume.First call — everything since midnight:
curl -H "Authorization: Bearer sl_live_..." \
"https://app.example.com/api/v1/responses?since=2026-07-01T00:00:00Z&limit=100"Say the last row returned had "received_at": "2026-07-02T18:24:11.000Z". The next poll picks up right after it:
curl -H "Authorization: Bearer sl_live_..." \
"https://app.example.com/api/v1/responses?since=2026-07-02T18:24:11.000Z&limit=100"Key introspection: returns the workspace and key metadata. Works with any valid key (no scope required) — use it to verify a new key or debug a failing integration.
curl -H "Authorization: Bearer sl_live_..." \
https://app.example.com/api/v1/meResponse:
{
"workspace": { "id": "org_...", "name": "Your MSP" },
"key": {
"id": "ak_...",
"name": "PowerBI export",
"prefix": "sl_live_h2K9cPq7",
"scopes": [
"read:responses",
"read:tech_insights",
"read:surveys",
"read:clients",
"read:techs"
],
"created_at": "2026-07-03T00:15:45.000Z",
"expires_at": null
}
}Lists survey responses. Requires the read:responses scope.
| Parameter | Type | Description |
|---|---|---|
days | integer | Look-back window in days, 1–365. Default 30. Ignored when since is set. |
since | string | ISO 8601 timestamp; return only responses received after it, oldest first. See “Incremental sync” above. Must be within the last 365 days. |
sentiment | string | Filter by sentiment: pos, neu, or neg. |
survey_id | string | Only responses from this survey. |
client_id | string | Only responses from this client company. |
tech_id | string | Only responses attributed to this technician. |
limit | integer | Page size, 1–200. Default 50. |
offset | integer | Rows to skip for pagination. Default 0. Not meaningful with since — see “Incremental sync” above. |
curl -H "Authorization: Bearer sl_live_..." \
"https://app.example.com/api/v1/responses?days=30&sentiment=neg&limit=50"Response:
{
"data": [
{
"id": "resp_...",
"received_at": "2026-07-02T18:24:11.000Z",
"sentiment": "neg",
"score": 1,
"ticket_id": "T20260702.0042",
"subject": "VPN keeps dropping",
"comment": "Still not fixed after two calls.",
"contact": "jane@client.com",
"alert_sent": true,
"client": { "id": "client_...", "name": "Acme Corp" },
"tech": { "id": "tech_...", "name": "Sam Rivera" },
"survey": { "id": "survey_...", "name": "Ticket CSAT", "type": "smiley" }
}
],
"pagination": { "limit": 50, "offset": 0, "returned": 1, "total": 1 }
}pagination.total is the number of rows matching the filters; keep requesting with offset += limit until returned is less than limit. tech and survey can be null (e.g. relationship/NPS surveys aren’t attributed to a technician), and ticket_id can be null for responses not tied to a ticket.
Lists technician-submitted insights — concerns, opportunities, and other front-line feedback raised from inside a ticket. This is a separate stream from customer CSAT responses. Requires the read:tech_insights scope.
| Parameter | Type | Description |
|---|---|---|
days | integer | Look-back window in days, 1–365. Default 30. Ignored when since is set. |
since | string | ISO 8601 timestamp; return only insights created after it, oldest first. See “Incremental sync” above. Must be within the last 365 days. |
status | string | Filter by status: open, acknowledged, or resolved. |
category | string | Filter by category slug. Categories are configurable per workspace — see Settings. |
client_id | string | Only insights raised about this client company. |
tech_id | string | Only insights raised by this technician. |
limit | integer | Page size, 1–200. Default 50. |
offset | integer | Rows to skip for pagination. Default 0. Not meaningful with since. |
curl -H "Authorization: Bearer sl_live_..." \
"https://app.example.com/api/v1/tech-insights?days=30&status=open&limit=50"Response:
{
"data": [
{
"id": "ti_...",
"created_at": "2026-07-10T14:02:00.000Z",
"updated_at": "2026-07-10T14:02:00.000Z",
"category": "opportunity",
"note": "Client's switches are EOL, worth a refresh quote.",
"status": "open",
"ticket_id": "T20260710.0019",
"client": { "id": "client_...", "name": "Acme Corp" },
"tech": { "id": "tech_...", "name": "Sam Rivera" },
"autotask_ticket": { "id": "12345", "number": "T20260710.0030", "status": "New" }
}
],
"pagination": { "limit": 50, "offset": 0, "returned": 1, "total": 1 }
}client and tech can be null when the insight wasn’t attributed to one. autotask_ticket is null until the insight is converted into a ticket.
Lists every survey in the workspace. Requires the read:surveys scope. Not paginated.
curl -H "Authorization: Bearer sl_live_..." \
https://app.example.com/api/v1/surveysResponse:
{
"data": [
{
"id": "survey_...",
"name": "Ticket CSAT",
"type": "smiley",
"status": "active",
"delivery_mode": "embed",
"question": "How did we do on this ticket?",
"description": "",
"sent": 812,
"responded": 340,
"csat": 94,
"created_at": "2026-01-04T16:00:00.000Z",
"updated_at": "2026-06-30T09:12:00.000Z"
}
]
}csat is null until the survey has at least one response. Use a survey’s id as survey_id on /api/v1/responses.
Lists every client company synced from your PSA. Requires the read:clients scope. Not paginated.
curl -H "Authorization: Bearer sl_live_..." \
https://app.example.com/api/v1/clientsResponse:
{
"data": [
{
"id": "client_...",
"name": "Acme Corp",
"external_id": "29384",
"source": "autotask"
}
]
}external_id is the company’s id in your PSA (e.g. an Autotask company id), or null if this client was created manually rather than synced. Use a client’s id as client_id on other endpoints.
Lists every technician synced from your PSA. Requires the read:techs scope. Not paginated.
curl -H "Authorization: Bearer sl_live_..." \
https://app.example.com/api/v1/techsResponse:
{
"data": [
{
"id": "tech_...",
"name": "Sam Rivera",
"email": "sam@yourmsp.com",
"external_id": "184",
"source": "autotask"
}
]
}external_id is the technician’s resource id in your PSA, or null if this technician was created manually rather than synced. Use a technician’s id as tech_id on other endpoints.
The API is versioned in the path (/api/v1). Within v1 we may add new endpoints and new response fields, but we won’t remove or rename existing fields or change their types — build your integration to ignore fields it doesn’t recognize. Breaking changes would ship as a new version path.
Need an endpoint that isn’t here yet, or hitting something unexpected? Contact support@insideloop.app.