Shipnoted

Docs

API reference

Base address https://shipnoted.com/api/v1. Every request needs a key; each endpoint lists the scope it needs.

Project#

get/api/v1/projects

List projects

Scope: read

The projects this key can use. A quick way to find the slug.

curl "https://shipnoted.com/api/v1/projects" \
  -H "Authorization: Bearer $SHIPNOTED_API_KEY"

Returns 200: Projects.

{
  "ok": true,
  "projects": [
    {
      "slug": "acme",
      "name": "Acme",
      "url": "https://acme.shipnoted.com",
      "live": true
    }
  ]
}
get/api/v1/{slug}/project

Get the project

Scope: read

Names, addresses and counts. The first call a new tool makes, to check its key and slug.

curl "https://shipnoted.com/api/v1/your-slug/project" \
  -H "Authorization: Bearer $SHIPNOTED_API_KEY"

Returns 200: Project.

get/api/v1/{slug}/themes

Requests grouped into themes

Scope: read

The same wish in different words, grouped, ranked by revenue then votes.

curl "https://shipnoted.com/api/v1/your-slug/themes" \
  -H "Authorization: Bearer $SHIPNOTED_API_KEY"

Returns 200: Themes.

get/api/v1/{slug}/segments

Customer groups that have voted

Scope: read

The values ?segment= accepts, with votes and revenue.

curl "https://shipnoted.com/api/v1/your-slug/segments" \
  -H "Authorization: Bearer $SHIPNOTED_API_KEY"

Returns 200: Segments.

get/api/v1/{slug}/loop

The loop in numbers

Scope: read

Requests received and shipped, median days to ship, people told, and pull requests waiting for you.

curl "https://shipnoted.com/api/v1/your-slug/loop" \
  -H "Authorization: Bearer $SHIPNOTED_API_KEY"

Returns 200: Stats and suggestions.

Releases#

get/api/v1/{slug}/releases

List releases

Scope: read

Query

  • statusdraft, scheduled or published
  • limit1 to 100, default 50
  • offsetSkip this many
curl "https://shipnoted.com/api/v1/your-slug/releases" \
  -H "Authorization: Bearer $SHIPNOTED_API_KEY"

Returns 200: Releases, newest first, with hasMore.

post/api/v1/{slug}/releases

Create a release

Scope: releases:write

Add publish: true to publish at once (and email: true to email confirmed subscribers). Without it you get a draft.

Body (JSON)

  • titlerequiredstring
  • bodystring · Markdown
  • versionstring
  • tags"new" | "improved" | "fixed" | "breaking"[]
  • coverstring · An image address
  • audiencestring · Aim it at a group: "plan:pro", "beta:true". Empty is everyone
  • announceboolean · Pop it up in the widget
  • linkedstring[]
curl -X POST "https://shipnoted.com/api/v1/your-slug/releases" \
  -H "Authorization: Bearer $SHIPNOTED_API_KEY" \
  -H "content-type: application/json" \
  -d '{"title":"Dark mode","body":"- Dark mode for the dashboard","version":"v1.4.0","tags":["new"],"publish":true,"email":true}'

Returns 201: The release.

{
  "ok": true,
  "release": {
    "id": "r_abc",
    "title": "Dark mode",
    "status": "published",
    "url": "https://acme.shipnoted.com/p/acme/dark-mode"
  },
  "emailed": 212
}
get/api/v1/{slug}/releases/{id}

Get a release

Scope: read

Includes the requests it answers.

curl "https://shipnoted.com/api/v1/your-slug/releases/r_abc" \
  -H "Authorization: Bearer $SHIPNOTED_API_KEY"

Returns 200: The release with linked requests.

patch/api/v1/{slug}/releases/{id}

Update a release

Scope: releases:write

Any of the create fields. A published release changes in place.

Body (JSON)

  • titlestring
  • bodystring · Markdown
  • versionstring
  • tags"new" | "improved" | "fixed" | "breaking"[]
  • coverstring · An image address
  • audiencestring · Aim it at a group: "plan:pro", "beta:true". Empty is everyone
  • announceboolean · Pop it up in the widget
  • linkedstring[]
