sprint-sync
v0.1.0
Published
Official Node.js SDK for the Sprint Sync API — create and query tasks, sprints, epics and QA checklists, and verify webhook signatures.
Maintainers
Readme
sprint-sync
Official Node.js SDK for the Sprint Sync API.
npm install sprint-syncGet a key from Sprint Sync → Settings → API Keys. Node 18+.
Quick start
import SprintSync from 'sprint-sync';
const ss = new SprintSync({ apiKey: process.env.SPRINTSYNC_API_KEY });
await ss.tasks.create({
projectKey: 'EMPL',
name: 'Add rate limiting to auth endpoints',
priority: 'Critical',
components: ['Infra'],
});Names, not ids
You pass 'High' and 'Frontend', not priority_id: 3. The API resolves them
inside your organization.
Those names are configured per organization, so check what yours accepts:
const config = await ss.projects.config('EMPL');
config.priorities; // ['Critical', 'High', 'Medium', 'Low', 'Enhancement']
config.statuses; // [{ name: 'Todo', category: 'todo' }, …]
config.components; // ['Core Platform', 'Infra', …]Send a name that doesn't exist and you get a 400 listing the ones that do.
Tasks
await ss.tasks.list('EMPL', { limit: 20 });
await ss.tasks.list('EMPL', { sprintId: 12 });
const { task_id } = await ss.tasks.create({ projectKey: 'EMPL', name: 'Fix login' });
await ss.tasks.update(task_id, { status: 'Done' });update leaves out what you leave out. The one exception is components, which
replaces the whole set — send every component the task should end up with, or
[] to clear them.
Importing a whole plan
Creating twenty related items one call at a time is slow, and a failure halfway
leaves an orphaned epic and half a sprint for someone to clean up by hand.
plans.import does it in one transaction: all of it lands or none of it does.
const plan = {
project_key: 'EMPL',
epic: { name: 'Checkout redesign' },
sprints: [{
name: 'Sprint 12',
start_date: '2026-09-08',
end_date: '2026-09-19',
tasks: [
{
name: 'Split CartSummary',
priority: 'High',
story_points: 3,
components: ['Frontend'],
subtasks: [{ name: 'Extract useCartTotals' }],
},
{ name: 'Payment intent expires mid-checkout', issue_type: 'bug', priority: 'Critical' },
],
}],
qa_checklist: {
testing_type: 'Regression',
flows: [{
name: 'Guest checkout',
steps: [{ name: 'Pay by card', checks: ['3DS challenge appears', 'Receipt email sends'] }],
}],
},
};
// See what it would create, without creating it
const { would_create } = await ss.plans.preview(plan);
// Commit
const result = await ss.plans.import(plan, { idempotencyKey: 'checkout-redesign-v1' });Pass an idempotencyKey if the call might be retried — a request that times out
after the server already committed would otherwise file the whole breakdown
twice. The same key with the same body replays the original response. The same
key with a different body is a 409, because that's a bug rather than a retry.
Receiving webhooks
Sprint Sync signs every delivery. Verify before you trust it:
import express from 'express';
import { constructEvent, EVENTS } from 'sprint-sync';
const app = express();
// express.raw, NOT express.json — see below
app.post('/hooks/sprintsync', express.raw({ type: 'application/json' }), (req, res) => {
let event;
try {
event = constructEvent({
secret: process.env.SPRINTSYNC_WEBHOOK_SECRET,
rawBody: req.body,
signature: req.get('X-SprintSync-Signature'),
});
} catch {
return res.sendStatus(401); // forged or misconfigured
}
if (event.event === EVENTS.TASK_STATUS_CHANGED) {
console.log(event.data.task_name, '→', event.data.new_status_id);
}
res.sendStatus(200); // ack fast; do the work after
});The raw body matters. The signature covers the exact bytes sent. Parsing to an object and re-stringifying changes key order and whitespace, and it will never match. This is the single most common reason verification "doesn't work".
verifySignature is available if you want a boolean instead of an exception.
Respond quickly. Sprint Sync retries 5xx three times with backoff and gives up on 4xx, so slow handlers get retried and duplicated.
Errors
Everything throws SprintSyncError:
import { SprintSyncError } from 'sprint-sync';
try {
await ss.tasks.create({ projectKey: 'EMPL', name: 'x', priority: 'Urgent' });
} catch (err) {
err.code; // 'validation'
err.status; // 400
err.message; // 'Unknown priority "Urgent". Available: Critical, High, …'
err.details; // per-item messages, when the endpoint returns them
err.retryable; // false
}Codes: auth, forbidden, not_found, validation, rate_limit, conflict,
server, network.
429, 5xx and network failures are retried twice automatically, honouring
Retry-After. A 400 is never retried — the same payload would fail again.
What a key cannot do
A key acts as whoever created it and can never exceed their permissions. On top of that, some things are closed to every key at any scope:
- Creating, changing or deleting organizations and projects
- Billing, ownership transfer, roles and membership
- Creating other keys, or re-pointing webhooks
A leaked key can make a mess of your tasks. It cannot take your account.
Keys can also be limited to specific projects, which is worth doing for anything running unattended in CI.
Options
new SprintSync({
apiKey: '…', // or SPRINTSYNC_API_KEY
baseUrl: '…', // or SPRINTSYNC_API_URL, for self-hosted
timeout: 30000,
maxRetries: 2,
});Also available
@sprintsync/mcp — connects
Sprint Sync to Claude Code, Cursor and other AI tools, so an assistant can read
your board and file work onto it directly.
Licence
MIT
