Skip to content
Knowledge ERP Docs

API Reference ↗
Surveys

Feedback Surveys

Send feedback surveys to named people or by public link, calculate NPS, automate reminders, and get alerted on low scores.

A feedback survey asks how something went, after the fact. It is not the pre-visit form, which collects what you need before a customer arrives — the two live in different places in the navigation and say which end of the visit they are for.

Feedback surveys are under Feedback surveys in the navigation.

In this section #

Detailed guides for everything covered here:

  • Survey Responses — One respondent's completed submission to a survey — the set of answers they gave to each question, tied back to the survey and, when there is one, the invitation that brought them in.

Building and sending #

Create a survey with custom question types, then deliver it via survey invitations. Invitations support email customization (subject, intro), a custom thank-you message, and an anonymous open link for public collection.

Sending one to people you name #

There are three places to do it, and they all raise the same invitations and send the same email. All three need Add a new Feedback Survey Invitation — the permission that used to grant nothing, because there was no way to make one — and all three do the work in the background, so sending to two hundred people is a button press rather than a page that hangs.

  • From the survey. Open the survey and press Send. Pick customers from the list, type in any other addresses, and it goes.
  • From the customers list. Tick the people you want and choose Send survey from the bulk actions — "I am looking at these people and want to survey them". Anyone with no email address on file is skipped and the confirmation says how many.
  • From Feedback surveys → Invitations. The same Send button, with a survey to pick.

Each person gets their own single-use link, and the invitation is tied to the customer it went to, so the answers come back attributed rather than anonymous. One address is invited once however many times it appears in the selection.

Sending one about a particular job #