curl -X PATCH "https://shipnoted.com/api/v1/your-slug/releases/r_abc" \
  -H "Authorization: Bearer $SHIPNOTED_API_KEY"

Returns 200: The release.

delete/api/v1/{slug}/releases/{id}

Delete a release

Scope: releases:write

curl -X DELETE "https://shipnoted.com/api/v1/your-slug/releases/r_abc" \
  -H "Authorization: Bearer $SHIPNOTED_API_KEY"

Returns 200: ok.

post/api/v1/{slug}/releases/{id}/publish

Publish a draft

Scope: releases:write

Linked requests become Shipped and the people who voted for them are emailed, exactly as from the dashboard.

Body (JSON)

  • emailboolean
curl -X POST "https://shipnoted.com/api/v1/your-slug/releases/r_abc/publish" \
  -H "Authorization: Bearer $SHIPNOTED_API_KEY"

Returns 200: The public url and how many were told.

{
  "ok": true,
  "url": "https://acme.shipnoted.com/p/acme/dark-mode",
  "emailed": 212,
  "votersNotified": 14,
  "requestsShipped": 3
}
post/api/v1/{slug}/draft

Draft release notes from changes

Scope: releases:write

Send pull request titles or commit subjects; Claude (or the built-in writer) groups them into New, Improved and Fixed. The draft is never published for you.

Body (JSON)

  • itemsrequiredobject[]
  • versionstring · Set as the version, such as a tag name
  • titlestring
curl -X POST "https://shipnoted.com/api/v1/your-slug/draft" \
  -H "Authorization: Bearer $SHIPNOTED_API_KEY" \
  -H "content-type: application/json" \
  -d '{"version":"v1.4.0","items":[{"title":"feat: dark mode (#212)"},{"title":"fix: login loop (#215)"}]}'

Returns 201: The draft's id and who wrote it (claude or builtin).

{
  "ok": true,
  "releaseId": "r_abc",
  "source": "claude"
}

Requests#

get/api/v1/{slug}/feedback

List requests

Scope: read

Query

  • statusopen, planned, progress, shipped or declined
  • qSearch words, or a reference like SN-42
  • sorttop, revenue or new
  • planOnly requests a customer on this plan voted for
  • segmentA group: "beta:true", "company:Acme"
  • limit1 to 100
  • offsetSkip this many
curl "https://shipnoted.com/api/v1/your-slug/feedback" \
  -H "Authorization: Bearer $SHIPNOTED_API_KEY"

Returns 200: Requests, with total and hasMore.

post/api/v1/{slug}/feedback

File a request, or move one

Scope: feedback:write

With a title it files a new request (send idempotencyKey so a retry files it once). With an id and a status it moves an existing request.

Body (JSON)

  • titlestring
  • bodystring
  • namestring
  • emailstring
  • sourcestring · Where it came from, such as Intercom
  • idempotencyKeystring
  • idstring · Instead of a title: the request to move
  • status"open" | "planned" | "progress" | "shipped" | "declined"
  • etastring
curl -X POST "https://shipnoted.com/api/v1/your-slug/feedback" \
  -H "Authorization: Bearer $SHIPNOTED_API_KEY" \
  -H "content-type: application/json" \
  -d '{"title":"Export to CSV","body":"We need it for invoices","email":"ada@acme.com","source":"Support","idempotencyKey":"ticket-8841"}'

Returns 201: The new request's id and reference, or ok.

{
  "ok": true,
  "id": "f_x",
  "ref": "SN-42",
  "duplicate": false
}
get/api/v1/{slug}/feedback/{id}

Get a request

Scope: read

The id may be its reference, like SN-42. Includes comments and linked GitHub issues.

curl "https://shipnoted.com/api/v1/your-slug/feedback/SN-42" \
  -H "Authorization: Bearer $SHIPNOTED_API_KEY"

Returns 200: The request.

patch/api/v1/{slug}/feedback/{id}

Change a request

Scope: feedback:write

Status and timing, who it is assigned to, and whether it is archived. An archived request leaves the inbox and the public board but keeps its votes.

Body (JSON)

  • status"open" | "planned" | "progress" | "shipped" | "declined"
  • etastring
  • assigneestring · A teammate's email, or an empty string to unassign
  • archivedboolean
