Mobile SasaDocs

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 with web_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

23358 is the shared survey code: surveys and the keywords that open them live there. 20973 is the shared keyword code: auto-replies and callbacks. A keyword reserved with 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

GET/api/v1/surveys/templates

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

POST/api/v1/surveys

Parameters

FieldTypeRequiredDescription
namestringYesInternal label.
keyword_uuiduuidNoThe keyword that opens this survey when texted. It decides the shortcode too, so shortcode_uuid is ignored when both are given.
shortcode_uuiduuidNoWithout 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_allbooleanNoDedicated 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_uuiduuidNoA USSD extension of yours that dials into the survey. It is pointed at the survey on creation.
web_enabledbooleanNoThe public web form. Default true.
opt_inobjectNoAsked 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_msisdnbooleanNoAccept one completed response per phone number.
sender_namestringNoSender the questions go out under, usually the shortcode the survey sits on.
welcome_messagestringNoSent together with the first question when a session starts.
end_messagestringNoSent after the last answer. Thank-yous, vouchers, next steps.
session_timeout_minutesintNoHow long a respondent may pause between answers before they start over. Default 60.
questionsQuestion[]NoThe 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

PUT/api/v1/surveys/{uuid}/reach

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

FieldTypeRequiredDescription
modestringNokeyword, shortcode or none. Optional: left out, it is derived from the fields given.
keyword_uuiduuidNoAn approved keyword of yours that no other survey holds. On a shared code it must be on 23358.
shortcode_uuiduuidNoThe shared survey code or a dedicated one you hold.
catch_allbooleanNoDedicated code only. Ignored otherwise.
extension_uuiduuidNoLeave it out to keep the USSD link as it is; "" unlinks; a uuid links that extension.
web_enabledbooleanNoSwitch 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

POST/api/v1/surveys/{uuid}/questions
GET/api/v1/surveys/{uuid}/questions
DELETE/api/v1/surveys/{uuid}/questions/{question_uuid}

Question fields

FieldTypeRequiredDescription
questionstringYesThe text sent to the participant.
question_typestringYesrange (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).
optionsJSON stringNoFor 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}".
sequenceintYesOrder the question is asked in (1-based).
is_requiredbooleanNoRequired questions re-prompt on an invalid answer.
parent_uuiduuidNoMakes this a follow-up of that question (see below). The type is then always open and sequence is the parent's.
trigger_kindstringNoFollow-ups only: yes or no (Yes / No parent), option (choice parent), band (Range parent).
trigger_valuestringNoWith option: the option text. With band: detractor, passive or promoter.
followupsFollowup[]NoInline 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

1 or Yes, 2 or No, the option number or the option text, in any case: all land as the same answer. The same applies to the opt-in question.

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_message is sent and the session ends for this survey.
  • 3 or the third label: stopped_message is 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.

PUT/api/v1/surveys/{uuid}/opt-in
# 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."
  }'
GET/api/v1/surveys/optouts
DELETE/api/v1/surveys/optouts/{uuid}

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.

Response: 200
{
  "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

The opt-in question is a billed message like any other. A send with opt-in costs one unit more per person who is asked.

Lifecycle

POST/api/v1/surveys/{uuid}/activate
POST/api/v1/surveys/{uuid}/pause
POST/api/v1/surveys/{uuid}/close

Statuses

FieldTypeRequiredDescription
draftinitialNoEditable (PATCH); not answering traffic. A survey needs at least one question before it can activate; no approval is involved.
activeliveNoTexts, dials, the web form and sends open sessions and questions flow.
pausedholdNoNew sessions refused; re-activate any time.
closedfinalNoDone. Data remains available.

Approval on amendments

Setting or changing a survey's run window (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

POST/api/v1/surveys/{uuid}/simulate
POST/api/v1/surveys/{uuid}/simulate-push

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

POST/api/v1/surveys/{uuid}/send

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

FieldTypeRequiredDescription
namestringNoThe send's name in reports. Default: the survey name and today's date.
source_typestringYesone (with phone), manual (with target_phones, at most 500), groups (with target_group_uuids) or all (every contact not blacklisted).
phonestringNoWith source_type one.
target_phonesstring[]NoWith source_type manual. Kenyan formats accepted; duplicates dropped.
target_group_uuidsuuid[]NoWith source_type groups.
scheduled_atRFC 3339NoSend at this time instead of now.
closes_atRFC 3339NoStop 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"
  }'
Response: 200
{
  "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

FieldTypeRequiredDescription
Scopeteam:surveys:sendNoThe API token needs it. Create one under Settings, API tokens.
StatusactiveNoA draft or paused survey is refused: activate it first.
ShortcoderequiredNoA 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.
BalanceunitsNoQuestion one (or the opt-in question) is a billed SMS like any other, and so is every question after it.
GET/api/v1/surveys/{uuid}/waves
DELETE/api/v1/surveys/{uuid}/waves/{wave_uuid}

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

POST/api/v1/surveys/{uuid}/start

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

Close a support ticket, deliver an order, finish a clinic visit: call this and the survey asks about it while the experience is still fresh, without the customer having to remember a keyword. One call per person, so your own system decides who gets asked and when.

USSD surveys cannot be started this way

Only the subscriber can open a USSD session, so a survey answering on a USSD extension can be advertised but not pushed. Send an SMS inviting them to dial the code instead.

The portal does this for you

Surveys, Send Survey in the portal: pick the survey, preview the questions and the opening message, choose who, name the send, send now or schedule it, and set how long it stays open.

Pull the responses

GET/api/v1/surveys/{uuid}/responses

Query parameters

FieldTypeRequiredDescription
date_from / date_toYYYY-MM-DDNoDefaults to today.
sourcestringNosms, ussd, web or simulator.
wave_uuiduuidNoAnswers 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"
Response: 200
{
  "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

Each question and confirmation the survey sends is a normal SMS billed in units from your SMS balance. A 3-question survey costs roughly 4 to 5 units per completed session, one more with the opt-in question.