API Reference

Integrate ImpactCheck with your platform to programmatically manage facilitators, generate survey links, and retrieve results.

The API is currently in v1. All endpoints are stable and backwards-compatible within this version.

Authentication

All API requests require a bearer token in the Authorization header.

Authorization: Bearer YOUR_API_KEY

Generate your API key from Account Settings in the ImpactCheck dashboard. Keep it secret — treat it like a password.

Regenerating your API key invalidates the previous one immediately.

Base URL

https://impactcheck.net/api/v1

All endpoints return JSON. Send request bodies as JSON with Content-Type: application/json.

Survey Modes

Each survey is one of two modes. The mode is set when the creator builds the survey and locks once the first response is collected.

Anonymous

No respondent identity captured at all — no name, email, IP, tracking cookies, or fingerprints. Use for candor-sensitive workshops, classroom check-ins, or anywhere the consultancy explicitly promised anonymity. Respondent list/status reads require a Named survey (403 on Anonymous); lookup and invitation-create return 200 with a warning and attach no respondent. Webhooks never fire.

Named

Respondent name + email captured at invite time (via the API) or at submission time (via the form). Required when integrating with a coaching platform / Matchwell / custom client portal. Aggregate reporting is the default; individual rows are admin-only on the consultancy's side. Webhooks fire on each submission.

The endpoint reference below indicates which mode each endpoint applies to.

API Key Scopes

Each API key carries a scope that determines what it can read. Create keys from Settings → API Keys in your dashboard. Default for new keys is aggregate.

ScopeCan readCannot read
aggregate List surveys, filter dimensions, aggregate results, multi-survey aggregate, respondent completion status. Individual responses, respondent identity in filter dimensions.
admin Everything aggregate can read, plus individual responses on Named surveys (JSON or CSV), respondent details, and the invitation / facilitator / respondent lookup write endpoints.

Reads that return individual respondent identity on an admin-scope key are recorded in an internal audit trail.

Rate Limiting

Each API key gets a per-minute request budget. Default is 120 requests / minute; high-volume integrations can request a higher limit per key in the dashboard.

On overflow, we return 429 Too Many Requests with a Retry-After header (seconds).

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
Retry-After: 60

Conditional requests still count toward your rate-limit budget, but ETag caching avoids transferring and recomputing the payload.

Requests with a missing, malformed, or revoked key are separately rate-limited per IP and receive 429 when that limit is exceeded.

Caching (ETags)

The Results endpoint (GET /surveys/:id/results) returns a weak ETag derived from the survey's responses and its current question/metadata state, so renaming a survey or question also invalidates it. Send If-None-Match on subsequent requests; we'll return 304 Not Modified if nothing has changed.

curl -H "Authorization: Bearer YOUR_API_KEY" \
     -H 'If-None-Match: W/"v1:80:1715000000:abc123"' \
     https://impactcheck.net/api/v1/surveys/15/results

# HTTP/1.1 304 Not Modified  (no body; still counts toward rate limit budget)

Also sets Cache-Control: private, max-age=30 so an HTTP cache or CDN can serve the same response for 30 seconds. The ETag changes whenever a new response lands.

Error Handling

The API uses standard HTTP status codes. Error responses include a JSON body with an error field.

Status Meaning
200Success — resource already exists
201Created — new resource was created
401Unauthorized — missing or invalid API key
404Not found — survey, facilitator, or respondent doesn't exist (or belongs to another organization)
422Validation error — missing required fields (respondent list/status on an Anonymous survey returns 403, not 422)

Example error response:

{
  "error": "email is required"
}
GET

List Surveys

Endpoint

GET /api/v1/surveys

Returns all active surveys for the authenticated user. Use the id field from the response as the survey_id parameter in other endpoints.

Response

{
  "surveys": [
    {
      "id": 15,
      "name": "Leadership Accelerator 2025",
      "programme_name": "Leadership Accelerator",
      "survey_type": "programme_evaluation",
      "template_name": "Post-Programme",
      "retrospective": false,
      "slug": "k7m2p9x4q1",
      "response_count": 25,
      "published_at": "2026-02-14T10:30:00Z",
      "survey_url": "https://impactcheck.net/k7m2p9x4q1"
    }
  ]
}

