Notes
Use Notes to add feedback, review comments, or internal context to a post or channel-less draft in Buffer. You can list, create, update, and delete Notes. These operations are experimental.
Before you start
You need a Buffer account, a personal API key or OAuth access token, and access to the post or channel-less draft you want to work with. For an app used by other people, use OAuth so each person connects their own Buffer account.
Request notes:read to list Notes and notes:write to create, update, or delete them. Request both for the complete workflow. These permissions do not replace the connected person's organization, channel, or role permissions.
Existing tokens do not gain Notes access automatically. Create a new personal key, or update your app client's permitted scopes and ask the person to authorize again with the Notes scopes. Other operations, such as finding posts or content items, require their own scopes.
Choose a thread
A Note belongs to exactly one subject. Use one of these objects:
{ "postId": "YOUR_POST_ID" }
{ "contentItemId": "YOUR_CONTENT_ITEM_ID" }
Use contentItemId for a channel-less draft's own thread. This is a ContentItem ID, not an Idea-board ID. A ContentItem's thread is separate from the threads on its channel-specific Posts. Provide exactly one subject field, with a non-null value. After a channel-less draft becomes channel-specific Posts, its original thread is closed to new Notes; use a Post's thread for new feedback.
1. List Notes
Run this query with the variables below:
query ListNotes($input: CollaborationNotesInput!, $first: Int = 10, $after: String) {
collaborationNotes(input: $input, first: $first, after: $after) {
edges {
cursor
node {
id
text
annotations { type text content indices url displayValue serviceEntityId }
authorId
author { id name }
allowedActions
createdAt
updatedAt
}
}
pageInfo { hasNextPage endCursor }
}
}
{
"input": { "subject": { "postId": "YOUR_POST_ID" } },
"first": 10,
"after": null
}
Notes are returned newest first. A page defaults to 10 Notes and accepts 1–100. When pageInfo.hasNextPage is true, pass pageInfo.endCursor as after to fetch the next page. Treat cursors as opaque strings.
The author can be null if their account was deleted. authorId remains available. Only request profile fields your app needs; the examples use ID and name.
2. Create a Note
Run this mutation with the variables below:
mutation CreateNote($input: CreateCollaborationNoteInput!) {
createCollaborationNote(input: $input) {
__typename
... on CreateCollaborationNoteSuccess {
edge { cursor node { id text allowedActions } }
}
... on MutationError { message }
}
}
{
"input": {
"subject": { "postId": "YOUR_POST_ID" },
"text": "Please review the revised caption.",
"annotations": []
}
}
A successful response has __typename: CreateCollaborationNoteSuccess. Save edge.node.id for later edits or deletion. Creating a Note does not publish the post.
For a channel-less draft, replace the subject with { "contentItemId": "YOUR_CONTENT_ITEM_ID" }. The rest of the request stays the same.
Add a mention
To include an optional mention, request both notes:read and notes:write. First, find recipients with selectableTeamMembers. For a Post, use its channelId field. Looking up the Post through post or posts requires posts:read in addition to the Notes scopes:
query FindNoteRecipients($input: SelectableTeamMembersInput!) {
selectableTeamMembers(input: $input, first: 20) {
edges { node { id urn name email } }
pageInfo { hasNextPage endCursor }
}
}
{
"input": {
"scope": { "channelId": "YOUR_POST_CHANNEL_ID" },
"search": "alex"
}
}
For a channel-less draft, use { "organizationId": "YOUR_ORGANIZATION_ID" } as the scope. See Team Members for pagination, search and access rules. Let the person choose a result; do not assume a name or email search identifies exactly one account.
Use the selected result's node.urn as serviceEntityId in the create mutation from step 2. For example, if the person selects Alex, send these variables:
{
"input": {
"subject": { "postId": "YOUR_POST_ID" },
"text": "@Alex please review the revised caption.",
"annotations": [
{
"type": "mention",
"serviceEntityId": "SELECTED_MEMBER_URN",
"content": "@Alex",
"text": "@Alex",
"indices": [0, 5]
}
]
}
}
Replace SELECTED_MEMBER_URN with the complete returned URN, such as urn:buffer:account:507f1f77bcf86cd799439011. For a channel-less draft, replace the subject with { "contentItemId": "YOUR_CONTENT_ITEM_ID" }.
Typing @Alex in the text alone does not create a mention. The annotation identifies the account. indices contains the zero-based start and exclusive end of the mention in the full Note text: [0, 5] covers @Alex. Count UTF-16 code units, as JavaScript string offsets do; some emoji take two units. content must equal the exact text covered by indices, or the write is rejected. The annotation's text must be non-empty; this example uses the same mention label for both fields. Recalculate the offsets if you change the text or mention label.
Buffer rechecks every recipient's access on create and update. If any recipient cannot read the subject's Notes, the entire write returns InvalidInputError without saving a partial Note. Choose a different recipient or remove the rejected mention. A refreshed picker can return the same member: organization scope does not check access to each specific subject. Mentions use the same notification behavior as other Notes; a successful write does not guarantee an email.
3. Update a Note
Run this mutation with the variables below:
mutation UpdateNote($input: UpdateCollaborationNoteInput!) {
updateCollaborationNote(input: $input) {
__typename
... on UpdateCollaborationNoteSuccess { note { id text allowedActions } }
... on MutationError { message }
}
}
{
"input": {
"noteId": "YOUR_NOTE_ID",
"text": "The revised caption is ready for review.",
"annotations": []
}
}
An update replaces the Note's text and annotations. Omitting annotations, passing null, or passing an empty array clears existing annotations. When editing formatted content, send annotations that match the replacement text; copying old character positions after changing text can make them invalid.
Use updateNote and deleteNote in the Note's allowedActions response to decide whether to show edit or delete controls. The API checks permissions again when the request runs.
4. Delete a Note
Run this mutation with the variables below:
mutation DeleteNote($input: DeleteCollaborationNoteInput!) {
deleteCollaborationNote(input: $input) {
__typename
... on DeleteCollaborationNoteSuccess { noteId }
... on MutationError { message }
}
}
{ "input": { "noteId": "YOUR_NOTE_ID" } }
Ask the person to confirm before deleting. Deletion is permanent. A successful response has __typename: DeleteCollaborationNoteSuccess and the deleted noteId. Listing the thread again should no longer return that Note.
Handle errors
Check both top-level GraphQL errors and the mutation result's __typename. HTTP 200 alone does not mean the mutation succeeded.
- Missing token permissions can return
INSUFFICIENT_SCOPEin top-levelerrors. Request the required scope and authorize again; retrying with the same token will not add access. - A mutation can return a typed error instead of its success type. The examples select
messagethroughMutationError. Creation can returnInvalidInputError,LimitReachedError, orCollaborationNoteThreadClosedError; update can returnInvalidInputError. - A closed thread accepts no new Notes, but existing Notes remain readable.
- Respect API rate limits and the
Retry-Afterheader. Do not blindly retry a create request after a network timeout: it may already have saved the Note.
Notifications
Notes created through the API use Buffer's existing batched notification flow. Eligible activity is grouped by recipient and organization in 30-minute windows. Delivery depends on notification preferences, access, read state, and other eligibility checks. Locked organization members receive no Notes notifications while locked, even when they can read Notes and be mentioned. A successful create response is not confirmation that an email was sent.