curl -X PATCH "https://shipnoted.com/api/v1/your-slug/feedback/SN-42" \
  -H "Authorization: Bearer $SHIPNOTED_API_KEY"

Returns 200: The request.

delete/api/v1/{slug}/feedback/{id}

Delete a request

Scope: feedback:write

curl -X DELETE "https://shipnoted.com/api/v1/your-slug/feedback/SN-42" \
  -H "Authorization: Bearer $SHIPNOTED_API_KEY"

Returns 200: ok.

post/api/v1/{slug}/feedback/{id}/comments

Reply as the team

Scope: feedback:write

Set internal: true for a note only your team sees.

Body (JSON)

  • bodyrequiredstring
  • namestring
  • internalboolean
curl -X POST "https://shipnoted.com/api/v1/your-slug/feedback/SN-42/comments" \
  -H "Authorization: Bearer $SHIPNOTED_API_KEY" \
  -H "content-type: application/json" \
  -d '{"body":"…"}'

Returns 201: The comment.

Subscribers#

get/api/v1/{slug}/subscribers

List subscribers

Scope: subscribers:read

Email addresses are personal data, so this has its own scope.

Query

  • statusactive, pending or unsubscribed
  • limit1 to 500, default 100
  • offsetSkip this many
curl "https://shipnoted.com/api/v1/your-slug/subscribers" \
  -H "Authorization: Bearer $SHIPNOTED_API_KEY"

Returns 200: Subscribers and total.

Webhook endpoints#

get/api/v1/{slug}/webhooks

List webhook endpoints

Scope: webhooks:write

curl "https://shipnoted.com/api/v1/your-slug/webhooks" \
  -H "Authorization: Bearer $SHIPNOTED_API_KEY"

Returns 200: Endpoints and the event catalogue.

post/api/v1/{slug}/webhooks

Create a webhook endpoint

Scope: webhooks:write

Events: feedback.created, feedback.status_changed, comment.created, release.published, feedback.voted, subscriber.confirmed. All of them when events is left out. The signing secret is in this response and never shown again.

Body (JSON)

  • urlrequiredstring · An https address
  • eventsstring[]
curl -X POST "https://shipnoted.com/api/v1/your-slug/webhooks" \
  -H "Authorization: Bearer $SHIPNOTED_API_KEY" \
  -H "content-type: application/json" \
  -d '{"url":"https://example.com/hooks/shipnoted","events":["feedback.created","release.published"]}'

Returns 201: The endpoint and its secret.

{
  "ok": true,
  "endpoint": {
    "id": "we_abc",
    "url": "https://example.com/hooks/shipnoted",
    "active": true
  },
  "secret": "whsec_…"
}
patch/api/v1/{slug}/webhooks/{id}

Pause, resume or change an endpoint

Scope: webhooks:write

Body (JSON)

  • activeboolean
  • eventsstring[]
curl -X PATCH "https://shipnoted.com/api/v1/your-slug/webhooks/we_abc" \
  -H "Authorization: Bearer $SHIPNOTED_API_KEY"

Returns 200: The endpoint.

delete/api/v1/{slug}/webhooks/{id}

Delete an endpoint

Scope: webhooks:write

curl -X DELETE "https://shipnoted.com/api/v1/your-slug/webhooks/we_abc" \
  -H "Authorization: Bearer $SHIPNOTED_API_KEY"

Returns 200: ok.

post/api/v1/{slug}/webhooks/{id}/test

Send a test ping

Scope: webhooks:write

A signed ping event, sent now. Reports what your server answered.

curl -X POST "https://shipnoted.com/api/v1/your-slug/webhooks/we_abc/test" \
  -H "Authorization: Bearer $SHIPNOTED_API_KEY"

Returns 200: ok and a note.

Errors#

Every error is { "ok": false, "error": "…" } with one of these statuses:

  • 400 A field is missing or not valid. The error says which.
  • 401 No key, a wrong key, or an expired key.
  • 402 The workspace is not on a plan with API access.
  • 403 The key is valid but lacks the scope this endpoint needs. The response names it.
  • 404 No such project, release, request or endpoint, or the key is limited to other projects.
  • 409 Already done, for example publishing a release that is published.
  • 429 More than 120 requests a minute. Wait the Retry-After seconds.