# Work with Notes through the Buffer API

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:

```json
{ "postId": "YOUR_POST_ID" }
```

```json
{ "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:

```graphql
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 }
  }
}
```

```json
{
  "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:

```graphql
mutation CreateNote($input: CreateCollaborationNoteInput!) {
  createCollaborationNote(input: $input) {
    __typename
    ... on CreateCollaborationNoteSuccess {
      edge { cursor node { id text allowedActions } }
    }
    ... on MutationError { message }
  }
}
```

```json
{
  "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.

## 3. Update a Note

Run this mutation with the variables below:

```graphql
mutation UpdateNote($input: UpdateCollaborationNoteInput!) {
  updateCollaborationNote(input: $input) {
    __typename
    ... on UpdateCollaborationNoteSuccess { note { id text allowedActions } }
    ... on MutationError { message }
  }
}
```

```json
{
  "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:

```graphql
mutation DeleteNote($input: DeleteCollaborationNoteInput!) {
  deleteCollaborationNote(input: $input) {
    __typename
    ... on DeleteCollaborationNoteSuccess { noteId }
    ... on MutationError { message }
  }
}
```

```json
{ "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_SCOPE` in top-level `errors`. 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 `message` through `MutationError`. Creation can return `InvalidInputError`, `LimitReachedError`, or `CollaborationNoteThreadClosedError`; update can return `InvalidInputError`.
- A closed thread accepts no new Notes, but existing Notes remain readable.
- Respect API rate limits and the `Retry-After` header. 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. A successful create response is not confirmation that an email was sent.