A Send Feedback Survey button also sits in the header of every record a survey can be tied back to — a sales order, a customer invoice, an appointment and a unit checkout. Pick the survey, confirm the email (pre-filled from the record's customer or booking contact) and it goes out immediately. The invitation is linked back to the record it was sent from, so responses stay traceable to the job that prompted them. The button appears only for accounts on the Surveys module.

Question types #

Twelve, and the builder explains each one as you pick it:

Type For
Multiple choice (single / multi) Pick one, or pick any number
Dropdown One from a longer list — radio buttons stop scanning well past about seven
Yes / No A straight yes or no
Star rating One to five stars
Scale A numbered scale — 1–5 for satisfaction, 1–7 for effort, 0–10 for a pain rating. Set both ends, and name them
NPS (0–10) The standard recommendation question. Drives the NPS score
Ranking Put the options in order. Reported as an average position, best first
Matrix / Grid Rate several things on the same scale. The choices are the rows; you write the columns, and give each column a value to get a mean per row
Date A calendar date
Short / Long text A line, or a paragraph

A ranking is answered by numbering the options rather than dragging them, so it works with a keyboard, with a screen reader, on a phone, and with scripting off.

Setting a scale's range #

A Scale question has a bottom as well as a top, and both ends can be labelled. Leave the bottom at 1 for satisfaction (1–5) or effort (1–7); set it to 0 for a Numeric Pain Rating Scale, where 0 genuinely means no pain and faking that in helper text is not the validated instrument.

Rate your pain right now — bottom 0, top 10, labelled No pain and Worst imaginable. The form shows eleven buttons with those words underneath.

An answer is checked against that range on the way in, wherever it comes from — the form, the anonymous link, the autosave, the CSV import, or anything talking to the database directly. A score that is not on the scale is refused and nothing is written, so a distribution can never contain a value nobody could have given.

Building a matrix #

A matrix's rows are the things being rated and its columns are the ratings on offer, and the builder now keeps the two together. Give each column a value — Poor 1, Fair 2, Good 3, Excellent 4 — and the results page reports a mean per row beside the distribution, which is the number worth tracking month to month. Leave the values blank when the columns are not a scale (Yes / No / N/A) and no mean is shown, rather than a meaningless one.

Matrices built before this existed keep their rows exactly as they were, and their columns are given their own position as a value (the first column 1, the second 2, and so on) so they can be averaged straight away. Clear those values in the builder if the columns are not ordinal.

Checking a survey before you send it #

Open a survey and press Preview. It renders the questionnaire exactly as a respondent will meet it, through the same code the public pages use, and records nothing: no invitation is marked opened, no response is created, no cap is spent. Questions that only appear once an earlier answer triggers them are shown with the rule that reveals them, because a hidden branch is the part nobody ever checks.

The survey's View page also spells out the answers on offer for every question — the choices, a matrix's rows and columns with their values, a scale's range and its anchors — so the questionnaire is reviewable without opening the editor.

Only asking what's relevant #

Every question can carry two independent rules, both set from dropdowns as you build the survey — there is nothing to type and no need to save first.

Show only when hides a question until an earlier answer calls for it. Pick the earlier question, pick the comparison — is answered, is not answered, is, is not — and, for the last two, pick the answer from a dropdown of that question's own options. So "What went wrong?" appears only after someone says they had a problem, and the people who didn't never see it.

Require an answer only when does the same for compulsory answers: "Tell us why" can be optional in general but required from anyone who left one star. Setting it replaces the plain Required toggle, because the rule now decides.

A few rules keep a survey coherent:

  • A question can only depend on one above it, so the dropdown offers earlier questions only. That also makes it impossible for two questions to depend on each other.
  • Move a question above something it depends on and the survey won't save until you either move it back or clear the rule. Nothing is changed silently.
  • A question other questions depend on can't be deleted until those rules are cleared, so you never end up with a branch pointing at nothing.
  • Questions answered with free text offer is answered and is not answered only. Matching typed text exactly would almost never fire.

The rules are applied again when the response arrives, not just in the browser: a question that was hidden is never stored and never blocks the submission, and a conditionally required question is genuinely enforced once its condition is met.

What the respondent sees #

The survey page belongs to your business, not to us — your name, logo and contact details are on it, and there is no route from it into the Knowledge ERP sign-in.

  • It names the record it's about. An invitation sent from a sales order, appointment or unit checkout shows that record at the top of the page: the number, the date, and what was on it. "Your recent order" is unanswerable for a customer who places several a month, so the page says which one. Only the single record the invitation link authorises is ever shown, and never any pricing or internal status. An open-link survey has no source record and shows no panel.
  • Questions that aren't required are marked Optional. A required question keeps its red asterisk. A question that is required only when its condition fires switches from Optional to the asterisk at the moment it does, so the page never invites someone to skip an answer the survey will then refuse.
  • Questions that don't apply aren't there. A question whose display rule isn't met is hidden as the respondent works down the page, and doesn't appear on the receipt either — it was never asked.
  • The thank-you page is a receipt. It repeats the answers just given, the record they were about, the time, and a reference to quote if the customer gets in touch. It is shown once, straight after submitting.

Who can answer, and how often #

An invitation is single-use. The link emailed to a named person opens once, records one response, and is spent — reloading it, or forwarding the email to a colleague, gets a "this link has already been used" page rather than a second response. A link that does not match its invitation is refused outright. Nothing below changes that; it was always true.

The anonymous open link is the one anyone can answer, so it has its own controls, all of them off by default:

  • Stop accepting answers on — a closing date and time. Afterwards the link says the survey closed and on what date, rather than 404ing: a dead-looking link gets reported as broken and somebody spends an afternoon on it.
  • Maximum responses — the link closes once that many are in.
  • One response per browser — on by default. It stops the ordinary ballot-stuffing case: the same person answering three times, or a page left open and submitted twice. Turn it off for a shared device — a tablet on a reception counter would otherwise take one answer and refuse everybody after. It is a cookie, so treat it as a deterrent rather than a guarantee.

Two more things guard the open link without any setup: a hidden trap field that only automated form-fillers complete (a filled one is silently ignored and answered with what looks like success), and a per-network rate limit on submissions, so a scripted flood is stopped in a few seconds rather than in your NPS score.

An open-link survey that has closed or filled up can still be answered by somebody who was invited — the cap and the closing date are about the public link, not about a person you asked individually.

Nobody loses a half-filled survey #

A response is saved as the respondent works, not only when they press Submit. If they close the tab, run out of battery, or put it down and come back tomorrow, their link reopens with every answer they had already given still in place.

  • It saves quietly. A line under the form says the answers are safe once they are. Nothing is required of the respondent, and with scripting turned off the survey behaves exactly as it used to.
  • A draft is not a response. It never reaches an NPS score, a response rate, the responses list, or the API, and it never raises a low-score alert — half of a bad score is not a bad score. Only submitting does any of that.
  • Submitting completes the draft rather than leaving a phantom abandonment next to the real response.
  • Drafts are how you find out where a survey loses people — see below.

Resuming needs to know who is answering, so it works on invitation links. The anonymous open link has nobody to resume as.

An anonymous response is not an invitation. Somebody who answers the public link was never invited, so no invitation is created for them and the response rate — invitations sent against invitations answered — keeps meaning what it says. If they leave an email address it is kept on the response itself, and the responses list shows it; if they do not, the response reads Anonymous.

Reading the results #

Reports → Feedback survey results. Pick a survey and a date range and the page shows:

  • The summary — responses in the period, how many people started and did not finish, the invitation open rate, and the NPS score with its promoter and detractor counts.
  • Every question, broken down. Choice questions show each option with its count and share — including options nobody picked, which is a finding that disappears if you only count the answers that came back. Star, scale and NPS questions show their average and their full distribution — a 0–10 scale counts from its own 0, not from 1 — and NPS also splits into promoters, passives and detractors. A scored matrix shows a mean per row beside the grid. Free-text questions list the answers in one place, so you can read them without going along a wide table one cell at a time.
  • Where people stop. Built from the responses that were started and never submitted, and attributed to the question they did not answer — that is the one worth rewriting or deleting. Somebody who opened the survey and answered nothing counts against the first question.
  • Responses over time, and the response table underneath, 25 to a page, newest first — the summary above still counts every response in the period, whichever page you are looking at.

Filtering by an answer. Choose a question and one of its answers and every number on the page is recomputed for the people who gave it — "what do the detractors say about delivery?" is one dropdown, not an export. Every question with a fixed set of answers can be filtered on, free text aside, because matching typed prose exactly would almost never hit.

A matrix and a ranking are filtered on a pair, because one answer to either is a pair: Explaining what was wrong — Poor, or Cost ranked 1. Picking the row and the rating together is what makes "what do the people who rated the explanation poorly write in the free text?" a single dropdown.

Everything on the page exports to CSV from the button in the header.

Survey history brought across from an older system. Some older systems kept no date on survey invitations or responses. Where an imported response has no date of its own, it is dated from its invitation, and where the invitation has none either, from the order or loan the survey was sent about — the order's sale date, or the date the order was entered where that is blank. An invitation with no sent date is dated from that order the same way. The period you pick on this page, the Responses list and the API all use that date. An imported response with no date anywhere, and no order to take one from, is not shown on this page or on the Responses list.

Two of the summary figures — started, not finished and the invitation open rate — only ever count invitations and responses this app sent itself. An old emailed invitation can still be opened, and a start left mid-answer still autosaved, after import — but counting either would move a rate meant to describe this app's own tracking, so both are left out. A period covering only imported history shows Not available rather than a number for both; a period covering both kinds shows the figure for what this app tracked, with a note naming the date that tracking began.

Survey data in the Report Builder #

Survey answers are a Report Builder data source, so they can be combined with everything else, saved as a report definition, shared, and put on a schedule — rather than exported and pivoted by hand every month.

The grain is one row per answer, with the survey, the question, the question type, the chosen option, the score, the written answer, the respondent and the submission date. Group by Question and average Score and you have the month's outcome table in two clicks. Submitted responses only: a half-filled form somebody walked away from would otherwise move a number you report to a funder.

Reminders and alerts #

Automated reminder emails nudge non-responders after a configurable delay, and low-score alerts email you when a rating crosses a threshold so you can follow up while it still matters. Invitations expire on a schedule.

No reminder is sent for an invitation brought across from an older system, even after you Resend it. Those invitations were sent by the older system, not by this app, so a reminder set up on an imported survey applies only to invitations sent from here. Resend still sends the invitation itself.

Thresholds are set per question, with a direction. A survey-wide threshold is still there as the fallback and every survey already configured keeps alerting on exactly what it alerted on before. But one number read as "low is bad" cannot be right for a 1–5 star question and a 0–10 NPS question in the same survey — and it cannot express a pain scale at all, where a 9 is the score somebody has to see today. On a scale, star or NPS question, set Alert when the score reaches and choose whether the alert fires at or below that value (satisfaction) or at or above it (pain, effort, risk).

Importing results #

Already have survey data? The internal admin tools include a CSV import with column mapping for bulk-loading historical results. Ratings are checked against their question's scale as they load: a value that is not on the scale stops the import and nothing is written, rather than leaving half a history behind.

Automating surveys #

Surveys can be sent without anyone clicking a button:

  • Triggers — a survey trigger auto-sends a survey a set number of hours after an event completes (with an optional reminder), evaluated by an hourly scheduled command. The events are:

    Source Fires when
    Sales Order The order ships or is delivered
    Appointment The appointment is marked completed
    Unit Checkout A checked-out unit is checked back in
    Rental Returned Every unit on a rental agreement is back
    Loan Returned Every unit on a loan agreement is back

    Send After (hours) may be 0, which sends the survey on the next run of the scheduled command rather than waiting — use it when the ask should land while the job is still fresh.

    Only for these appointment types narrows an appointment trigger. Leave it blank — the default, and what every trigger written before this had — and the survey goes out after every kind of appointment. Pick the types that end something and a discharge-outcomes survey stops going out after a patient's second session of twelve. The other sources have no equivalent: a sales order, a unit checkout, a rental and a loan carry no type to narrow by.

    You can see what is queued. Each trigger on the survey's page says how many invitations are waiting and when the next one goes — "3 invitations scheduled, next at 14:00" — so arming one is not a matter of waiting a day and asking a customer whether anything happened. A trigger that is switched off says so and counts nothing.

    Marking an appointment completed says what it will send. The confirmation on Mark Completed names the survey, the address and the delay whenever a trigger is armed for that appointment.

    Rental and loan surveys wait for the whole agreement to come back. Bring half the kit in and the customer hears nothing; the survey goes out once, when the last unit is checked in, so a job returned in three trips is still one email. You only see the sources for modules you subscribe to — Rental Returned needs the Rentals module, Loan Returned and Unit Checkout need Inventory.

  • Automation builder — the Send Survey action in the workflow builder sends a survey from any rule, with the recipient email interpolated from the trigger context.

  • Webhooks — subscribe to survey.completed, survey.low_score, and questionnaire.completed to react to responses in real time. See Webhooks.

Doing it from the API #

The whole lifecycle is scriptable — build a survey, add questions and choices, then send and track invitations.

# Create a survey
curl -X POST "https://your-domain.com/api/v1/surveys" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name": "Post-purchase", "is_active": true, "low_score_threshold": 6}'

# Add an NPS question to it
curl -X POST "https://your-domain.com/api/v1/survey-questions" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"survey_id": "<survey-id>", "type": "nps", "question_text": "How likely are you to recommend us?"}'