Example Request

curl https://impactcheck.net/api/v1/surveys \
  -H "Authorization: Bearer YOUR_API_KEY"
POST

Facilitator Lookup

Endpoint

POST /api/v1/surveys/:survey_id/facilitators/lookup

Look up a facilitator by email. Creates the facilitator and/or assigns them to the survey if they don't exist yet. Returns the facilitator's unique survey link token. This endpoint is idempotent — calling it multiple times with the same email returns the same result.

URL Parameters

Parameter Type Description
survey_id integer The ID of the survey to assign the facilitator to. Must belong to the authenticated user.

Request Body

Field Type Required Default Description
email string required Facilitator's email address. Used as the unique lookup key within your account.
first_name string required* Facilitator's first name. Required when creating a new facilitator. Updates the value if it has changed.
last_name string optional "" Facilitator's last name. Optional. Updates the value if supplied and changed.
create_facilitator boolean optional true If the facilitator doesn't exist, create them. Set to false to return 404 instead.
assign_to_survey boolean optional true If the facilitator isn't assigned to this survey, assign them. Set to false to return 404 instead.

Response

Returns 201 if anything was newly created or assigned, 200 if everything already existed.

{
  "facilitator": {
    "id": 42,
    "first_name": "Jane",
    "last_name":  "Smith",
    "email": "jane@coaching.com"
  },
  "survey_facilitator": {
    "token": "a8f3x2m9"
  },
  "survey_url": "https://impactcheck.net/k7m2p9x4q1?f=a8f3x2m9",
  "created": false,
  "assigned": true
}
Field Description
facilitator The facilitator object (id, first_name, last_name, email)
survey_facilitator.token The opaque token for this facilitator's survey link
survey_url The full URL to send to participants — includes the ?f= token. Append &g=Cohort+3 to tag a group and &s=Day+1 to tag a session.
created true if the facilitator was newly created
assigned true if the facilitator was newly assigned to this survey

Example Request

curl -X POST https://impactcheck.net/api/v1/surveys/15/facilitators/lookup \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email":      "jane@coaching.com",
    "first_name": "Jane",
    "last_name":  "Smith"
  }'

Quickstart

1

Generate your API key

Go to Account Settings in the ImpactCheck dashboard and click "Generate API Key".

2

Create a survey

Create and publish a survey in the dashboard. Note its ID from the URL (e.g., /surveys/15).

3

Look up a facilitator

Call the Facilitator Lookup endpoint with the coach's email. You'll get back a unique survey URL.

4

Send the link to participants

Use the survey_url from the response. Responses submitted via this link are automatically attributed to the facilitator.

POST

Respondent Lookup

Named only

Endpoint

POST /api/v1/surveys/:survey_id/respondents/lookup

Look up a respondent by email. Creates the respondent record (organization-scoped, reusable across surveys) and assigns them to the survey if they aren't already. Returns the survey_respondent.token — the per-invite hash that identifies this person on this survey. Idempotent — same (survey, email) returns the same token. Use this when you want to manage URL composition yourself; otherwise prefer the Invitation endpoint below.

URL Parameters

ParameterTypeDescription
survey_id integer Must belong to your organization. On an Anonymous survey the call no-ops gracefully (see note below) rather than erroring.

Body Parameters

ParameterTypeDescription
emailstringRequired. Respondent's email address. Case-insensitive.
first_namestringRequired. Respondent's first name. Updated on subsequent calls if changed.
last_namestringOptional, defaults to empty string. Respondent's last name.

Response

{
  "anonymity":         "named",
  "respondent":        { "id": 12, "first_name": "Alice", "last_name": "Smith", "email": "alice@example.com" },
  "survey_respondent": { "token": "kx9p2m...", "completed_at": null },
  "created":  true,
  "assigned": true,
  "warnings": []
}

Returns 200 on the second+ call (already exists, no creation) and 201 on first creation.

Anonymous surveys degrade gracefully. You don't need to know the mode before calling. On an Anonymous survey this no-ops (no respondent attached, no token) and returns 200 with "anonymity": "anonymous", respondent: null, and a warnings entry — never a 422. anonymity is always present so you can learn the mode from the response.

