Surveys
Multi-question conversations over SMS, USSD or a web form: a customer texts your keyword, dials a code, scans a QR, or receives the first question from you; every answer is stored for you to pull over the API. No app, no data bundle needed on the customer's side.
A survey has two halves: create it (questions, the ways in, the messages) and send it (a named send to a list, now or at a time). The ways in sit side by side; a survey can use any combination and change them at any time:
- A keyword on the shared survey code 23358 or your own code: every inbound text matching it opens a session.
- Your own shortcode, no keyword: every text to a dedicated code with no other match opens the survey (
catch_all). - USSD: an extension of yours dials into the survey (
extension_uuid). Dial-in only. - Web link and QR: every survey has a public form (
GET /surveys/{uuid}/web-link); switch it off withweb_enabled: false. - A send: you send the first question to one number, a group, a pasted list or every contact, from the survey's shortcode. Replies find the survey through the open session, even on the shared code while other teams use it.
Endpoints live under /api/v1/ with a scoped mbs_ token or JWT.
Two shared codes, two jobs
for_survey: true goes on 23358; one without goes on 20973. A survey cannot be anchored to 20973. Keywords and surveys created before this rule stay where they are.Start from a template
Five ready-made question sets, each with a welcome and closing message: customer_satisfaction, event_rsvp, product_feedback, staff_pulse and market_research. Copy one into the questions array below and edit as you like. The portal's Surveys, New Survey wizard offers the same templates, lets you reserve a keyword, pick a USSD extension and set the opt-in in the same flow, and creates everything in one go.
Create a survey
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Internal label. |
| keyword_uuid | uuid | No | The keyword that opens this survey when texted. It decides the shortcode too, so shortcode_uuid is ignored when both are given. |
| shortcode_uuid | uuid | No | Without a keyword: the shortcode the survey runs on and sends from, from GET /api/v1/keywords/shortcodes. The shared survey code or a dedicated one your team holds. |
| catch_all | boolean | No | Dedicated shortcodes only: every text to the code with no keyword match and no open session starts this survey. One survey per code may do this. |
| extension_uuid | uuid | No | A USSD extension of yours that dials into the survey. It is pointed at the survey on creation. |
| web_enabled | boolean | No | The public web form. Default true. |
| opt_in | object | No | Asked first on every send: { "enabled": true, "message": "May we ask 3 quick questions?", "labels": ["Yes","No","Stop future surveys"], "declined_message": "...", "stopped_message": "..." }. See Opt-in. |
| one_per_msisdn | boolean | No | Accept one completed response per phone number. |
| sender_name | string | No | Sender the questions go out under, usually the shortcode the survey sits on. |
| welcome_message | string | No | Sent together with the first question when a session starts. |
| end_message | string | No | Sent after the last answer. Thank-yous, vouchers, next steps. |
| session_timeout_minutes | int | No | How long a respondent may pause between answers before they start over. Default 60. |
| questions | Question[] | No | The questions, inline (sequence is stored as given), each with optional followups; or add them one at a time afterwards (below). |
# Paste your token once (it starts with mbs_):
export MOBILESASA_TOKEN="mbs_your_token_here"
curl -X POST https://api.mobilesasa.com/api/v1/surveys \
-H "Authorization: Bearer $MOBILESASA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Delivery satisfaction, July",
"keyword_uuid": "8c11d2ab-…",
"sender_name": "23358",
"welcome_message": "Thanks for ordering from Dukapay! 3 quick questions:",
"end_message": "Asante! Your feedback keeps us sharp.",
"session_timeout_minutes": 30,
"opt_in": {
"enabled": true,
"message": "May we ask you 3 quick questions?",
"labels": [
"Yes",
"No",
"Stop future surveys"
]
},
"questions": [
{
"question": "How likely are you to recommend us? Reply 1 (not at all) to 10 (very likely)",
"question_type": "range",
"options": "{\"min\":1,\"max\":10}",
"sequence": 1,
"is_required": true,
"followups": [
{
"question": "Sorry to hear that. What went wrong?",
"trigger_kind": "band",
"trigger_value": "detractor"
}
]
},
{
"question": "Was the rider courteous? Reply YES or NO",
"question_type": "yes_no",
"sequence": 2,
"is_required": true,
"followups": [
{
"question": "What should the rider have done differently?",
"trigger_kind": "no"
}
]
},
{
"question": "Anything we should improve?",
"question_type": "open",
"sequence": 3,
"is_required": false
}
]
}'Change the ways in later
Rewires how people enter a survey, draft or live. Sessions already open keep going; new ones start the new way, and sends go out from the new shortcode.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| mode | string | No | keyword, shortcode or none. Optional: left out, it is derived from the fields given. |
| keyword_uuid | uuid | No | An approved keyword of yours that no other survey holds. On a shared code it must be on 23358. |
| shortcode_uuid | uuid | No | The shared survey code or a dedicated one you hold. |
| catch_all | boolean | No | Dedicated code only. Ignored otherwise. |
| extension_uuid | uuid | No | Leave it out to keep the USSD link as it is; "" unlinks; a uuid links that extension. |
| web_enabled | boolean | No | Switch the public web form on or off. |
# Paste your token once (it starts with mbs_):
export MOBILESASA_TOKEN="mbs_your_token_here"
curl -X PUT https://api.mobilesasa.com/api/v1/surveys/5e9dd410-…/reach \
-H "Authorization: Bearer $MOBILESASA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"shortcode_uuid": "5811569b-…",
"catch_all": false,
"extension_uuid": "a7c0…",
"web_enabled": true
}'Where to find the shortcode uuid
GET /api/v1/keywords/shortcodes lists every shortcode your team can use, with is_platform_shortcode true on the shared codes and shared_purpose saying which job each one does (surveys or keywords). A survey with no shortcode, keyword, extension or web form cannot be sent or answered.Add questions
Question fields
| Field | Type | Required | Description |
|---|---|---|---|
| question | string | Yes | The text sent to the participant. |
| question_type | string | Yes | range (Range: a number between min and max), single_choice (Multiple Choice, Pick One), multiple_choice (Multiple Choice, Pick Many), yes_no (Yes / No), numeric (Open Ended, Number), open (Open Ended, Free Text). |
| options | JSON string | No | For choice types: the options as a JSON array string, e.g. "[\"M-Pesa\",\"Card\",\"Cash\"]"; participants reply with the option number or the option text. For range: "{\"min\":1,\"max\":10}". |
| sequence | int | Yes | Order the question is asked in (1-based). |
| is_required | boolean | No | Required questions re-prompt on an invalid answer. |
| parent_uuid | uuid | No | Makes this a follow-up of that question (see below). The type is then always open and sequence is the parent's. |
| trigger_kind | string | No | Follow-ups only: yes or no (Yes / No parent), option (choice parent), band (Range parent). |
| trigger_value | string | No | With option: the option text. With band: detractor, passive or promoter. |
| followups | Followup[] | No | Inline alternative when creating a question: an array of { question, trigger_kind, trigger_value }. |
# Paste your token once (it starts with mbs_):
export MOBILESASA_TOKEN="mbs_your_token_here"
curl -X POST https://api.mobilesasa.com/api/v1/surveys/5e9dd410-…/questions \
-H "Authorization: Bearer $MOBILESASA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"question": "How did you pay?",
"question_type": "single_choice",
"options": "[\"M-Pesa\",\"Card\",\"Cash\"]",
"sequence": 4,
"is_required": true,
"followups": [
{
"question": "What made you choose cash today?",
"trigger_kind": "option",
"trigger_value": "Cash"
}
]
}'Answers are forgiving
Range bands
A Range question is read in three bands, the way a recommendation score is: on 1 to 10, 1 to 5 detractors, 6 to 7 passive, 8 to 10 promoters. Other scales keep the same proportions. The responses page, the reports and the xlsx export show the three counts and the score (promoters minus detractors as a percentage of all answers).
Follow-up questions
A follow-up hangs off a main question and is asked right after it, only when the answer matches its trigger: a No, a particular option, or a band. Follow-ups are open ended, so the person says why in their own words. On SMS and USSD the follow-up is the next prompt; on the web form it appears under the question the moment the matching answer is picked. In responses and exports a follow-up answer carries the question it followed (parent_question).
Opt-in and the stop list
A survey you send may ask permission first. With opt_in.enabled the first message of a send is the welcome, the opt-in message and three numbered answers in a fixed order: yes, no, stop future surveys. The labels are yours to word (labels, each under 40 characters); the order is not.
- 1 or the first label: question one is asked.
- 2 or the second label:
declined_messageis sent and the session ends for this survey. - 3 or the third label:
stopped_messageis sent, the session ends, and the number goes on the stop list of that shortcode.
STOP typed on its own in a session the survey started does the same as answer 3, at any point in the survey. In a session the person started by texting a keyword, STOP just ends that survey.
The stop list is honoured by sends only. A number on it is skipped, unbilled, whatever list it came in on; the send's skipped count says how many. Keyword, USSD and web starts are the person's own doing and are never blocked. On the shared survey code the list is one list: a person who stopped is stopped for every team sending from that code, because the number is what they see.
# Paste your token once (it starts with mbs_):
export MOBILESASA_TOKEN="mbs_your_token_here"
curl -X PUT https://api.mobilesasa.com/api/v1/surveys/5e9dd410-…/opt-in \
-H "Authorization: Bearer $MOBILESASA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"enabled": true,
"message": "May we ask you 3 quick questions about your delivery?",
"labels": [
"Yes",
"No",
"Stop future surveys"
],
"declined_message": "No problem, we will not ask you this time. Thank you.",
"stopped_message": "Understood. You will not receive surveys from this number again."
}'The stop lists of every shortcode your team can send from, newest first. A stop your own survey recorded can be lifted; one another team's survey recorded on the shared code cannot.
{
"success": true,
"data": {
"optouts": [
{ "uuid": "0b7e…", "shortcode": "23358", "msisdn": "254712345678", "survey_name": "Delivery satisfaction, July", "source": "sms", "created_at": "2026-10-15T09:12:40Z" }
]
}
}One extra SMS per person
Lifecycle
Statuses
| Field | Type | Required | Description |
|---|---|---|---|
| draft | initial | No | Editable (PATCH); not answering traffic. A survey needs at least one question before it can activate; no approval is involved. |
| active | live | No | Texts, dials, the web form and sends open sessions and questions flow. |
| paused | hold | No | New sessions refused; re-activate any time. |
| closed | final | No | Done. Data remains available. |
Approval on amendments
POST /surveys/{uuid}/amend with starts_at/ends_at) puts it through a quick system review; the survey can't activate while that review is pending. You're emailed automatically when it's approved or rejected.Test it before going live
The simulator walks a fake session without sending real SMS. Post the participant's next reply to /simulate, get back what the survey would send. /simulate-push opens the session the way a send would, opt-in question first when it is on; then continue with /simulate.
# Paste your token once (it starts with mbs_):
export MOBILESASA_TOKEN="mbs_your_token_here"
curl -X POST https://api.mobilesasa.com/api/v1/surveys/5e9dd410-…/simulate \
-H "Authorization: Bearer $MOBILESASA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"msisdn": "254712345678",
"message": "START"
}'You can also restrict a live survey to invited numbers (PUT /surveys/{uuid}/whitelist with {"msisdns": ["2547…"]}; empty array lifts the restriction). Only those phone numbers can answer.
Send a survey
A survey usually waits to be texted. This makes it speak first, as a named send: give it an audience and it goes out as a campaign (priced, reserved, cancellable, listed with the rest) from the survey's shortcode, and every reply comes back as an answer to that send. The name is what the report is cut by later: "Quarter 1 2026", "Quarter 2 2026".
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | No | The send's name in reports. Default: the survey name and today's date. |
| source_type | string | Yes | one (with phone), manual (with target_phones, at most 500), groups (with target_group_uuids) or all (every contact not blacklisted). |
| phone | string | No | With source_type one. |
| target_phones | string[] | No | With source_type manual. Kenyan formats accepted; duplicates dropped. |
| target_group_uuids | uuid[] | No | With source_type groups. |
| scheduled_at | RFC 3339 | No | Send at this time instead of now. |
| closes_at | RFC 3339 | No | Stop accepting answers to this send after this time. People who reply later are told the survey has closed. It closes the send, not the survey. |
# Paste your token once (it starts with mbs_):
export MOBILESASA_TOKEN="mbs_your_token_here"
curl -X POST https://api.mobilesasa.com/api/v1/surveys/5e9dd410-…/send \
-H "Authorization: Bearer $MOBILESASA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Quarter 4 2026",
"source_type": "groups",
"target_group_uuids": [
"9f31…"
],
"scheduled_at": "2026-10-20T09:00:00+03:00",
"closes_at": "2026-10-27T18:00:00+03:00"
}'{
"success": true,
"data": {
"wave": { "uuid": "c41a…", "name": "Quarter 4 2026", "audience_summary": "1 group(s)", "scheduled_at": "2026-10-20T06:00:00Z", "closes_at": "2026-10-27T15:00:00Z", "recipients": 1240, "sent": 0, "delivered": 0, "started": 0, "completed": 0, "declined": 0, "stopped": 0, "answers": 0 },
"campaign_uuid": "7d02…",
"skipped": 3,
"message": "Scheduled for 20 Oct 2026, 09:00. 3 number(s) skipped: they asked for no surveys from 23358."
}
}Requirements
| Field | Type | Required | Description |
|---|---|---|---|
| Scope | team:surveys:send | No | The API token needs it. Create one under Settings, API tokens. |
| Status | active | No | A draft or paused survey is refused: activate it first. |
| Shortcode | required | No | A keyword or a shortcode on the survey decides where the question is sent from, and where the answer comes back to. A survey with neither has nowhere to send from: set its reach first. |
| Balance | units | No | Question one (or the opt-in question) is a billed SMS like any other, and so is every question after it. |
Every send of the survey with its counts: recipients, sent, delivered, started, completed, declined, stopped, answers. Only a send nobody has answered can be deleted (a draft left by a failed send).
One number, right now
The single-number shortcut: opens the session and sends question one (or the opt-in question) to one number, outside any named send. A number on the shortcode's stop list is refused.
# Paste your token once (it starts with mbs_):
export MOBILESASA_TOKEN="mbs_your_token_here"
curl -X POST https://api.mobilesasa.com/api/v1/surveys/5e9dd410-…/start \
-H "Authorization: Bearer $MOBILESASA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"msisdn": "254712345678"
}'Where this earns its keep
USSD surveys cannot be started this way
The portal does this for you
Pull the responses
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
| date_from / date_to | YYYY-MM-DD | No | Defaults to today. |
| source | string | No | sms, ussd, web or simulator. |
| wave_uuid | uuid | No | Answers from one named send only. Exports take the same filter. |
# Paste your token once (it starts with mbs_):
export MOBILESASA_TOKEN="mbs_your_token_here"
curl "https://api.mobilesasa.com/api/v1/surveys/5e9dd410-…/responses?date_from=2026-10-01&date_to=2026-10-15&wave_uuid=c41a…" \
-H "Authorization: Bearer $MOBILESASA_TOKEN"{
"success": true,
"data": [
{
"uuid": "77b0…",
"session_uuid": "d2c4…",
"msisdn": "254712345678",
"question": "How likely are you to recommend us? Reply 1 (not at all) to 10 (very likely)",
"response": "3",
"source": "sms",
"wave_uuid": "c41a…",
"wave_name": "Quarter 4 2026",
"created_at": "2026-10-11T12:40:19Z"
},
{
"uuid": "77b1…",
"session_uuid": "d2c4…",
"msisdn": "254712345678",
"question": "Sorry to hear that. What went wrong?",
"parent_question": "How likely are you to recommend us? Reply 1 (not at all) to 10 (very likely)",
"response": "The rider was two hours late",
"source": "sms",
"wave_uuid": "c41a…",
"wave_name": "Quarter 4 2026",
"created_at": "2026-10-11T12:41:02Z"
}
]
}Answers cost units