Chains
A Chain is an executable workflow: a graph of Bits (its steps — plain tasks or agent/twin/tool nodes) that runs when a trigger fires. Streaks, periods, signatures, and the puzzle/consistency views are run history — a successful run signs the period and bumps the streak. Configure when a chain runs with chain triggers; configure how it runs with the execution policy and its Bit-tree DAG.
Endpoints
| Method | Path | Description | Auth | Rate Limit |
|---|---|---|---|---|
| GET | /chains | List all chains | JWT | 60/min |
| GET | /chains/:id | Get a chain (supports include* flags) | JWT | 60/min |
| GET | /chains/:id/streak | Get current streak info | JWT | 60/min |
| POST | /chains | Create a chain | JWT + Entitlement | 30/min |
| PATCH | /chains/:id | Update a chain | JWT | 30/min |
| DELETE | /chains/:id | Delete a chain | JWT | 30/min |
Create Chain
Entitlement: productivity.chain.create
Request
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Chain title (1–150 chars) |
description | string | No | Description of the chain (max 500 chars) |
chainyId | string (uuid) | No | Parent chainy ID |
protocolType | string | No | Execution protocol: scheduled, constraint, event_driven, ai_triggered, adaptive, one_shot |
requiredVerificationLevel | string | No | Minimum verification level for bit completions to count: self_attested, note_provided, artifact_backed, peer_validated, ai_verified, external_source, cryptographic |
targetPerPeriod | integer | No | Target completions per period (min 1) |
startDate | string | No | ISO 8601 start date |
endDate | string | No | ISO 8601 end date |
colorHex | string | No | Hex color code in #RRGGBB format |
isVisible | boolean | No | Whether the chain is visible (default true) |
habitType | string | No | Deprecated — use protocolType instead |
If you pass
chainyId, use theidfrom Create Chainy's response as$CHAINY_ID.
Response
{
"data": {
"id": "cm5chain01",
"title": "Daily Vocabulary Practice",
"description": "Learn 20 new Spanish words every day",
"chainyId": "cm5abc123",
"protocolType": "scheduled",
"colorHex": "#27AE60",
"isVisible": true,
"currentStreak": 0,
"longestStreak": 0,
"createdAt": "2026-03-17T10:30:00.000Z",
"updatedAt": "2026-03-17T10:30:00.000Z"
}
}Code Examples
curl -X POST https://api.chainabit.com/api/v1/chains \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Daily Vocabulary Practice",
"description": "Learn 20 new Spanish words every day",
"chainyId": "'$CHAINY_ID'",
"protocolType": "scheduled",
"colorHex": "#27AE60"
}'const BASE_URL = process.env.BASE_URL;
const TOKEN = process.env.TOKEN;
const CHAINY_ID = process.env.CHAINY_ID; // from Create Chainy's response
const response = await fetch(`${BASE_URL}/chains`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "Daily Vocabulary Practice",
description: "Learn 20 new Spanish words every day",
chainyId: CHAINY_ID,
protocolType: "scheduled",
colorHex: "#27AE60",
}),
});
const { data } = await response.json();import requests, os
chainy_id = os.environ["CHAINY_ID"] # from Create Chainy's response
response = requests.post(
f"{os.environ['BASE_URL']}/chains",
headers={"Authorization": f"Bearer {os.environ['TOKEN']}"},
json={
"title": "Daily Vocabulary Practice",
"description": "Learn 20 new Spanish words every day",
"chainyId": chainy_id,
"protocolType": "scheduled",
"colorHex": "#27AE60",
},
)
data = response.json()["data"]Get Chain with Computed Fields
GET /chains/:id accepts a set of boolean flags that enrich the chain object with computed statistics, period state, signature history, and streak data. Each flag is passed as its own query parameter — there is no combined include parameter.
Request
| Parameter | Type | Default | Description |
|---|---|---|---|
includeStats | boolean | true | Add aggregate counters (totalBits, completedBits, currentPeriodCompleted, hasTodayBit, hasCurrentWeekBit) |
includeConsistency | boolean | true | Add consistency metrics. Only applied when includeStats is true. |
includePeriodState | boolean | false | Add the currentPeriod object (required vs. completed counts for the active period) |
includeSignatureSummary | boolean | false | Add the periodSignaturesSummary object (signature history for the chain) |
includeStreak | boolean | false | Add currentStreak, longestStreak, and lastSignedAt |
from | string | — | ISO 8601 start date to bound consistency/period computation |
to | string | — | ISO 8601 end date to bound consistency/period computation |
Use the
idfrom Create Chain's response (data.id) as$CHAIN_ID.
Response
{
"data": {
"id": "cm5chain01",
"title": "Daily Vocabulary Practice",
"colorHex": "#27AE60",
"status": "active",
"totalBits": 7,
"completedBits": 6,
"currentPeriodCompleted": false,
"currentPeriod": {
"id": "cm5per001",
"scheduleId": "cm5sch001",
"periodStart": "2026-03-16T00:00:00.000Z",
"periodEnd": "2026-03-16T23:59:59.000Z",
"requiredCount": 1,
"completedCount": 0,
"status": "in_progress",
"evaluatedAt": null
},
"currentStreak": 5,
"longestStreak": 12,
"lastSignedAt": "2026-03-16T18:00:00.000Z"
}
}To retrieve the chain's Bits, use the dedicated
GET /chains/:id/bit-treeendpoint — Bits are not embedded in the chain detail response.
Code Examples
curl "https://api.chainabit.com/api/v1/chains/$CHAIN_ID?includePeriodState=true&includeStreak=true" \
-H "Authorization: Bearer $TOKEN"const BASE_URL = process.env.BASE_URL;
const TOKEN = process.env.TOKEN;
const CHAIN_ID = process.env.CHAIN_ID; // from Create Chain's response
const params = new URLSearchParams({
includePeriodState: "true",
includeStreak: "true",
});
const response = await fetch(
`${BASE_URL}/chains/${CHAIN_ID}?${params}`,
{ headers: { Authorization: `Bearer ${TOKEN}` } }
);
const { data } = await response.json();import requests, os
chain_id = os.environ["CHAIN_ID"] # from Create Chain's response
response = requests.get(
f"{os.environ['BASE_URL']}/chains/{chain_id}",
params={"includePeriodState": "true", "includeStreak": "true"},
headers={"Authorization": f"Bearer {os.environ['TOKEN']}"},
)
data = response.json()["data"]Get Chain Streak
Request
Reuses
$CHAIN_IDfrom Create Chain's response.
Response
{
"data": {
"current": 5,
"longest": 12,
"lastCompletedAt": "2026-03-16T18:00:00.000Z",
"startedAt": "2026-03-12T07:00:00.000Z"
}
}Code Examples
curl https://api.chainabit.com/api/v1/chains/$CHAIN_ID/streak \
-H "Authorization: Bearer $TOKEN"const BASE_URL = process.env.BASE_URL;
const TOKEN = process.env.TOKEN;
const CHAIN_ID = process.env.CHAIN_ID; // from Create Chain's response
const response = await fetch(`${BASE_URL}/chains/${CHAIN_ID}/streak`, {
headers: { Authorization: `Bearer ${TOKEN}` },
});
const { data } = await response.json();import requests, os
chain_id = os.environ["CHAIN_ID"] # from Create Chain's response
response = requests.get(
f"{os.environ['BASE_URL']}/chains/{chain_id}/streak",
headers={"Authorization": f"Bearer {os.environ['TOKEN']}"},
)
data = response.json()["data"]Chain Periods
Periods divide a chain into time-bounded segments for tracking.
Endpoints
| Method | Path | Description | Auth | Rate Limit |
|---|---|---|---|---|
| GET | /chains/:chainId/periods | List periods for a chain | JWT | 60/min |
| GET | /chains-periods/:id | Get a specific period | JWT | 60/min |
Reuses
$CHAIN_IDfrom Create Chain's response.
curl https://api.chainabit.com/api/v1/chains/$CHAIN_ID/periods \
-H "Authorization: Bearer $TOKEN"const BASE_URL = process.env.BASE_URL;
const TOKEN = process.env.TOKEN;
const CHAIN_ID = process.env.CHAIN_ID; // from Create Chain's response
const response = await fetch(`${BASE_URL}/chains/${CHAIN_ID}/periods`, {
headers: { Authorization: `Bearer ${TOKEN}` },
});
const { data } = await response.json();import requests, os
chain_id = os.environ["CHAIN_ID"] # from Create Chain's response
response = requests.get(
f"{os.environ['BASE_URL']}/chains/{chain_id}/periods",
headers={"Authorization": f"Bearer {os.environ['TOKEN']}"},
)
data = response.json()["data"]{
"data": [
{
"id": "cm5per001",
"chainId": "cm5chain01",
"startDate": "2026-03-11T00:00:00.000Z",
"endDate": "2026-03-17T23:59:59.000Z",
"completionRate": 0.85,
"totalBits": 7,
"completedBits": 6
}
],
"meta": { "total": 1 }
}Chain Calendar
A calendar summary view for a chain.
Endpoints
| Method | Path | Description | Auth | Rate Limit |
|---|---|---|---|---|
| GET | /chains/:chainId/calendar | Calendar summary | JWT | 30/min |
Reuses
$CHAIN_IDfrom Create Chain's response.
curl https://api.chainabit.com/api/v1/chains/$CHAIN_ID/calendar \
-H "Authorization: Bearer $TOKEN"const BASE_URL = process.env.BASE_URL;
const TOKEN = process.env.TOKEN;
const CHAIN_ID = process.env.CHAIN_ID; // from Create Chain's response
const response = await fetch(`${BASE_URL}/chains/${CHAIN_ID}/calendar`, {
headers: { Authorization: `Bearer ${TOKEN}` },
});
const { data } = await response.json();import requests, os
chain_id = os.environ["CHAIN_ID"] # from Create Chain's response
response = requests.get(
f"{os.environ['BASE_URL']}/chains/{chain_id}/calendar",
headers={"Authorization": f"Bearer {os.environ['TOKEN']}"},
)
data = response.json()["data"]{
"data": {
"month": "2026-03",
"days": {
"2026-03-01": { "completed": 1, "total": 1, "streak": true },
"2026-03-02": { "completed": 0, "total": 1, "streak": false },
"2026-03-03": { "completed": 1, "total": 1, "streak": true }
}
}
}Chain Signatures
Signatures capture the behavioral fingerprint of a chain period.
Endpoints
| Method | Path | Description | Auth | Rate Limit |
|---|---|---|---|---|
| GET | /chains/:chainId/signatures | List signatures for a chain | JWT | 60/min |
| GET | /chains-signatures/by-period/:periodId | Get signature by period | JWT | 60/min |
Reuses
$CHAIN_IDfrom Create Chain's response.
curl https://api.chainabit.com/api/v1/chains/$CHAIN_ID/signatures \
-H "Authorization: Bearer $TOKEN"const BASE_URL = process.env.BASE_URL;
const TOKEN = process.env.TOKEN;
const CHAIN_ID = process.env.CHAIN_ID; // from Create Chain's response
const response = await fetch(`${BASE_URL}/chains/${CHAIN_ID}/signatures`, {
headers: { Authorization: `Bearer ${TOKEN}` },
});
const { data } = await response.json();import requests, os
chain_id = os.environ["CHAIN_ID"] # from Create Chain's response
response = requests.get(
f"{os.environ['BASE_URL']}/chains/{chain_id}/signatures",
headers={"Authorization": f"Bearer {os.environ['TOKEN']}"},
)
data = response.json()["data"]{
"data": [
{
"id": "cm5sig001",
"chainId": "cm5chain01",
"periodId": "cm5per001",
"consistencyScore": 0.87,
"averageCompletionTime": "14:30",
"preferredDays": ["monday", "wednesday", "friday"],
"createdAt": "2026-03-17T00:00:00.000Z"
}
]
}Chain Schedules
Define recurrence schedules for chains.
Endpoints
| Method | Path | Description | Auth | Rate Limit |
|---|---|---|---|---|
| GET | /chains/:chainId/schedules | List schedules | JWT | 60/min |
| POST | /chains/:chainId/schedules | Create a schedule | JWT | 30/min |
| PATCH | /chains-schedules/:id | Update a schedule | JWT | 30/min |
| POST | /chains-schedules/:id/deactivate | Deactivate a schedule | JWT | 30/min |
Create Schedule
Request
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | daily, weekly, custom |
daysOfWeek | number[] | No | Days of week (0=Sun, 6=Sat) for weekly type |
timeOfDay | string | No | Preferred time in HH:mm format |
timezone | string | No | IANA timezone string |
Reuses
$CHAIN_IDfrom Create Chain's response.
Response
{
"data": {
"id": "cm5sched01",
"chainId": "cm5chain01",
"type": "weekly",
"daysOfWeek": [1, 3, 5],
"timeOfDay": "09:00",
"timezone": "Europe/Istanbul",
"active": true,
"createdAt": "2026-03-17T10:00:00.000Z"
}
}Code Examples
curl -X POST https://api.chainabit.com/api/v1/chains/$CHAIN_ID/schedules \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "weekly",
"daysOfWeek": [1, 3, 5],
"timeOfDay": "09:00",
"timezone": "Europe/Istanbul"
}'const BASE_URL = process.env.BASE_URL;
const TOKEN = process.env.TOKEN;
const CHAIN_ID = process.env.CHAIN_ID; // from Create Chain's response
const response = await fetch(`${BASE_URL}/chains/${CHAIN_ID}/schedules`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
type: "weekly",
daysOfWeek: [1, 3, 5],
timeOfDay: "09:00",
timezone: "Europe/Istanbul",
}),
});
const { data } = await response.json();import requests, os
chain_id = os.environ["CHAIN_ID"] # from Create Chain's response
response = requests.post(
f"{os.environ['BASE_URL']}/chains/{chain_id}/schedules",
headers={"Authorization": f"Bearer {os.environ['TOKEN']}"},
json={
"type": "weekly",
"daysOfWeek": [1, 3, 5],
"timeOfDay": "09:00",
"timezone": "Europe/Istanbul",
},
)
data = response.json()["data"]Deactivate Schedule
Request
Use the
idfrom Create Schedule's response (data.id) as$SCHEDULE_ID.
Response
{
"data": {
"id": "cm5sched01",
"active": false,
"updatedAt": "2026-03-17T15:00:00.000Z"
}
}Code Examples
curl -X POST https://api.chainabit.com/api/v1/chains-schedules/$SCHEDULE_ID/deactivate \
-H "Authorization: Bearer $TOKEN"const BASE_URL = process.env.BASE_URL;
const TOKEN = process.env.TOKEN;
const SCHEDULE_ID = process.env.SCHEDULE_ID; // from Create Schedule's response
const response = await fetch(`${BASE_URL}/chains-schedules/${SCHEDULE_ID}/deactivate`, {
method: "POST",
headers: { Authorization: `Bearer ${TOKEN}` },
});
const { data } = await response.json();import requests, os
schedule_id = os.environ["SCHEDULE_ID"] # from Create Schedule's response
response = requests.post(
f"{os.environ['BASE_URL']}/chains-schedules/{schedule_id}/deactivate",
headers={"Authorization": f"Bearer {os.environ['TOKEN']}"},
)
data = response.json()["data"]Chain Templates
Browse, fork, and publish chain templates from the public catalog.
Endpoints
| Method | Path | Description | Auth | Rate Limit |
|---|---|---|---|---|
| GET | /chains-templates | List templates (public catalog) | JWT | 60/min |
| GET | /chains-templates/:id | Get a template | JWT | 60/min |
| POST | /chains-templates/:id/fork | Fork a template into your chains | JWT + Entitlement | 10/min |
| POST | /chains-templates/:chainId/publish-template | Publish a chain as template | JWT + Entitlement | 10/min |
Fork Template
Request
Use the
idof a template from List Templates (the public catalog) as$TEMPLATE_ID.chainyIdreuses$CHAINY_IDfrom Create Chainy's response.
Response
{
"data": {
"id": "cm5chain02",
"title": "Morning Routine - Forked",
"chainyId": "cm5abc123",
"templateId": "cm5tmpl01",
"createdAt": "2026-03-17T10:00:00.000Z"
}
}Code Examples
curl -X POST https://api.chainabit.com/api/v1/chains-templates/$TEMPLATE_ID/fork \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"chainyId": "'$CHAINY_ID'"
}'const BASE_URL = process.env.BASE_URL;
const TOKEN = process.env.TOKEN;
const TEMPLATE_ID = process.env.TEMPLATE_ID; // from the public templates catalog
const CHAINY_ID = process.env.CHAINY_ID; // from Create Chainy's response
const response = await fetch(`${BASE_URL}/chains-templates/${TEMPLATE_ID}/fork`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ chainyId: CHAINY_ID }),
});
const { data } = await response.json();import requests, os
template_id = os.environ["TEMPLATE_ID"] # from the public templates catalog
chainy_id = os.environ["CHAINY_ID"] # from Create Chainy's response
response = requests.post(
f"{os.environ['BASE_URL']}/chains-templates/{template_id}/fork",
headers={"Authorization": f"Bearer {os.environ['TOKEN']}"},
json={"chainyId": chainy_id},
)
data = response.json()["data"]Publish as Template
Request
Reuses
$CHAIN_IDfrom Create Chain's response — the chain you're publishing.
Response
{
"data": {
"id": "cm5tmpl02",
"title": "Daily Spanish Practice",
"sourceChainId": "cm5chain01",
"category": "learning",
"published": true,
"createdAt": "2026-03-17T10:00:00.000Z"
}
}Code Examples
curl -X POST https://api.chainabit.com/api/v1/chains-templates/$CHAIN_ID/publish-template \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Daily Spanish Practice",
"description": "A structured daily routine for learning Spanish vocabulary and grammar",
"category": "learning",
"tags": ["language", "spanish", "daily"]
}'const BASE_URL = process.env.BASE_URL;
const TOKEN = process.env.TOKEN;
const CHAIN_ID = process.env.CHAIN_ID; // from Create Chain's response
const response = await fetch(`${BASE_URL}/chains-templates/${CHAIN_ID}/publish-template`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "Daily Spanish Practice",
description: "A structured daily routine for learning Spanish vocabulary and grammar",
category: "learning",
tags: ["language", "spanish", "daily"],
}),
});
const { data } = await response.json();import requests, os
chain_id = os.environ["CHAIN_ID"] # from Create Chain's response
response = requests.post(
f"{os.environ['BASE_URL']}/chains-templates/{chain_id}/publish-template",
headers={"Authorization": f"Bearer {os.environ['TOKEN']}"},
json={
"title": "Daily Spanish Practice",
"description": "A structured daily routine for learning Spanish vocabulary and grammar",
"category": "learning",
"tags": ["language", "spanish", "daily"],
},
)
data = response.json()["data"]