Example Request

curl -X POST https://impactcheck.net/api/v1/surveys/15/respondents/lookup \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "email": "alice@example.com", "first_name": "Alice", "last_name": "Smith" }'
POST

Invitation URL (meta)

Endpoint

POST /api/v1/surveys/:survey_id/invitations

Convenience endpoint that does find-or-create for both respondent and facilitator and returns a fully composed survey URL. Each input is independent — send only what you have. Use this when you want a single API call to produce a ready-to-send link.

Body Parameters

ParameterTypeDescription
respondentobjectOptional. {email, first_name, last_name?}. Identifies the respondent on Named surveys. On Anonymous surveys it's dropped (with a warning) rather than rejected — see the note below. last_name is optional.
facilitatorobjectOptional. {email, first_name, last_name?}. Find-or-create.
group_identifierstringOptional. Free-form cohort/programme tag. Pass-through to the response.
session_identifierstringOptional. Free-form session/event tag. Pass-through to the response.

Response

{
  "survey_url": "https://impactcheck.net/abc1234567?r=kx9p2m...&f=sf123456&g=Cohort+A&s=Session+3",
  "anonymity":  "named",
  "respondent":  { "id": 12, "first_name": "Alice", "last_name": "Smith", "email": "alice@example.com", "token": "kx9p2m..." },
  "facilitator": { "id": 7,  "first_name": "Sam",   "last_name": "Coach", "email": "coach@example.com",  "token": "sf123456" },
  "warnings": []
}

The composed URL only includes query params for inputs that were populated — no empty ?g=&s= clutter.

Send a uniform payload — anonymity is handled for you. You don't need to know a survey's mode before calling. On an Anonymous survey, a supplied respondent is dropped (never attached) and the response comes back 201 with "anonymity": "anonymous", respondent: null, the ?f=&g=&s= link intact, and a warnings entry explaining the drop. anonymity is always present and warnings is always an array (empty when nothing was dropped). Facilitator, group, and session work on Anonymous surveys — only per-person respondent identity is omitted.

Example Request

curl -X POST https://impactcheck.net/api/v1/surveys/15/invitations \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "respondent":         { "email": "alice@example.com", "first_name": "Alice", "last_name": "Smith" },
    "facilitator":        { "email": "coach@example.com", "first_name": "Sam",   "last_name": "Coach" },
    "group_identifier":   "Cohort A",
    "session_identifier": "Session 3"
  }'
GET

Filter Dimensions

aggregate scope

Endpoint

GET /api/v1/surveys/:survey_id/filter_dimensions

Returns the set of filter values consumers can use when fetching results — facilitators, groups, sessions, plus respondents on admin-scope keys (Named surveys only). Designed so you can render a complete filter UI in a single API call.

To protect respondent anonymity, any facilitator, group, or session with fewer than 3 responses is omitted from this list (and its count is never revealed). A bucket appears once it reaches 3.

Response

{
  "facilitators": [
    { "token": "abc12345", "first_name": "Sam", "last_name": "Coach", "response_count": 18 }
  ],
  "groups":   [{ "value": "Cohort A", "response_count": 14 }],
  "sessions": [{ "value": "Session 1", "response_count": 30 }],
  "total_responses": 30,
  "has_unattributed": true,
  "respondents": [    // admin scope + Named survey only
    {
      "id": 12, "first_name": "Alice", "last_name": "Smith", "email": "alice@x.com",
      "invitations": [
        { "token": "kx9p2m..", "completed_at": "2026-05-04T10:00:00Z" },
        { "token": "qz4n7r..", "completed_at": null }
      ]
    }
  ]
}
GET

Results (aggregate)

aggregate scope ETag-cached

Endpoint

GET /api/v1/surveys/:survey_id/results

Per-question rollup — same data the IC dashboard uses. Returns a distribution per scale question, mean, retrospective deltas where applicable, response counts, and a date-bucketed trend if the survey has been live more than 7 days.

Query Parameters (all optional, all combine)