# Add a 0-10 pain rating scale, alerting when a score is 7 or higher
curl -X POST "https://your-domain.com/api/v1/survey-questions" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"survey_id": "<survey-id>", "type": "scale", "question_text": "Rate your pain right now",
       "scale_min": 0, "scale_max": 10,
       "scale_min_label": "No pain", "scale_max_label": "Worst imaginable",
       "alert_threshold": 7, "alert_direction": "at_or_above"}'

# Add a scored matrix — the rows are its choices, the columns carry the scale
curl -X POST "https://your-domain.com/api/v1/survey-questions" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"survey_id": "<survey-id>", "type": "matrix", "question_text": "Rate each of these",
       "matrix_columns": ["Poor", "Fair", "Good", "Excellent"],
       "matrix_column_values": {"Poor": 1, "Fair": 2, "Good": 3, "Excellent": 4}}'

# Send an invitation (queues the email; track status on the returned record)
curl -X POST "https://your-domain.com/api/v1/survey-invitations" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"survey_id": "<survey-id>", "email": "customer@example.com"}'

# Auto-send 24h after a discharge appointment only
curl -X POST "https://your-domain.com/api/v1/survey-triggers" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"survey_id": "<survey-id>", "source_type": "appointment", "delay_hours": 24, "appointment_type_ids": ["<appointment-type-id>"]}'

# Auto-send 24h after a sales order ships
curl -X POST "https://your-domain.com/api/v1/survey-triggers" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"survey_id": "<survey-id>", "source_type": "sales_order", "delay_hours": 24}'

# Auto-send 48h after a rental comes back in full
curl -X POST "https://your-domain.com/api/v1/survey-triggers" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"survey_id": "<survey-id>", "source_type": "rental_return", "delay_hours": 48}'

# Send a pre-visit form for an appointment
curl -X POST "https://your-domain.com/api/v1/questionnaire-invitations" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"source_type": "appointment", "source_id": "<appointment-id>"}'

# …or for a sales order
curl -X POST "https://your-domain.com/api/v1/questionnaire-invitations" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"source_type": "sales_order", "source_id": "<sales-order-id>"}'

A pre-visit form invitation returns url — the recipient's personal, single-use link — rather than a bare token. Treat it as a credential: anyone holding it can answer on that customer's behalf. It is null once the form has been answered.

survey-questions, survey-choices, survey-invitations, and survey-triggers are gated by the surveys module; questionnaire-invitations by the appointments module.