TypeScript SDK
Official Node/TypeScript client for the ProjeX API. Use it from scripts, CI jobs, and backend services to discover live cohorts, create milestone or task submissions, and administer cohorts, teams and their members — all with the same rules as the web app.
Example apps
Start here if you want a runnable Next.js app instead of copying snippets. Set PROJEX_API_URL and PROJEX_API_KEY from Account → API keys, then clone the sample that matches how you integrate.
Example apps
Clone these Next.js samples to try the same flow as Account → API keys: live cohorts → milestones/tasks → submit (with optional file attachments).
NEXT_PUBLIC_SUBMISSION_API_EXAMPLE_REPO and NEXT_PUBLIC_SUBMISSION_SDK_EXAMPLE_REPO.Overview
@pkg-projex/sdk is a thin, typed wrapper over the public REST API at /api/v1. Each SDK method maps to exactly one HTTP request. There is no separate business logic in the client — validation, submission windows, team resolution, and authorization are enforced on the server.
Typical flow: authenticate with a personal API key → list the caller's live cohorts → load milestones or tasks → submit (or resubmit) work. The package is designed for server-side and automation environments (Node 18+, Bun, Deno, edge workers). Do not embed API keys in browser bundles or public frontends.
Installation
Install from npm under the @pkg-projex scope. The package is ESM-first (type: module) and ships TypeScript declarations.
npm install @pkg-projex/sdk # or pnpm add @pkg-projex/sdk # or yarn add @pkg-projex/sdk
Runtime requirements
| Requirement | Details |
|---|---|
| fetch | Must be available globally (Node 18+, modern browsers, edge runtimes). On older Node, pass a fetch polyfill via the fetch client option. |
| Module system | ESM import. CommonJS consumers should use dynamic import() or a bundler. |
| Secrets | Store PROJEX_API_KEY in environment variables or a secrets manager — never commit keys to source control. |
Authentication & scopes
Every request is authenticated as the user who owns the API key. Generate a key in Account → API keys. The plaintext secret is shown once at creation and looks like pjx_live_….
The SDK attaches Authorization: Bearer <apiKey> on every call (equivalent to the x-api-key header for raw HTTP). Scopes on the key can only narrow what that user could already do in the product — they never escalate privileges. Ownership and deadline checks still apply in the service layer.
Available scopes
| Scope | Grants access to | SDK methods |
|---|---|---|
cohorts:read | Discover cohorts the key owner is enrolled in, including run status (planned / live / closed). | cohorts.list, live, get |
milestones:read | List milestones and tasks inside a cohort the user can access. | cohorts.milestones, cohorts.tasks |
submissions:read | Read submissions previously created by the key owner for a milestone or task. | milestones.submissions, tasks.submissions |
submissions:write | Create new submissions and request presigned upload URLs. | milestones.submit, tasks.submit, uploads.presign |
cohorts:write | Create, update and soft-delete cohorts. | cohorts.create, update, delete |
teams:read | List the teams inside a cohort. | teams.list |
teams:write | Create, update and soft-delete teams. | teams.create, update, delete |
cohort-members:write | Enrol, re-role and remove cohort members. | cohorts.members.* |
team-members:write | Add, re-role and remove a team’s learners and staff. | teams.members.* |
403 insufficient_scope. Revoking the key in Account immediately invalidates all subsequent SDK calls. For the five administration scopes the scope is only half the check — the key owner must also hold the matching role in that specific cohort or team. A manager of cohort A can do nothing in cohort B, and a learner’s key with cohorts:write still creates nothing.Configuration
Construct a single ProjexClient and reuse it. Configuration resolves in this order for each option: constructor argument → environment variable → built-in default. An API key is mandatory (via argument or PROJEX_API_KEY); the client throws at construction time if neither is set.
| Option | Type | Env / default | Description |
|---|---|---|---|
apiKey | string | PROJEX_API_KEY (required) | Personal access token. Sent as a Bearer credential. Never log or expose this value. |
baseUrl | string | PROJEX_API_URL, else https://projex.xceleratordemo.in | Application origin only (scheme + host, optional port). Do not include /api/v1 — the SDK appends it. |
timeoutMs | number | 30000 | Abort the underlying fetch if the server does not respond within this many milliseconds. |
maxRetries | number | 2 | Additional attempts after the first failure for HTTP 429 and 5xx responses. Uses exponential backoff and respects Retry-After when present. |
fetch | typeof fetch | globalThis.fetch | Inject a custom fetch implementation for tests or older runtimes. |
Examples
import { ProjexClient } from '@pkg-projex/sdk';
// Recommended for CI / local scripts: configure via env only
const projex = new ProjexClient();
// Explicit overrides (win over env)
const staging = new ProjexClient({
apiKey: process.env.PROJEX_API_KEY!,
baseUrl: 'https://staging.example.com',
timeoutMs: 45_000,
maxRetries: 3,
});# .env / shell # Local ProjeX app/API origin export PROJEX_API_URL="http://localhost:3000" export PROJEX_API_KEY="pjx_live_…"
Quickstart
End-to-end example: find a live cohort, pick the first milestone with a public ref, and create a submission. Adjust error handling for production use.
import { ProjexClient, ProjexApiError } from '@pkg-projex/sdk';
const projex = new ProjexClient();
try {
// List methods return a page: { data, nextCursor }.
const live = (await projex.cohorts.live()).data;
if (!live.length) {
throw new Error('No live cohorts for this account');
}
const milestones = (await projex.cohorts.milestones(live[0].id)).data;
const milestone = milestones.find((m) => m.ref) ?? milestones[0];
if (!milestone) {
throw new Error('No milestones in cohort');
}
// Prefer a full T{team}M{ms} ref when available from your workflow.
const ref = milestone.ref ?? milestone.id;
const { submission } = await projex.milestones.submit(ref, {
title: 'Sprint deliverable',
submission: 'https://github.com/org/repo/pull/42',
});
console.log('Created submission', submission.id);
} catch (err) {
if (err instanceof ProjexApiError) {
console.error(err.status, err.code, err.message, err.requestId);
}
throw err;
}Cohorts
Cohort methods discover which programs the authenticated user belongs to and what they can work on. List endpoints only return cohorts where the user is a team member. Milestone and task listing additionally require the milestones:read scope.
Cohort status is derived server-side from start/end dates: planned (not started), live (currently open), closed (ended). Most submission workflows should start with cohorts.live().
projex.cohorts.list(opts?)
Returns every cohort the API key owner is enrolled in, optionally filtered by run status. Use this when you need planned or closed cohorts as well as live ones (for example reporting). For day-to-day submission scripts, prefer live().
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
opts.status | 'planned' | 'live' | 'closed' | No | When set, only cohorts in that phase are returned. When omitted, all enrolled cohorts are returned. |
opts.q | string | No | Free-text search over name/id/type. |
opts.limit | number | No | Page size (default 50, max 100). |
opts.cursor | string | No | Opaque cursor from a previous page’s nextCursor. |
Returns
Promise<Page<Cohort>> — { data, nextCursor }. Iterate every page with projex.cohorts.iterate(opts?).
type Page<T> = { data: T[]; nextCursor: string | null };
type Cohort = {
id: string; // Stable cohort UUID — use with get / milestones / tasks
name: string; // Display name
type: string | null; // Cohort type when configured
visibility: string | null; // Visibility setting when configured
startDate: string | null; // ISO-8601 UTC
endDate: string | null; // ISO-8601 UTC
status: 'planned' | 'live' | 'closed';
};projex.cohorts.live()
Convenience helper equivalent to list({ status: 'live' }). Returns only cohorts that are currently open for work. This is the recommended entry point for submission automations.
Parameters
Accepts the same q / limit / cursor options as list.
Returns
Promise<Page<Cohort>> — same shape as list.
projex.cohorts.get(cohortId)
Loads a single cohort the user belongs to, including the teams they are a member of inside that cohort. Use the teams array when you need an explicit teamId for milestone submission (required only if the user is on more than one team).
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
cohortId | string | Yes | Cohort UUID from list or live. Returns 404 if the cohort does not exist or the user is not a member. |
Returns
type CohortDetail = Cohort & {
teams: Array<{
id: string; // Pass as teamId on milestone submit when needed
publicId: number; // Numeric team public id (used in T{{team}}M{{ms}} refs)
name: string;
}>;
};projex.cohorts.milestones(cohortId)
Lists milestones for the cohort, ordered by their configured sequence. Milestones are global to the cohort — the team is only resolved at submit time. Each item’s ref is directly submittable (a fully-qualified T{team}M{ms} when you’re on one team, else the milestone id) — pass it straight to milestones.submit.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
cohortId | string | Yes | Cohort UUID. Requires cohorts:read and milestones:read. |
opts | { q?, limit?, cursor? } | No | Search + pagination (same as list). |
Returns
Promise<Page<Milestone>> — walk pages with projex.cohorts.iterateMilestones(cohortId).
type Milestone = {
id: string;
ref: string; // Directly submittable (e.g. "T12M3" or the id)
publicId: number | null;
title: string;
description: string | null;
order: number | null;
startDate: string | null; // Submission window start (ISO-8601)
endDate: string | null; // Submission window end (ISO-8601)
submissionType: string | null;
};projex.cohorts.tasks(cohortId, opts?)
Lists tasks in the cohort. Tasks are team-scoped: you get the tasks of the team(s) you belong to in that cohort (multi-team users get all their teams’). Task refs use the TS{n} form (e.g. TS42) for tasks.submit.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
cohortId | string | Yes | Cohort UUID. |
opts.milestoneId | string | No | Only tasks belonging to this milestone id. |
opts.assignee | 'me' | 'others' | No | 'me' = assigned to you; 'others' = assigned to teammates. Omit for all team tasks. |
opts | { q?, limit?, cursor? } | No | Search + pagination (same as list). |
Returns
Promise<Page<Task>> — walk pages with projex.cohorts.iterateTasks(cohortId, opts?).
type Task = {
id: string;
ref: string; // e.g. "TS42" — use with tasks.submit
publicId: number;
title: string;
description: string | null;
status: string;
priority: string;
milestoneId: string | null;
deadline: string | null; // ISO-8601 when set
};Milestones
Milestone submissions are attributed to a team in the cohort (not only the individual). The server auto-selects the user's team when they belong to exactly one; if they belong to multiple teams you must pass teamId or the API returns 409 ambiguous_team.
Submissions are append-only: each successful submit inserts a new row. There is no PATCH. Resubmitting before the deadline is another POST; the latest by createdAt is treated as current. Optional replacesSubmissionId records lineage for UI without deleting history. Active redo requests are completed automatically when you submit.
submissions:write. Listing needs submissions:read. Outside the allowed window (before start, after end/redo deadline, or cohort closed) the API responds with 422 validation_error.projex.milestones.submit(ref, input)
Creates a milestone submission (or revision). projex.milestones.resubmit is an alias of the same method for readability in calling code.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
ref | string | Yes | Milestone reference such as T12M3, or the internal milestone UUID when accepted by the API. |
input.title | string | Yes | Human-readable title for this submission revision. Must be non-empty. |
input.submission | string | Yes | Primary payload: typically a URL (GitHub PR, Figma, demo) or markdown/plain-text body. Must be non-empty. |
input.teamId | string | Conditional | Team UUID. Required when the user is on more than one team for this cohort/milestone. Omit for the common single-team case. |
input.contributionAreas | array | No | Optional breakdown of contribution across teammates. See schema below. |
input.reflectionAnswers | array | No | Answers to reflection questions configured on the milestone: { questionId, answer }[]. |
input.replacesSubmissionId | string | No | Id of a previous submission this revision supersedes. Metadata only — prior rows are retained. |
contributionAreas item
{
contributionAreaId?: string; // Existing catalog area id
customAreaName?: string; // Free-text area when not using catalog
breakdown: Array<{
userId: string; // Teammate user id
percentage: number; // Share for this area (typically 0–100)
}>;
}Example
const { submission } = await projex.milestones.submit('T12M3', {
title: 'Sprint 2 build',
submission: 'https://github.com/me/proj/pull/12',
teamId: 'optional-when-multi-team',
contributionAreas: [
{
contributionAreaId: 'area-id',
breakdown: [
{ userId: 'user-a', percentage: 60 },
{ userId: 'user-b', percentage: 40 },
],
},
],
reflectionAnswers: [
{ questionId: 'q1', answer: 'We validated the approach with mentors.' },
],
replacesSubmissionId: 'previous-submission-id',
});Returns
{
submission: {
id: string;
teamId: string;
milestoneId: string;
userId: string; // Submitter (API key owner)
title: string;
submission: string;
redoAttempt?: number; // Present when responding to a redo
isRedo?: boolean;
createdAt: string | Date;
updatedAt: string | Date;
// Additional team_submissions fields may be present
}
}projex.milestones.submissions(ref)
Returns submissions created by the authenticated user for the given milestone, newest first. Useful to show history or to obtain an id for replacesSubmissionId.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
ref | string | Yes | Milestone ref or id (same forms as submit). |
Returns
Promise<Page<unknown>> — { data, nextCursor }, newest first. Accepts { q?, limit?, cursor? }.
// page.data entries (team_submissions rows):
{
id: string;
teamId: string;
milestoneId: string;
userId: string;
title: string;
submission: string;
replacesSubmissionId: string | null;
createdAt: string | Date;
}Tasks
Task submissions are attributed to the individual user (the API key owner), not a team. No teamId is required. References use the TS{n} public form from cohorts.tasks. Semantics otherwise match milestones: append-only creates, optional revision lineage, server-enforced windows where applicable.
projex.tasks.submit(ref, input)
Creates a task submission. projex.tasks.resubmit is an alias of the same call.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
ref | string | Yes | Task reference such as TS42, or internal task id when accepted. |
input.title | string | Yes | Non-empty title for this submission. |
input.submission | string | Yes | URL, markdown, or plain-text body describing the completed work. |
input.replacesSubmissionId | string | No | Optional id of a prior submission this revision replaces (lineage only). |
Example & returns
const { submission } = await projex.tasks.submit('TS42', {
title: 'Bugfix write-up',
submission: '## Steps\n1. Reproduced on staging…',
});
// submission: {
// id, taskId, submittedBy, title, submission, createdAt, …
// }projex.tasks.submissions(ref)
Lists the caller's submissions for a task, newest first. Requires submissions:read.
Returns
Promise<Page<unknown>> — { data, nextCursor }, newest first. Accepts { q?, limit?, cursor? }.
// page.data entries (task_submissions rows):
{
id: string;
taskId: string;
submittedBy: string;
title: string;
submission: string;
replacesSubmissionId: string | null;
createdAt: string | Date;
}Uploads
Large files are not posted through the API body. Instead you obtain a short-lived presigned URL, PUT the bytes directly to object storage, then keep the returned fileKey for your workflow. Keys are namespaced per user so one account cannot attach another user's upload.
submissions:write. Maximum size: 50 MB. Allowed contentType prefixes: image/, application/pdf, application/zip, application/octet-stream, text/.projex.uploads.presign(input)
Step 1 of the upload flow: validates metadata and returns a presigned PUT URL plus storage key.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
input.fileName | string | Yes | Original file name (1–255 characters). Sanitized for the object key. |
input.contentType | string | Yes | MIME type. Must match the allowlist prefixes above or the API returns 422. |
input.size | number | No | Declared size in bytes. When set, must be a positive integer ≤ 50_000_000. |
Returns & follow-up
const { uploadUrl, fileKey, publicUrl, expiresIn } =
await projex.uploads.presign({
fileName: 'report.pdf',
contentType: 'application/pdf',
size: 812345,
});
// uploadUrl — presigned PUT target; complete before expiresIn seconds
// fileKey — e.g. submissions/<userId>/<uuid>/report.pdf
// publicUrl — the file's URL once uploaded; usable directly as a submission
// expiresIn — TTL in seconds (typically 3600)
await fetch(uploadUrl, {
method: 'PUT',
headers: { 'content-type': 'application/pdf' },
body: fileBytes, // Buffer, Blob, or Uint8Array
});
// Then either attach it, or use publicUrl as the submission value:
await projex.milestones.submit(ref, {
title: 'Report',
submission: publicUrl,
attachments: [{ fileKey, fileName: 'report.pdf' }],
});Administration
The second surface of the SDK: cohort and team CRUD plus membership, added in 0.2.0. Everything here needs an administration scope on the key and the key owner’s role in that specific cohort or team — see Authentication & scopes. Members are always identified by email, never an internal membership id.
import { ProjexClient } from '@pkg-projex/sdk';
const projex = new ProjexClient();
const cohort = await projex.cohorts.create({
projectId: 'prj_123',
name: 'Cohort 12',
visibility: 'public', // omitting this means "private" — see below
startDate: '2026-08-01',
endDate: '2026-10-01',
});
const team = await projex.teams.create(cohort.id, { name: 'Team Rocket' });
await projex.cohorts.members.add(cohort.id, {
email: 'ada@example.com',
role: 'learner',
teamId: team.id,
});dryRun & idempotency
Every write takes an optional third argument, AdminOptions, and behaves the same way across all of them.
| Option | Type | Description |
|---|---|---|
dryRun | boolean | Runs the full validation and permission check and reports what would happen, writing nothing. Sent as ?dryRun=true. |
idempotencyKey | string | Sent as the Idempotency-Key header; an accidental retry of the same call replays the first response instead of repeating the work. |
pjx_test_…) never writes — every call behaves as a dry run, whatever you pass.Both deletes return a discriminated union, so narrow on the tag before reading the payload:
type DeleteResult<T> =
| { ok: true; deleted: T } // it happened
| { dryRun: true; impact: T }; // preview only, nothing was written
const preview = await projex.cohorts.delete(cohortId, { dryRun: true });
if ('impact' in preview) {
console.log(preview.impact); // { teams: 4, members: 37, milestones: 6 }
}projex.cohorts.create / update / delete
Scope: cohorts:write. create also needs manage-project on the template it runs from, and makes you the new cohort’s manager.
Parameters — create(input, opts?)
| Name | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | The project template the cohort runs from. |
name | string | Yes | Non-empty. |
startDate | string | Yes | Any parseable date. endDate must be on or after it. |
endDate | string | Yes | See above. |
type | 'team' | 'individual' | No — team | An individual cohort gives every learner an automatic solo team, and refuses teams.create. |
visibility | 'public' | 'private' | No — private | Read the callout below before omitting it. |
clusterId | string | null | Only when private | The cluster whose roster seeds the cohort. Accepted and simply stored on a public cohort. |
visibility defaults to private, and a private cohort requires a cluster. So the minimal-looking call — projectId, name, dates, nothing else — throws ProjexApiError with code: 'validation_error' (“Cluster is required for private cohorts”). Pass visibility: 'public' if you don’t want a cluster. The same rule applies to update: switching a cohort to private needs a clusterId either in the patch or already on the cohort.Returns
type AdminCohort = Cohort & {
projectId: string;
clusterId: string | null;
};
// update(cohortId, input, opts?) — every field optional; only what you send changes.
// Accepts: name, visibility, clusterId, startDate, endDate.
await projex.cohorts.update(cohortId, { name: 'Cohort 12 (Autumn)' });
// delete(cohortId, opts?) — soft, and it cascades to the cohort's teams,
// members, milestones and submissions. Preview it first.
await projex.cohorts.delete(cohortId);
// → { ok: true, deleted: { teams: 4, members: 37, milestones: 6 } }projex.cohorts.members
Scope: cohort-members:write. add walks the whole chain — app user → org membership → cohort enrolment → optional team placement — skipping every layer that already exists, so retries are safe and re-adding someone is a no-op rather than an error. Read created to see what actually happened; all four false means they were already there.
await projex.cohorts.members.add(cohortId, {
email: 'ada@example.com', // required — the identifier for every member call
role: 'manager' | 'mentor' | 'evaluator' | 'learner', // default: learner
name: 'Ada Lovelace', // used only if the user has to be created
teamId: 'team_1', // optional placement; without it a learner is team-less
notify: true, // onboarding email, newly created users only
});
// → {
// userId, cohortMemberId, role, team: { id, name } | null,
// created: { user, orgMember, cohortMember, teamMember }, // all booleans
// }
// Changes the cohort role. Never deletes work, but it does reshuffle team
// placement — cohort role is the source of truth for which roster you sit on.
await projex.cohorts.members.changeRole(cohortId, {
email: 'ada@example.com',
role: 'mentor',
});
// → { userId, cohortMemberId, previousRole, role, rosterChanges: {
// removedFromTeams, addedToTeams, removedStaffFrom, addedStaffTo } }
// Shallow: removes them from the cohort and its teams. Their account and org
// membership survive — they may belong to other cohorts.
await projex.cohorts.members.remove(cohortId, { email: 'ada@example.com' });
// → { ok: true, userId, role }projex.teams
list needs teams:read; the writes need teams:write. Note the asymmetry in the arguments: teams are created and listed under a cohort, then updated and deleted by team id.
const { data, nextCursor } = await projex.teams.list(cohortId);
// → data: [{ id, publicId, name, description, cohortId, teamKind,
// memberCount, staffCount }]
// Paged like every list method — 50 per page, 100 max. Filters on top of
// { q, limit, cursor }: teamKind ('multi' | 'solo') and memberEmail.
await projex.teams.list(cohortId, { q: 'rocket', teamKind: 'multi' });
// Or let the SDK walk the pages — an individual cohort has one solo team
// per learner, so this is usually what you want.
for await (const team of projex.teams.iterate(cohortId)) {
console.log(team.name);
}
await projex.teams.create(cohortId, {
name: 'Team Rocket',
description: 'Optional',
coverImage: null,
});
await projex.teams.update(teamId, { name: 'Team Rocket II' });
// Also accepts: description, overview, coverImage.
// Soft-deletes the team and its tasks, submissions, members and staff. Its
// learners stay enrolled in the cohort but end up on no team.
await projex.teams.delete(teamId, { dryRun: true });
// → { dryRun: true, impact: { learners: 3, staff: 1 } }create throws code: 'conflict' (409) with details.reason: 'individual_cohort' in an individual cohort, where the solo teams are made for you.projex.teams.members
Scope: team-members:write. The role picks the roster — learner goes to members, mentor and evaluator to staff — and staff are enrolled in the cohort at that role first if they aren’t already.
await projex.teams.members.add(teamId, {
email: 'ada@example.com',
role: 'learner' | 'mentor' | 'evaluator', // default: learner
name: 'Ada Lovelace',
notify: true,
});
// → the same ProvisioningSummary as cohorts.members.add
await projex.teams.members.changeRole(teamId, {
email: 'ada@example.com',
role: 'mentor',
});
// → { userId, teamId, teamRole }
// Removes whichever seat they hold. Cohort enrolment is untouched.
await projex.teams.members.remove(teamId, { email: 'ada@example.com' });
// → { ok: true, userId, removed: 'learner' | 'staff' }code: 'conflict' (409) with details.reason: 'role_locked_to_cohort' — change the cohort role via cohorts.members.changeRole first. Someone who isn’t in the cohort at all gives details.reason: 'not_cohort_member'.Errors & retries
Failed HTTP responses throw ProjexApiError, a subclass of Error with structured fields for handling and support correlation.
| Property | Type | Meaning |
|---|---|---|
status | number | HTTP status code |
code | string | Stable machine-readable error code from the API envelope |
message | string | Human-readable explanation |
requestId | string? | Correlation id — include when contacting support |
details | unknown? | Optional structured details (e.g. Zod flatten) |
Error codes
| code | HTTP | When it happens |
|---|---|---|
invalid_api_key | 401 | Missing, malformed, expired, revoked, or disabled key |
insufficient_scope | 403 | Key lacks a scope required by the endpoint |
not_found | 404 | Unknown cohort, milestone, or task (or no access) |
ambiguous_team | 409 | Milestone submit without teamId while user is on multiple teams |
conflict | 409 | Administration methods: the call clashes with current state. Branch on details.reason — individual_cohort, role_locked_to_cohort, not_cohort_member, user_banned |
validation_error | 422 | Invalid body, disallowed content type, or closed submission window |
rate_limited | 429 | Too many requests — SDK retries with backoff |
internal_error | 500 | Unexpected server failure — SDK retries |
import { ProjexApiError } from '@pkg-projex/sdk';
try {
await projex.milestones.submit(ref, input);
} catch (err) {
if (err instanceof ProjexApiError) {
if (err.code === 'ambiguous_team') {
// Prompt for teamId from cohorts.get(...).teams
}
console.error(err.status, err.code, err.message, err.requestId);
}
throw err;
}429 and 5xx. Client errors (4xx other than rate limits) fail immediately so invalid payloads are not repeated.Identifiers
Public refs are stable, human-readable identifiers used across the product (including GitHub webhook linking). Always prefer values returned by list endpoints over constructing refs by hand.
| Resource | Form | Example | Notes |
|---|---|---|---|
| Task | TS{n} | TS42 | Global serial public id |
| Milestone | T{team}M{ms} | T12M3 | Team public id + milestone public id |
| Cohort | UUID | From cohorts.list / live | No separate public slug in v1 |
For interactive HTTP exploration see the API reference (Swagger).