Run Discord surveys from code
The Subo API lets you create conversational surveys and polls, generate their scripts with AI, open them to a Discord server or a web audience, and pull the answers back out. Every recipe below is a working request you can paste into a terminal.
Quickstart
Three steps: get a key, find your community id, make a call. Everything after that is the same pattern.
1. Create an API key
Open the Subo web app,
go to Community Account Tab → API Keys and create a key.
It is scoped to that one community and inherits your role, so you can
optionally cap it at Creator access. The key starts with
sbo_live_ and is shown once, so store it before closing the dialog.
Send it on every request:
X-API-Key: sbo_live_xxxxxxxxxxxxxxxxxxxxxxxx 2. Find your community id
Almost every endpoint is nested under a community. List the ones your key can reach:
curl "https://api.subo.ai/v1/communities" \
-H "X-API-Key: $SUBO_API_KEY" For a Discord community the id is the server (snowflake) id, returned as a string.
3. Send a write request
Writes take an Idempotency-Key header holding a UUID v4. Retry
a failed request with the same key and Subo replays the original result
instead of creating a duplicate project.
-H "Idempotency-Key: $(uuidgen)" Recipe: generate a survey from a plain-English goal
This is the shortest path from nothing to a working survey, and the one
worth reaching for if you are writing an agent. Describe what you want to
learn in the intent field and Subo writes the script:
curl -X POST "https://api.subo.ai/v1/communities/$COMMUNITY_ID/projects" \
-H "X-API-Key: $SUBO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"name": "Post-event feedback",
"type": "convo",
"intent": "Measure how members felt about the weekend tournament and collect suggestions for the next one",
"max_blocks": 6,
"privacy_mode": "anonymous"
}'
The 201 response includes the generated script with its
blocks plus a credits_used figure. A few notes that save a round trip:
typeisconvofor a multi-question survey orpollfor a single question.max_blocksdefaults to 5 and is capped at 20.-
intent, an explicitscriptandsource_project_idare mutually exclusive. Send exactly one. -
AI generation spends bot credits. With an empty balance the call returns
402 payment_requiredrather than a partial survey. - The script is generated in the community's configured language, not the language of your intent string.
New projects start inactive. Nobody can answer yet, which gives you room to review or edit before opening it.
Recipe: write the script yourself
When you want exact control, send the blocks instead of an intent. Same endpoint, different field:
curl -X POST "https://api.subo.ai/v1/communities/$COMMUNITY_ID/projects" \
-H "X-API-Key: $SUBO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"name": "Server health check",
"type": "convo",
"privacy_mode": "anonymous",
"script": {
"blocks": [
{
"type": "single_punch",
"title": "How often do you hang out here?",
"options": [
{ "value": "Every day" },
{ "value": "A few times a week" },
{ "value": "Rarely" }
]
},
{
"type": "open_text",
"title": "What would make this server better?"
}
]
}
}'
To replace the script of an existing project, PUT it whole. The
project has to be inactive:
PUT https://api.subo.ai/v1/communities/{communityId}/projects/{projectId}/script
Individual blocks have their own endpoints for surgical edits:
POST .../script/blocks to append,
PUT .../script/blocks/{blockId} to change one,
DELETE to drop it.
Skip logic is worth calling out. You can hand-write the raw
precondition mini-language, but the API also accepts a structured
show_when / hide_when object and compiles it into a
correct precondition for you. Prefer the structured form: it removes a
whole class of quoting and inversion mistakes.
Recipe: open the project to an audience
Opening is what makes a project live. It is also where you choose whether this runs inside Discord or as a web link.
curl -X POST "https://api.subo.ai/v1/communities/$COMMUNITY_ID/projects/$PROJECT_ID/open" \
-H "X-API-Key: $SUBO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"delivery": {
"audience": {
"participation": "private",
"response_channel": "discord"
},
"invitation": {
"post_channel_id": "123456789012345678",
"message": "Two minutes of your time, and you get the role."
},
"closing": {
"mode": "scheduled",
"time": "2026-09-01T18:00:00Z"
}
}
}' The audience combination decides who can answer and where:
| participation | response_channel | What you get |
|---|---|---|
private | discord | Members answer in your Discord server, as a conversation with the bot. |
private | web | A link, but only your verified members can complete it. |
open_web | web | An open link anyone can answer, no Discord account needed. |
Add required_role_ids under audience to restrict a
Discord audience to specific roles. To close early, call
POST .../projects/{projectId}/close.
Recipe: pull the responses
curl "https://api.subo.ai/v1/communities/$COMMUNITY_ID/projects/$PROJECT_ID/responses?page=1&per_page=100" \
-H "X-API-Key: $SUBO_API_KEY" The shape you get back:
{
"data": [
{
"id": "...",
"project_id": "...",
"submitted_at": "2026-08-02T14:03:11Z",
"session_number": 1,
"user_id": "...",
"platform_id": "...",
"provider": "discord",
"answers": [
{ "block_id": 1, "option_id": 2, "value": "A few times a week" },
{ "block_id": 2, "value": "More events in EU hours." }
]
}
],
"pagination": { "page": 1, "per_page": 100, "total": 214, "has_more": true }
} per_pagedefaults to 20 and maxes out at 100. Page untilhas_moreis false.-
user_idis the Subo user id and the only globally unique key.platform_idis the account id inside its platform namespace, so it is only meaningful paired withprovider. Join onuser_id. -
One respondent can appear more than once when a project allows repeat
completions.
session_numberseparates those runs. - Privacy modes do not redact this endpoint. Subo's own
surfaces mask identity on an anonymous project: the Responses tab hides
it and the XLSX export blanks the id, name, nickname and session columns.
The API does neither.
user_id,platform_idandsession_numbercome back on every project whatever itsprivacy_mode, and theresponse.submittedwebhook carries the same fields. If you promised your members anonymity, drop those fields at your ingestion boundary rather than assuming the API already did.
Polling this endpoint on a timer works, but webhooks are the better pipeline.
Recipe: summarize open-ended answers with AI
Free-text answers are the ones nobody has time to read. Trigger a
summarization pass over the open_text blocks:
curl -X POST "https://api.subo.ai/v1/communities/$COMMUNITY_ID/projects/$PROJECT_ID/analysis" \
-H "X-API-Key: $SUBO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "force": false }'
The job runs asynchronously, so the POST returns as soon as it is queued.
Read the result with a GET on the same path, which also returns
the aggregated stats and distributions for the closed questions:
curl "https://api.subo.ai/v1/communities/$COMMUNITY_ID/projects/$PROJECT_ID/analysis" \
-H "X-API-Key: $SUBO_API_KEY"
Set force: true to re-run a summary that already exists, for
instance after a wave of new responses. Analysis spends bot credits and
returns 402 payment_required on an empty balance. Rather than
polling for completion, subscribe to analysis.completed.
Recipe: subscribe to webhooks and verify them
Register an endpoint and the events you care about:
curl -X POST "https://api.subo.ai/v1/communities/$COMMUNITY_ID/webhooks" \
-H "X-API-Key: $SUBO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Responses to our warehouse",
"url": "https://example.com/hooks/subo",
"events": ["response.submitted", "analysis.completed", "project.closed"]
}'
The response contains signing_secret, shown once and never again.
Store it before you close the terminal. Every delivery then arrives with
these headers:
| Header | Value |
|---|---|
X-Subo-Event | The event type, for example response.submitted |
X-Subo-Delivery | A unique event id, useful for deduplication |
X-Subo-Signature | sha256=<hmac hex> over the raw body |
Verifying in Node:
import crypto from "node:crypto";
// req.body must be the RAW bytes, not a parsed and re-serialized object.
function verifySuboWebhook(rawBody, header, secret) {
const expected =
"sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(header || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
} Things worth knowing before you rely on this in production:
- Hash the raw request body. Re-serializing the JSON changes the bytes and every signature check will fail.
-
Compare in constant time. A plain
===leaks timing information. - Subo waits 10 seconds for your endpoint, then retries after 30s, 5m, 30m, 2h and 8h before abandoning the delivery. Return a 2xx fast and do the real work off the request path.
-
Deliveries are at-least-once. Deduplicate on
X-Subo-Delivery. -
response.submittedrequires a paid tier. The project lifecycle events are available to everyone. -
POST .../webhooks/{webhookId}/testfires a test delivery, andGET .../webhooks/{webhookId}/deliveriesshows attempt history with the status codes you returned. Use both before blaming the network.
Available events:
project.createdproject.updatedproject.openedproject.closedproject.status_changedproject.deletedresponse.submittedanalysis.completed
Recipe: clone a ready-made template
Subo ships curated templates: quizzes, prediction contests, feedback surveys. Cloning one is faster than writing a script and carries over the scoring, grading and reward blocks that make it work.
# Browse the catalog
curl "https://api.subo.ai/v1/templates" -H "X-API-Key: $SUBO_API_KEY"
# Clone one into your community
curl -X POST "https://api.subo.ai/v1/communities/$COMMUNITY_ID/templates/$TEMPLATE_ID/clone" \
-H "X-API-Key: $SUBO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "name": "Season 4 prediction contest" }' The clone is a deep copy: script, scoring buckets, grading, action blocks and calculated fields all come along. Anything bound to the source server, such as channels, roles and schedules, is stripped so you start clean. The new project arrives inactive, ready for an open call.
A machine-readable list of every public template, with slugs and clone links, lives at /templates.json.
Reference tables
Block types
| Type | What it does |
|---|---|
single_punch | Pick one option. Also covers yes/no. |
multi_punch | Pick several options. |
open_text | Free text. The input for AI analysis. |
open_numeric | A number. |
content_block | Says something without asking anything. |
action_block | Grants XP, a role or an achievement mid-conversation. |
calculated_block | Computes a value from earlier answers. |
Privacy modes
privacy_mode | Who can see who said what |
|---|---|
transparent | Answers are attributed openly. |
semi-private | Admins can see individual answers; other members cannot. |
anonymous | Nobody, including admins, can tie an answer to a member. |
Omit privacy_mode on create and the survey inherits the
privacy mode the community has configured as its default, the same one
the app applies. A community that has never changed that setting is on
anonymous. Set the field explicitly when
anonymity matters rather than relying on someone else's setting.
Rate limits
| Tier | Requests per minute |
|---|---|
| Free | 60 |
| Premium | 300 |
| VIP | 600 |
| Custom Bot | 1000 |
Limits are counted per key in a one-minute window. Every response carries
X-RateLimit-Limit, X-RateLimit-Remaining and
X-RateLimit-Reset; a 429 adds Retry-After.
See pricing for what else each tier includes.
Errors
| Status | Code | Usual cause |
|---|---|---|
| 400 | invalid_request | Bad body, unknown block type, or two mutually exclusive script sources. |
| 402 | payment_required | Out of bot credits for an AI call, or a feature above your tier. |
| 403 | forbidden | A Creator-scoped key reaching for a project it does not own. |
| 404 | not_found | Wrong community or project id for this key. |
| 429 | rate_limit_exceeded | Over the tier limit. Wait Retry-After seconds. |
FAQ
- How do I authenticate with the Subo API?
- Send your key in the X-API-Key header on every request. Keys start with sbo_live_ and are created in the Subo web app under Community Account Tab, then API Keys. Each key is scoped to a single community and inherits your role, and it is shown only once.
- What is the Subo API base URL?
- https://api.subo.ai/v1. Non-breaking changes ship in place under /v1; breaking changes would ship under a new version prefix.
- Can an AI agent create a Discord survey with the Subo API?
- Yes. Pass an intent field, a plain-text description of what you want to learn, when creating a project and Subo generates the full multi-question script for you. The 201 response contains the generated blocks, so an agent can create a working survey in a single call.
- What are the Subo API rate limits?
- Limits are per API key and depend on the community tier: 60 requests per minute on Free, 300 on Premium, 600 on VIP and 1000 on Custom Bot. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers, and a 429 includes Retry-After.
- How do I get survey responses out of Subo?
- Either poll GET /communities/{communityId}/projects/{projectId}/responses, which is paginated, or subscribe a webhook to the response.submitted event and receive each submission as it happens. Webhooks are the recommended path for pipelines.
- How do I verify a Subo webhook signature?
- Compute an HMAC-SHA256 of the raw request body using the signing secret returned when you created the webhook, then compare it to the X-Subo-Signature header, which has the form sha256=<hex>. Always compare with a constant-time function and always hash the raw bytes, not a re-serialized object.
Where to go next
- Full endpoint reference for every field and response shape.
- OpenAPI 3.1 spec to generate a client or feed an agent.
- llms.txt if you are pointing an AI agent at Subo.
- Tutorials for the no-code path through the same features.
- Support server when something behaves in a way this page did not predict.
START A CONVERSATION WITH YOUR COMMUNITY.
No credit card. No setup hassle. Just jump in and play around with few limitations. Do more with paid plans when you're ready.