ParameterDescription
facilitatorToken from filter_dimensions (the opaque token, not a numeric id), or unattributed.
groupGroup identifier, or none.
sessionSession identifier, or none.
respondentRespondent token. Admin scope only.

Response

HTTP/1.1 200 OK
ETag: W/"v1:30:1715000000:abc123"
Cache-Control: private, max-age=30

{
  "survey":          { "id": 15, "name": "...", "anonymity": "named", "response_count": 30, ... },
  "total_responses": 30,
  "suppressed": false,
  "min_display_responses": 3,
  "question_results": [
    { "question": {...}, "response_count": 30, "distribution": {"1": 0, "2": 1, ...}, "mean": 4.2 }
  ],
  "responses_over_time": [{ "date": "2026-04-30", "count": 6 }, ...],
  "filters": { "facilitator": "abc12345" }
}
Anonymity threshold. When the matched response set (after filters) has between 1 and min_display_responses − 1 responses, suppressed is true and question_results / responses_over_time come back empty — total_responses is still reported. Treat a suppressed payload as "not enough responses to display." Zero responses returns suppressed: false with empty results.
GET

Individual Responses

admin scope Named only

Endpoint

GET /api/v1/surveys/:survey_id/responses
GET /api/v1/surveys/:survey_id/responses.csv

Paginated individual response rows with respondent identity. Default 50 per page, max 200. Append .csv (or send Accept: text/csv) for the same data formatted as CSV — column-for-column matching the dashboard's CSV export. Returns 403 on Anonymous surveys.

Query Parameters

ParameterDescription
pagePage number (1-indexed). Default 1.
per_pagePage size. Default 50, max 200.
facilitator / group / session / respondentSame filters as the aggregate endpoint.

JSON Response

{
  "data": [
    {
      "id": 1234,
      "submitted_at": "2026-05-04T10:00:00Z",
      "respondent":  { "first_name": "Alice", "last_name": "Smith", "email": "alice@example.com" },
      "facilitator": { "first_name": "Sam",   "last_name": "Coach", "email": "coach@example.com" },
      "group_identifier": "Cohort A",
      "session_identifier": "Session 1",
      "answers": [
        { "question_id": 23, "question_type": "scale", "value": "4", "value_before": "2" }
      ]
    }
  ],
  "pagination": { "page": 1, "per_page": 50, "total": 30, "next_page": null }
}
GET

Respondents (list)

admin scope Named only

Endpoint

GET /api/v1/surveys/:survey_id/respondents

Completion-status list — one row per person, with each invitation they've received nested under invitations. A respondent can be invited multiple times (e.g. after session 1 and again after session 5), so each invitation has its own token and completed_at.

Response

{
  "data": [
    {
      "id": 12, "first_name": "Alice", "last_name": "Smith", "email": "alice@x.com",
      "invitations": [
        { "token": "kx9p2m..", "completed_at": "2026-05-04T10:00:00Z" },
        { "token": "qz4n7r..", "completed_at": null }
      ]
    },
    {
      "id": 13, "first_name": "Bob", "last_name": "Jones", "email": "bob@x.com",
      "invitations": [
        { "token": "by7n3z..", "completed_at": null }
      ]
    }
  ]
}
GET

Respondent Status (bulk)

aggregate scope Named only

Endpoint

GET /api/v1/surveys/:survey_id/respondents/status?tokens[]=t1&tokens[]=t2

Bulk completion-check for tokens you already hold. Designed for the "did this batch of invitees finish?" use case without requiring the admin scope (no PII in the response). Up to 200 tokens per request.

Response

{
  "data": [
    { "token": "kx9p2m..", "found": true,  "completed": true,  "completed_at": "2026-05-04T10:00:00Z" },
    { "token": "by7n3z..", "found": true,  "completed": false, "completed_at": null },
    { "token": "missing.", "found": false, "completed": false, "completed_at": null }
  ]
}

Each token in the request is echoed in the response in the same order, so you can map results 1:1 to your input list. Duplicate tokens are each echoed; blank tokens are returned with found: false. Only a completely empty tokens[] is rejected (422). Unknown tokens return found: false rather than 404.

POST

Respondent Erasure

admin scope

Endpoint

POST /api/v1/respondents/erase

