Shipnoted

Docs

Node client

The same package as the CLI. A typed client for the REST API and a webhook verifier, with no dependencies.

Install#

npm i shipnoted

Node 18 or later (it uses the built-in fetch). ESM, with TypeScript types included. Use it on a server: an API key must never reach a browser.

The client#

import { createClient } from "shipnoted"

const sn = createClient({ key: process.env.SHIPNOTED_API_KEY, project: "your-slug" })

// releases
const { release } = await sn.releases.create({ title: "Dark mode", body: "- Dark mode", tags: ["new"], publish: true, email: true })
await sn.releases.update(release.id, { version: "v1.4.0" })

// requests
const { feedback } = await sn.feedback.list({ status: "open", sort: "revenue", segment: "beta:true" })
await sn.feedback.setStatus("SN-42", "planned", "Q1")
await sn.feedback.reply("SN-42", "On it", { internal: true })
const { ref } = await sn.feedback.create({ title: "Export to CSV", email: "ada@acme.com", idempotencyKey: "ticket-8841" })

// everything else
await sn.themes(); await sn.segments(); await sn.loop()
await sn.subscribers.list({ status: "active" })
await sn.webhooks.create({ url: "https://example.com/hook" })

Every method returns the parsed JSON. The shape of each is in the API reference, and createClient also accepts url (a different host) and fetch (your own, for tests or proxies).

Errors#

import { ShipnotedError } from "shipnoted"

try {
  await sn.releases.publish("r_abc")
} catch (e) {
  if (e instanceof ShipnotedError) console.log(e.status, e.message, e.body) // 409 "That release is already published."
}

Verify webhooks#

import { verifyWebhook, WebhookVerificationError } from "shipnoted"

const event = verifyWebhook({
  secret: process.env.SHIPNOTED_WEBHOOK_SECRET, // whsec_…
  headers: req.headers,                          // a Headers object or a plain object
  body: rawBody,                                 // the raw string or Buffer, not parsed JSON
})
// → { id, type, created, data } or throws WebhookVerificationError

It checks the signature in constant time, accepts any one of several signatures (so you can rotate secrets), and refuses events more than five minutes old (toleranceSec changes that). Full guide: Webhooks.

Testing your handler#

import { signWebhook } from "shipnoted"

const body = JSON.stringify({ id: "evt_1", type: "feedback.created", created: 1, data: { title: "Hi" } })
const headers = signWebhook({ secret, id: "wd_1", body })
const res = await app.request("/hooks/shipnoted", { method: "POST", headers, body })