Loomio User API documentation
/api/user0 is the user-oriented API for integrations with Loomio. It uses the API key of a user account, and every action is performed as that user.
Group operations use the memberships and group permissions of the API-key user. Instance-administrator status does not expand an API key's access to groups or content; use the Server API for instance-level administration.
Use the API key from the Loomio user account that will perform the actions. A dedicated bot account is useful when an integration should not be invited to polls or receive notifications.
Signed-in users can find their API key and group IDs on the API access page.
Send the API key in an Authorization: Bearer header. API keys in query strings are rejected because URLs can be recorded by proxies and access logs.
Authorization: Bearer YOUR_API_KEY
The examples use YOUR_API_KEY, group ID 123, and https://www.loomio.com/. Replace these with your API key, group ID, and Loomio installation URL.
Sending request bodies
Send JSON with Content-Type: application/json. The cURL examples use --json, which sets this header automatically. The create and edit endpoints for discussions, comments, and polls accept either a flat object or an object nested under the resource name, such as {"poll": {...}}.
Send arrays as JSON arrays, such as "options": ["foo", "bar"]. If you use form encoding instead, arrays need bracketed field names, such as options[]=foo&options[]=bar; repeating options=foo&options=bar keeps only the last value.
Check the HTTP status and response body after each request. A validation failure returns 422 with an errors object; a successful poll creation returns the poll in polls and its options in poll_options.
Endpoint summary
| Method | Endpoint | Purpose |
|---|---|---|
POST |
/api/user0/comments |
Create a comment or reply |
PATCH |
/api/user0/comments/:id |
Edit a comment |
DELETE |
/api/user0/comments/:id |
Soft-delete a comment |
POST |
/api/user0/discussions |
Create a discussion |
GET |
/api/user0/discussions/:id |
Get a discussion |
GET |
/api/user0/discussions |
List discussions in a group |
PATCH |
/api/user0/discussions/:id |
Edit a discussion |
DELETE |
/api/user0/discussions/:id |
Soft-delete a discussion |
GET |
/api/user0/groups |
List the API-key user's groups |
GET |
/api/user0/groups/:id_or_key_or_handle |
Get a visible group |
GET |
/api/user0/memberships |
List a group's memberships |
POST |
/api/user0/memberships |
Add members and optionally remove absent members |
GET |
/api/user0/reports |
Generate a participation report |
POST |
/api/user0/polls |
Create a poll |
GET |
/api/user0/polls/:id |
Get a poll |
GET |
/api/user0/polls |
List polls in a group |
PATCH |
/api/user0/polls/:id |
Edit a poll |
DELETE |
/api/user0/polls/:id |
Soft-delete a poll |
GET |
/api/user0/search |
Search visible discussions, comments, polls, votes, and outcomes |
GET |
/api/user0/threads |
List visible discussion and standalone-poll threads |
GET |
/api/user0/threads/:topic_id |
Get a thread |
GET |
/api/user0/threads/:topic_id/items |
Get the ordered items in a thread |
GET |
/api/user0/threads/:topic_id/markdown |
Get a complete thread as Markdown |
GET |
/api/user0/chatbots |
List a group's chat integrations and webhooks |
POST |
/api/user0/chatbots |
Create a chat integration or webhook |
PATCH |
/api/user0/chatbots/:id |
Update a chat integration or webhook |
DELETE |
/api/user0/chatbots/:id |
Delete a chat integration or webhook |
POST |
/api/user0/chatbots/check |
Send a webhook connection test |
Pagination
Discussions, groups, memberships, polls, threads, thread items, and webhooks accept limit (default 50) and offset (default 0). Groups, thread items, and webhooks cap limit at 500.
Increase offset by the number of primary records returned to fetch the next page. meta.total is the full matching count; related records do not count toward the page size.
Participation reports return the complete requested data, and search returns up to 20 results. Neither supports pagination.
Discussions, polls, and memberships also accept the legacy aliases per for limit and from for offset. Use limit and offset for new integrations.
Related records
User API responses use a compound format: the primary records are accompanied by related records such as topics, groups, users, polls, and reactions. This lets a client populate a local record store from one request, but can include more data than a simple integration needs.
Pass compact=1 to omit bulky related topics, groups, parent groups, memberships, reactions, tags, and translations. Primary records and the related records needed to interpret their content remain present.
curl 'https://www.loomio.com/api/user0/threads/123/items?compact=1' \
-H 'Authorization: Bearer YOUR_API_KEY'
For direct control, pass exclude_types with space-separated singular record types. For example, exclude_types=group reaction omits related groups and reactions. Common values are topic, group, parent, membership, reaction, tag, translation, user, discussion, poll, poll_option, stance, stance_choice, outcome, and topic_item. Exclusions apply to related records, not the primary resource requested by the endpoint.
Collection responses include meta.total when an exact collection size is defined. The total is calculated before limit and offset are applied. Endpoints such as search that intentionally return a bounded result set omit meta.total rather than returning null.
Comments
Create Comment
Create a comment in a discussion as the API-key user.
POST /api/user0/comments
Params
| Name | Description |
|---|---|
discussion_id |
Integer, required. ID of the discussion to comment on |
body |
Comment body, required unless an attachment is provided |
body_format |
Either md or html, optional, default md |
Example
curl https://www.loomio.com/api/user0/comments \
-H 'Authorization: Bearer YOUR_API_KEY' \
--json '{
"discussion_id": 123,
"body": "example comment",
"body_format": "md"
}'
Edit Comment
Edit a comment as the API-key user. The same permissions apply as in Loomio: the user must be allowed to edit that comment.
PATCH /api/user0/comments/:id
Params
| Name | Description |
|---|---|
body |
Updated comment body |
body_format |
Either md or html, optional, default md |
Example
curl https://www.loomio.com/api/user0/comments/123 \
-X PATCH \
-H 'Authorization: Bearer YOUR_API_KEY' \
--json '{
"body": "updated comment",
"body_format": "md"
}'
Soft Delete Comment
Soft delete a comment as the API-key user. This discards the comment, hides its body, and keeps the comment record in place.
DELETE /api/user0/comments/:id
Example
curl https://www.loomio.com/api/user0/comments/123 \
-X DELETE \
-H 'Authorization: Bearer YOUR_API_KEY'
Discussions
Create Discussion
Create a discussion as the API-key user.
POST /api/user0/discussions
Params
| Name | Description |
|---|---|
group_id |
Group where thread will exist |
title |
Title of the thread, required |
description |
Context for the thread, optional |
description_format |
Either md or html, optional, default md |
recipient_audience |
group or null. If group, the whole group will be notified about the new thread |
recipient_user_ids |
Array of user IDs to notify or invite to the thread |
recipient_emails |
Array of email addresses of people to invite to the thread |
recipient_message |
Message to include in the email invitation |
Example
curl https://www.loomio.com/api/user0/discussions \
-H 'Authorization: Bearer YOUR_API_KEY' \
--json '{
"group_id": 123,
"title": "example thread",
"recipient_emails": [
"person@example.com"
]
}'
Show Discussion
Fetch a discussion using the discussion ID, an integer, or key, a string.
GET /api/user0/discussions/:id
Example
curl https://www.loomio.com/api/user0/discussions/abc123 \
-H 'Authorization: Bearer YOUR_API_KEY'
List Discussions
List discussions visible to the API-key user in a group. For a publicly visible group, a nonmember can list its public discussions; private discussions remain restricted to users who can read them in Loomio.
GET /api/user0/discussions
Params
| Name | Description |
|---|---|
group_id |
Integer, required. ID of the group to list discussions from |
status |
String, optional, default open. Values: open, closed, all |
limit |
Integer, optional, default 50. Page size |
offset |
Integer, optional, default 0. Offset for pagination |
Legacy: per and from are accepted as aliases for limit and offset and will continue to work.
Example
curl 'https://www.loomio.com/api/user0/discussions?group_id=123' \
-H 'Authorization: Bearer YOUR_API_KEY'
Edit Discussion
Edit a discussion as the API-key user. The same permissions apply as in Loomio: the user must be allowed to edit that discussion.
PATCH /api/user0/discussions/:id
Params
| Name | Description |
|---|---|
title |
Updated title |
description |
Updated context |
description_format |
Either md or html, optional, default md |
recipient_audience |
group or null. If group, the whole group will be notified about the edit |
recipient_user_ids |
Array of user IDs to notify or invite to the thread |
recipient_emails |
Array of email addresses of people to invite to the thread |
recipient_message |
Message to include in the email invitation |
Example
curl https://www.loomio.com/api/user0/discussions/123 \
-X PATCH \
-H 'Authorization: Bearer YOUR_API_KEY' \
--json '{
"title": "updated thread title",
"description": "updated context",
"description_format": "md"
}'
Soft Delete Discussion
Soft delete a discussion as the API-key user. This discards the discussion and keeps the discussion record in place.
DELETE /api/user0/discussions/:id
Example
curl https://www.loomio.com/api/user0/discussions/123 \
-X DELETE \
-H 'Authorization: Bearer YOUR_API_KEY'
Groups
List groups
Return the groups in which the API-key user has an active membership.
GET /api/user0/groups
| Name | Description |
|---|---|
limit |
Integer, optional, default 50, maximum 500. Page size |
offset |
Integer, optional, default 0. Offset for pagination |
curl 'https://www.loomio.com/api/user0/groups?limit=50&offset=0' \
-H 'Authorization: Bearer YOUR_API_KEY'
The response contains a groups page ordered by ascending group ID, with the full matching group count in meta.total. It includes parent groups and subgroups, including groups whose subscription is not currently active. Check the enabled field when an integration should operate only on enabled groups.
Important group fields include:
| Field | Description |
|---|---|
id |
Numeric group ID used by other User API endpoints |
key |
Stable short key used in Loomio URLs |
handle |
Human-readable group handle |
name |
Group name |
full_name |
Group name including its parent-group context |
parent_id |
Numeric parent-group ID for a subgroup, otherwise null |
enabled |
Whether the group and its subscription are active |
memberships_count |
Number of active and pending memberships |
accepted_memberships_count |
Number of accepted memberships |
pending_memberships_count |
Number of pending invitations |
admin_memberships_count |
Number of group administrators |
discussions_count |
Number of discussions directly in the group |
polls_count |
Number of polls directly in the group |
subgroups_count |
Number of subgroups |
The response may include additional group settings, related parent-group records and the API user's memberships. Clients should ignore fields they do not use.
Get a group
Return one group visible to the API-key user.
GET /api/user0/groups/:id_or_key_or_handle
The identifier can be the group's numeric ID, key or handle.
curl https://www.loomio.com/api/user0/groups/123 \
-H 'Authorization: Bearer YOUR_API_KEY'
curl https://www.loomio.com/api/user0/groups/example-group \
-H 'Authorization: Bearer YOUR_API_KEY'
The response contains the group in the groups array and uses the same fields as the list endpoint. A request for a group the API-key user cannot access returns a permission error.
Memberships
List Memberships
List the memberships visible to the API-key user. Group members can read member names, IDs, titles, and roles. Email addresses are included only for the API-key user's own account or when the API-key user is a group administrator.
GET /api/user0/memberships
Params
| Name | Description |
|---|---|
group_id |
Integer, required. ID of the group whose memberships will be listed |
limit |
Integer, optional, default 50. Page size |
offset |
Integer, optional, default 0. Offset for pagination |
Legacy: per and from are accepted as aliases for limit and offset and will continue to work.
The response contains a page of memberships and related users. meta.total is the total number of visible memberships in the group before pagination.
Example
curl 'https://www.loomio.com/api/user0/memberships?group_id=123&limit=50&offset=0' \
-H 'Authorization: Bearer YOUR_API_KEY'
Manage Memberships
Send a list of emails. It will invite all the new email addresses to the group. Unlike listing memberships, this operation requires group-administrator permission.
POST /api/user0/memberships
Params
| Name | Description |
|---|---|
group_id |
Integer, required. ID of the group whose memberships will be managed |
emails |
Array of strings, required. Email addresses of people to invite into the group |
remove_absent |
Boolean. If true, remove anyone from the group whose email is not present in the list |
Example
curl https://www.loomio.com/api/user0/memberships \
-H 'Authorization: Bearer YOUR_API_KEY' \
--json '{
"group_id": 123,
"emails": [
"person@example.com"
]
}'
If you pass remove_absent=1, any members of the group who were not included in the list will be removed from the group. Be careful, you could remove everyone in your group.
curl https://www.loomio.com/api/user0/memberships \
-H 'Authorization: Bearer YOUR_API_KEY' \
--json '{
"group_id": 123,
"emails": [
"person@example.com"
],
"remove_absent": 1
}'
This returns an object with {added_emails: ["person@added.com"], removed_emails: ["person@removed.com"]}.
Participation report
Return the same aggregated participation data used by Loomio's Participation report.
GET /api/user0/reports
Params
| Name | Description |
|---|---|
section |
Report section: base, users, or countries. Use users for per-person activity |
group_scope |
custom or my. The legacy all value is treated as my because User API keys never receive instance-wide access |
group_ids |
Comma-separated group IDs when group_scope=custom. IDs outside the API user's memberships are ignored |
start_month |
First month to include in YYYY-MM format; defaults to 12 months ago |
end_month |
Last month to include in YYYY-MM format; defaults to the current month |
interval |
Interval for the base section: day, week, month, or year |
member_type |
Set to delegate with section=users to return only current delegates |
A person is a delegate when they have an active delegate membership in any selected group. Their counts are aggregated across all selected groups. Delegate rows are returned even when every activity count is zero. Counts cover threads, comments, polls, votes, outcomes, and reactions; they are not voting participation rates. User rows also include identified ballots issued, cast, and missed. Anonymous polls are excluded from all per-person voting counts. all_votes_cast is true only when at least one ballot was issued and every issued ballot was cast.
The API applies the same group visibility rules as the in-product report. A user API key cannot expose report data from groups that user cannot access.
Example
curl 'https://www.loomio.com/api/user0/reports?section=users&group_scope=custom&group_ids=123&member_type=delegate&start_month=2026-01&end_month=2026-09' \
-H 'Authorization: Bearer YOUR_API_KEY'
The users array contains complete activity rows:
{
"users": [
{
"id": 456,
"name": "Ada Lovelace",
"country": "NZ",
"delegate": true,
"threads": 2,
"comments": 8,
"polls": 1,
"votes": 5,
"votes_cast": 5,
"votes_issued": 6,
"votes_missed": 1,
"all_votes_cast": false,
"outcomes": 1,
"reactions": 4
}
]
}
Polls
Create Poll
Create a poll as the API-key user.
POST /api/user0/polls
Params
| Name | Description |
|---|---|
group_id |
Integer, optional, default null. ID of group for poll. If discussion_id is passed, group_id is ignored |
discussion_id |
Integer, optional, default null. ID of discussion thread to add this poll to |
title |
String, required. Title of the poll |
poll_type |
String, required. Values: proposal, poll, count, score, ranked_choice, meeting, dot_vote |
details |
String, optional. The body text of the poll |
details_format |
String, optional, default md. Values: md or html |
options |
Simple option names as an array of strings. For proposal, use agree, disagree, abstain, or block to get the standard labels, icons, meanings, and prompts. For meeting, provide ISO 8601 date or datetime strings. For other poll types, provide option names. For detailed options, use poll_options_attributes instead |
poll_options_attributes |
Alternative to options: an array of option objects with names, meanings, prompts, icons, and ordering. See Create a poll with detailed options |
reason_prompt |
String, optional. Shared prompt for the voting reason, used by poll types without per-option prompts, such as poll, score, and ranked_choice |
stance_reason_required |
String, default optional. Values: disabled, optional, required, required_for_disagree_or_block, required_for_block. The last two apply to the disagree and block icons. Anonymous polls disable voting reasons |
closing_at |
ISO 8601 string or null, default null. Example: 2026-09-01T12:00:00Z. If null, voting is disabled and poll is considered work in progress |
specified_voters_only |
Boolean, optional, default false. If true, only specified people can vote. If false, everyone in the group will be invited to vote |
hide_results |
String, optional, default off. Values: off, until_vote, until_closed |
shuffle_options |
Boolean, default false. Display options to voters in random order |
anonymous |
Boolean, optional, default false. Hide identities of voters |
recipient_audience |
group or null, optional, default null. If group, the whole group will be notified |
notify_on_closing_soon |
String, optional, default nobody. Values: nobody, author, voters_uncast, voters |
recipient_user_ids |
Array of user IDs to notify or invite |
recipient_emails |
Array of email addresses of people to invite to vote |
recipient_message |
Message to include in the email invitation |
notify_recipients |
Boolean, default false. If false, add people without sending notifications. If true, everyone invited in this request will get a notification email |
Example
curl https://www.loomio.com/api/user0/polls \
-H 'Authorization: Bearer YOUR_API_KEY' \
--json '{
"group_id": 123,
"title": "example poll",
"poll_type": "proposal",
"options": [
"agree",
"disagree"
],
"closing_at": "2026-09-01T12:00:00Z",
"recipient_emails": [
"person@example.com"
]
}'
Create a draft poll with many options
Send all option names in the options array. Omitting closing_at creates a work-in-progress poll with voting disabled; it does not hide the poll from people who can read its thread. Add a future closing_at when you are ready to open voting.
Add all your option names to the array:
curl https://www.loomio.com/api/user0/polls \
-H 'Authorization: Bearer YOUR_API_KEY' \
--json '{
"group_id": 123,
"title": "Meeting venue",
"poll_type": "poll",
"options": [
"Town hall",
"Library",
"Community centre"
]
}'
Create a poll with detailed options
Use options for a list of names, or poll_options_attributes when each option needs a meaning, a reason prompt, or an icon. Choose one format per request. Both formats work in a flat JSON body or inside a poll object.
Each object in poll_options_attributes accepts these fields:
| Field | Description |
|---|---|
name |
String, required for a new option. The label shown to voters |
meaning |
String, optional. Explains what choosing this option means |
prompt |
String, optional. Reason prompt shown when this option is selected in a proposal, count, or check. For other poll types, set reason_prompt on the poll |
icon |
String, optional. For proposal, count, and check, use agree, disagree, abstain, or block |
priority |
Integer. Option order, starting at 0. Supply a distinct priority for each option; meeting options are ordered by date instead |
test_operator |
String or null, optional. Vote-share requirement: gte means at least, lte means no more than. Set to null to disable the requirement |
test_percent |
Integer, optional. Percentage for the vote-share requirement, from 0 to 100 |
test_against |
String, optional. score_percent compares the option's share of total score or points cast; voter_percent compares its voters with all eligible voters |
id |
Integer. Include when editing an existing option; omit for a new option |
_destroy |
Boolean, optional. Set to true with an existing option's id to remove it |
Create a proposal with custom option labels and prompts:
curl https://www.loomio.com/api/user0/polls \
-H 'Authorization: Bearer YOUR_API_KEY' \
--json '{
"group_id": 123,
"title": "Should we adopt this proposal?",
"poll_type": "proposal",
"stance_reason_required": "optional",
"poll_options_attributes": [
{
"name": "Support",
"meaning": "I support adopting this proposal",
"prompt": "What do you support about it?",
"icon": "agree",
"priority": 0
},
{
"name": "Oppose",
"meaning": "I would prefer another approach",
"prompt": "What would you change?",
"icon": "disagree",
"priority": 1
}
]
}'
For a regular poll, use the same detailed option format with a shared reason_prompt on the poll:
curl https://www.loomio.com/api/user0/polls \
-H 'Authorization: Bearer YOUR_API_KEY' \
--json '{
"group_id": 123,
"title": "Choose a venue",
"poll_type": "poll",
"reason_prompt": "Why do you prefer this venue?",
"stance_reason_required": "optional",
"poll_options_attributes": [
{
"name": "Town hall",
"meaning": "Central, accessible venue",
"priority": 0
},
{
"name": "Library",
"meaning": "Smaller room with no hire fee",
"priority": 1
}
]
}'
These examples omit closing_at, so voting stays disabled until a future closing date is set.
Show Poll
Fetch a poll using the poll ID, an integer, or key, a string.
GET /api/user0/polls/:id
Example
curl https://www.loomio.com/api/user0/polls/abc123 \
-H 'Authorization: Bearer YOUR_API_KEY'
List Polls
List polls visible to the API-key user in a group. For a publicly visible group, a nonmember can list its public polls; private polls remain restricted to users who can read them in Loomio. The response includes each visible poll's current outcome, so you can use status=closed to list decided proposals.
GET /api/user0/polls
Params
| Name | Description |
|---|---|
group_id |
Integer, required. ID of the group to list polls from |
status |
String, optional, default active. Values: active, closed, all |
limit |
Integer, optional, default 50. Page size |
offset |
Integer, optional, default 0. Offset for pagination |
Legacy: per and from are accepted as aliases for limit and offset and will continue to work.
Example
curl 'https://www.loomio.com/api/user0/polls?group_id=123' \
-H 'Authorization: Bearer YOUR_API_KEY'
Edit Poll
Edit a poll as the API-key user. The same permissions apply as in Loomio: the user must be allowed to edit that poll.
PATCH /api/user0/polls/:id
Params
| Name | Description |
|---|---|
title |
Updated title |
details |
Updated poll details |
details_format |
Either md or html, optional, default md |
options |
Updated option names. Changing options may affect existing votes depending on poll state |
poll_options_attributes |
Alternative to options. Update detailed option objects by id, add objects without an id, or remove an option with id and _destroy: true. Omitted options remain unchanged |
reason_prompt |
Updated shared reason prompt for poll types without per-option prompts |
stance_reason_required |
Updated reason requirement; the same values apply as when creating a poll |
closing_at |
ISO 8601 string or null |
recipient_audience |
group or null. If group, the whole group will be notified |
recipient_user_ids |
Array of user IDs to notify or invite |
recipient_emails |
Array of email addresses of people to invite to vote |
recipient_message |
Message to include in the email invitation |
Example
curl https://www.loomio.com/api/user0/polls/123 \
-X PATCH \
-H 'Authorization: Bearer YOUR_API_KEY' \
--json '{
"title": "updated poll title",
"details": "updated details",
"details_format": "md"
}'
Edit detailed options
Get option IDs from the poll_options array in a create or get response. Send only the fields to change, together with each existing option's id:
curl https://www.loomio.com/api/user0/polls/123 \
-X PATCH \
-H 'Authorization: Bearer YOUR_API_KEY' \
--json '{
"poll_options_attributes": [
{
"id": 456,
"meaning": "Updated explanation",
"prompt": "What would improve this proposal?"
},
{
"name": "Abstain",
"meaning": "I leave the decision to others",
"icon": "abstain",
"priority": 2
}
]
}'
Option IDs must belong to the poll being edited. Editing requires permission to edit the poll, and closed polls cannot be edited. Anonymous polls also freeze option configuration after the first ballot is submitted.
Soft Delete Poll
Soft delete a poll as the API-key user. This discards the poll and keeps the poll record in place.
DELETE /api/user0/polls/:id
Example
curl https://www.loomio.com/api/user0/polls/123 \
-X DELETE \
-H 'Authorization: Bearer YOUR_API_KEY'
Search
Search discussions, comments, polls, votes, and outcomes visible to the API-key user. Results include public content even when the user is not a member of its group; private content remains subject to normal topic visibility.
GET /api/user0/search
Params
| Name | Description |
|---|---|
query |
Search text. Exact and fuzzy matches are supported |
group_id |
Restrict results to one visible group |
org_id |
Restrict results to a visible parent group and its visible subgroups. Use 0 for direct discussions |
type |
Restrict results to one type: Discussion, Comment, Poll, Stance, or Outcome |
types |
Comma-separated list of result types |
tag |
Restrict results to topics with this tag |
author_id |
Restrict results to content by one author. Without query, returns that author's recent visible activity |
order |
Set to authored_at_desc to order matching content by authoring time |
curl 'https://www.loomio.com/api/user0/search?query=quarterly+planning&type=Discussion' \
-H 'Authorization: Bearer YOUR_API_KEY'
The response contains a search_results array. Each result identifies the matched record and its visible context with fields including searchable_type, searchable_id, highlight, group_id, group_name, discussion_key, poll_key, author_id, author_name, authored_at, and tags. Fields that do not apply to a result are null.
Threads
List Threads
List the discussion and poll threads visible to the API-key user, ordered by latest activity. A thread ID is its topic_id.
GET /api/user0/threads
Params
| Name | Description |
|---|---|
limit |
Integer, optional, default 50. Page size |
offset |
Integer, optional, default 0. Offset for pagination |
Example
curl 'https://www.loomio.com/api/user0/threads?limit=50&offset=0' \
-H 'Authorization: Bearer YOUR_API_KEY'
Read Thread
Read a thread, its ordered event stream, or its complete visible Markdown document.
GET /api/user0/threads/:topic_id
GET /api/user0/threads/:topic_id/items
GET /api/user0/threads/:topic_id/markdown
The items endpoint accepts these pagination parameters:
| Name | Description |
|---|---|
limit |
Integer, optional, default 50, maximum 500. Page size |
offset |
Integer, optional, default 0. Offset for pagination |
Example
GET https://www.loomio.com/api/user0/threads/<topic_id>
GET https://www.loomio.com/api/user0/threads/<topic_id>/items?limit=50&offset=0
GET https://www.loomio.com/api/user0/threads/<topic_id>/markdown
The items endpoint returns a page of the event stream in ascending sequence order, including visible comments, polls, votes, and outcomes. meta.total is the full item count for the thread. Fetch subsequent pages to read the entire stream. The markdown endpoint returns the complete visible thread as one Markdown document. Vote reasons are included only when they are visible to the API-key user.
All thread endpoints enforce the same permissions as the Loomio interface. The API key does not grant access to a thread the user cannot normally open.
Webhooks
The User API is request-based: an integration calls Loomio when it wants to read or change data. A group webhook provides the push direction. Loomio sends selected group events to your endpoint as they happen, so an integration does not need to poll the REST API for changes.
Webhooks are configured per group and require group-administrator permission. They can be managed through the Loomio interface:
- Open the group.
- Open the group menu and select Chat integrations.
- Add the integration matching the payload format your endpoint accepts. For a general-purpose endpoint, use the Mattermost/Markdown format.
- Enter a name and the destination URL.
- Select the events that Loomio should send automatically.
- Save the integration and use Test connection to send a test message.
Use an HTTPS destination with an unguessable URL. Loomio requires the destination to resolve to a public address and blocks requests to local or private network addresses.
Agents and other integrations can instead manage webhooks through the Bearer-authenticated chatbot endpoints described below. The resource is named chatbots for compatibility with Loomio's chat integrations, but it also represents general outgoing webhooks.
List webhooks
Return the chat integrations configured for a group. The API-key user must be an administrator of that group. The response includes destination URLs, so it must not be exposed to ordinary group members.
GET /api/user0/chatbots?group_id=123
| Name | Description |
|---|---|
group_id |
Integer, required. ID of the group whose webhooks will be listed |
limit |
Integer, optional, default 50, maximum 500. Page size |
offset |
Integer, optional, default 0. Offset for pagination |
curl 'https://www.loomio.com/api/user0/chatbots?group_id=123&limit=50&offset=0' \
-H 'Authorization: Bearer YOUR_API_KEY'
The response contains a chatbots page ordered by ascending integration ID, with the full count for the group in meta.total. Each integration has these fields:
| Field | Description |
|---|---|
id |
Integration ID used for updates and deletion |
group_id |
Group receiving the events |
name |
Administrative name for the integration |
kind |
webhook for an outgoing webhook or matrix for a Matrix integration |
webhook_kind |
Payload format: markdown, slack, discord, microsoft, or webex |
server |
Destination URL |
event_kinds |
Events sent automatically |
notification_only |
Whether messages contain only the notification headline |
Create a webhook
POST /api/user0/chatbots
curl https://www.loomio.com/api/user0/chatbots \
-H 'Authorization: Bearer YOUR_API_KEY' \
--json '{
"group_id": 123,
"name": "Planning system",
"kind": "webhook",
"webhook_kind": "markdown",
"server": "https://hooks.example.org/loomio/unguessable-token",
"event_kinds": [
"new_discussion",
"new_comment",
"poll_created",
"outcome_created"
],
"notification_only": false
}'
The API-key user must be an administrator of group_id. The destination is validated as a public URL before it is saved.
Update a webhook
PATCH /api/user0/chatbots/:id
Send any fields that should change. The webhook cannot be transferred to another group by changing group_id.
curl https://www.loomio.com/api/user0/chatbots/456 \
-X PATCH \
-H 'Authorization: Bearer YOUR_API_KEY' \
--json '{
"name": "Planning events",
"event_kinds": [
"new_discussion",
"outcome_created"
]
}'
Test a webhook destination
Send a Markdown-compatible test message to a destination before or after saving its configuration.
POST /api/user0/chatbots/check
curl https://www.loomio.com/api/user0/chatbots/check \
-H 'Authorization: Bearer YOUR_API_KEY' \
--json '{
"group_id": 123,
"server": "https://hooks.example.org/loomio/unguessable-token"
}'
Delete a webhook
DELETE /api/user0/chatbots/:id
curl https://www.loomio.com/api/user0/chatbots/456 \
-X DELETE \
-H 'Authorization: Bearer YOUR_API_KEY'
Deleting the configuration stops future deliveries. It does not delete any Loomio group content.
Event types
A webhook can subscribe to these event types:
| Event | When it is sent |
|---|---|
new_discussion |
A discussion is started |
discussion_edited |
A discussion is edited |
new_comment |
A comment is created |
poll_created |
A poll is started |
poll_edited |
A poll is edited |
poll_closing_soon |
A poll is approaching its closing time |
poll_expired |
A poll reaches its closing time |
poll_closed_by_user |
A person closes a poll manually |
poll_reopened |
A poll is reopened |
outcome_created |
An outcome is published |
outcome_updated |
An outcome is updated |
outcome_review_due |
An outcome review becomes due |
stance_created |
A vote is cast |
stance_updated |
A vote is changed |
The webhook belongs to one group and receives subscribed events from that group. People can also explicitly select the integration when sharing or sending some notifications, even when the corresponding automatic event is not selected.
HTTP delivery
Loomio sends an asynchronous HTTP POST to the configured URL with this header:
Content-Type: application/json; charset=utf-8
The request timeout is five seconds. A 2xx response, including 204 No Content, is treated as successful. Webhook consumers should respond promptly, process longer work asynchronously, and tolerate duplicate or out-of-order deliveries.
Loomio does not currently add a webhook signature, shared-secret header, event ID, or delivery ID. Treat the full destination URL as a credential, do not expose it publicly, and include an unguessable token in the URL when the receiving service supports one. If a stable machine-readable event schema or signed delivery is required, use the webhook as a change notification and retrieve the current records through the authenticated User API.
Payload formats
Webhook payloads are presentation-oriented messages intended for chat services. They are not complete serialized Loomio records. Links in the message identify the affected Loomio content; an integration can follow up through the User API when it needs structured current state.
| Integration format | Principal JSON fields |
|---|---|
| Mattermost/Markdown | text, icon_url, username |
| Slack | text |
| Discord | content, limited to approximately 1,900 characters |
| Microsoft Teams | @type, @context, themeColor, text, sections |
| Webex | markdown |
For example, the general Markdown format sends a body shaped like:
{
"text": "Ada started a discussion: [Quarterly planning](https://example.loomio.org/d/example)",
"icon_url": "https://example.loomio.org/path/to/group-logo.png",
"username": "Loomio"
}
The exact message text depends on the event, group locale, notification-only setting, and Loomio version. Consumers should rely on the selected format's documented top-level fields rather than parsing sentence wording.