GDPR right-to-erasure. Anonymises the respondent matching email across your whole organisation: name and email are removed permanently, but their submitted responses remain so aggregate results stay intact. Org-level (not survey-scoped). Erasing the same email a second time returns 404 (the person is no longer identifiable in your organisation).

Request body

{ "email": "alice@example.com" }

Response

{
  "erased": true,
  "already_erased": false,
  "respondent_id": 42,
  "invitation_count": 3
}

Returns 404 when no respondent in your organisation matches the email, 422 when email is missing. This endpoint works even when your plan has lapsed — erasure is a legal obligation, so it's never gated.

Erasure cannot be undone. If the same person is later re-registered via Respondent Lookup, they get a fresh respondent record with no link to their erased history.
GET

Multi-survey Aggregate

aggregate scope

Endpoint

GET /api/v1/surveys/aggregate?ids[]=1&ids[]=2&strategy=union

Cross-survey roll-up for "compare cohort 1 vs cohort 2" reporting. Two strategies:

  • union (default) — pool all responses across the listed surveys into one combined result. Surveys must share question structure — position, type, resolved scale points, choice options, and retrospective mode must all match; otherwise the request is rejected (422).
  • compare — return parallel result objects, one per survey, for side-by-side rendering.

Response (compare)

{
  "strategy": "compare",
  "min_display_responses": 3,
  "surveys": [
    { "survey": { "id": 9,  "name": "Cohort A", "response_count": 30 }, "suppressed": false, "question_results": [...], "total_responses": 30 },
    { "survey": { "id": 10, "name": "Cohort B", "response_count": 2  }, "suppressed": true,  "question_results": [],    "total_responses": 2  }
  ]
}

Returns 422 if surveys diverge structurally and you ask for union, or if you pass more than 100 survey IDs in one request. 404 if any of the listed survey IDs belongs to another organisation.

The same anonymity threshold applies. In compare, each survey carries its own suppressed flag. In union, the pooled set is suppressed ("suppressed": true, empty question_results) when the combined total is between 1 and min_display_responses − 1.
EVENT

Response Webhook

Named only

Set webhook_url on a Named survey (in the creator UI), and ImpactCheck will POST a signed event to that URL each time a response lands. The signature scheme is Stripe-compatible — existing webhook libraries (Ruby, Node, Python, Go, etc.) decode it without modification.

Headers

Content-Type: application/json
X-ImpactCheck-Signature: t=<unix_timestamp>,v1=<HMAC-SHA256 hex>
Idempotency-Key: evt_<response_id>
User-Agent: ImpactCheck-Webhook/1.0

The HMAC is computed over "<timestamp>.<raw_body>" using your survey's signing secret (shown once in the creator UI, prefixed whsec_). The Idempotency-Key (also the body's id) is stable across retries — dedupe on it.

Body

{
  "id": "evt_1234",
  "event": "response.created",
  "survey":      { "id": 9, "slug": "abc1234567", "name": "Programme Eval" },
  "respondent":  { "token": "kx9p2m...", "email": "alice@example.com", "first_name": "Alice", "last_name": "Smith" },
  "response":    {
    "id": 1234,
    "submitted_at": "2026-05-07T14:30:00Z",
    "group_identifier":   "Cohort A",
    "session_identifier": "Session 3"
  },
  "facilitator": { "token": "sf123456", "first_name": "Sam", "last_name": "Coach", "email": "coach@example.com" }
}

Reliability

  • Asynchronous — submission flow never waits on consumer availability.
  • Retry on non-2xx response or network error: 3 retries at 1m, 5m, 25m. Retries reuse the same id / Idempotency-Key, so dedupe on it to handle the rare double-delivery.
  • Anonymous surveys never fire webhooks.
  • Every attempt (succeeded or failed) is logged in your survey's webhook activity log.

Verifying signatures

  1. Split the X-ImpactCheck-Signature header on ,, parse t=<timestamp> and v1=<hex>.
  2. Reject events older than 5 minutes (|now - t| > 300) to prevent replay.
  3. Compute HMAC-SHA256(secret, "<t>.<raw_body>") and constant-time-compare against v1.