Sredify API
Read a company's SR&ED projects, people, assessments, hours and claims. Add recorded time, notes and links.
Overview
- Base address:
https://app.sredify.ca/api/v1. JSON in, JSON out. Version 1; anything that breaks your code would go in a new version. - The whole API as an OpenAPI 3.1 file, for code generators and tools like Postman.
- Two ways in. An API key is for a company's own scripts: an Admin creates it in the app. An app is for software used by many companies (a timesheet tool, a browser extension): you register it on Developers, and each company's Admin approves it.
- Each key or app works in one company. Everything is limited to that company.
- Never available: source code, GitHub or Jira tokens, sign-in details, member emails and roles, the company's history log, other companies.
Quick start: API key
A company Admin opens Account, Apps and API, clicks New key, picks the permissions and copies the key (shown once). Then:
# 1. A company Admin creates a key: Account, Apps and API, New key.
export SRED_KEY="sred_key_..."
# 2. Read the company's projects.
curl -s https://app.sredify.ca/api/v1/projects -H "Authorization: Bearer $SRED_KEY"
# 3. Record time (needs the "Record time" permission on the key).
curl -s https://app.sredify.ca/api/v1/hours \
-H "Authorization: Bearer $SRED_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"projectId":"p_robot","email":"rita@acme.example","date":"2026-03-14","hours":7.5,"description":"Grip force tests"}'Connect an app
Apps use OAuth 2.0 with PKCE (the standard way Google, Microsoft and others let apps connect). Register your app on Developers. Say what it does in at least one full sentence: people read it on the approval screen, and on a verified app a new description waits for Sredify to review it. Pick the kind:
- Browser extension or web page (no secret). The code runs on the person's computer, so it can't keep a secret. PKCE protects each sign-in.
- Server app (with a secret). The code runs on your servers. It sends PKCE and its secret, so a stolen code or refresh token is useless without the secret.
- Make a random
code_verifier(43 to 128 characters) and itscode_challenge= base64url(SHA-256(verifier)). - Send the person to
https://app.sredify.ca/oauth/authorizewithresponse_type=code,client_id,redirect_uri(exactly as registered),scope(space separated),state,code_challenge,code_challenge_method=S256. - They sign in and a company Admin approves. Only Admins can approve; Editors and View only see that they can't. They come back to your
redirect_uriwith?code=&state=, or?error=access_deniedif they cancel. - Within 10 minutes, POST the code to
https://app.sredify.ca/api/oauth/tokenonce. You get an access token (60 minutes) and a refresh token (30 days). - Call the API with
Authorization: Bearer sred_at_.... When it expires, POST the refresh token to the same address. The refresh token changes every time: save the new one. Using an old one again signs your app out of that company (we assume it was stolen). - To disconnect, POST the token to
https://app.sredify.ca/api/oauth/revoke.
App icon. Upload it on Developers (you crop it square there), or send the image as the body of PUT https://app.sredify.ca/api/developers/apps/{id}/icon while signed in: PNG, JPEG or WebP, square, 128 to 1024 px, up to 512 KB (256 px is plenty). DELETE on the same address removes it. Sredify serves the icon itself, so the approval screen never loads anything from your server. No icon: people see the first letter of your app's name.
New apps show an “Unverified app” warning on the approval screen until Sredify checks who is behind them. Once an app is verified, a new name waits for Sredify to review it: people keep seeing the approved name until then. Return addresses must be https://, http://localhost, or an extension address (chrome-extension://, https://<id>.chromiumapp.org/).
Sredify Apps. A verified app can ask for a public page in Sredify Apps: open Store listing on Developers. It needs a developer name, a page address (fixed once listed), a one-line tagline, a support email or page, 2 to 6 screenshots (16:10, at least 1280 x 800, sample data only) and a Get link: the page on your site where an Admin lands from Get. Start the authorize step above from there, because the PKCE verifier has to stay with your app. Your website, privacy policy and Get link must be public https:// pages (not localhost). Sredify reviews the listing before it goes live, and later changes wait for review the same way. Screenshots are served by Sredify, like icons.
Browser extension sample
A complete Chrome extension background script: connect, keep tokens fresh, call the API, disconnect.
// background.js (Chrome extension, Manifest V3)
// manifest.json needs: "permissions": ["identity", "storage"]
// Register this extension's return address on /developers:
// chrome.identity.getRedirectURL() -> https://<extension-id>.chromiumapp.org/
const BASE = "https://app.sredify.ca";
const CLIENT_ID = "sred_app_..."; // from /developers
const REDIRECT = chrome.identity.getRedirectURL();
const b64url = (buf) =>
btoa(String.fromCharCode(...new Uint8Array(buf))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
async function tokenRequest(params) {
const r = await fetch(BASE + "/api/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams(params),
});
const j = await r.json();
if (!r.ok) throw new Error(j.error_description || j.error);
return j;
}
const save = (t) => chrome.storage.local.set({ tokens: { ...t, expiresAt: Date.now() + t.expires_in * 1000 } });
// 1. Connect: the person signs in and a company Admin approves.
async function connect(scope = "projects:read hours:write") {
const verifier = b64url(crypto.getRandomValues(new Uint8Array(32)));
const challenge = b64url(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)));
const state = b64url(crypto.getRandomValues(new Uint8Array(16)));
const url = BASE + "/oauth/authorize?" + new URLSearchParams({
response_type: "code", client_id: CLIENT_ID, redirect_uri: REDIRECT, scope, state,
code_challenge: challenge, code_challenge_method: "S256",
});
const back = new URL(await chrome.identity.launchWebAuthFlow({ url, interactive: true }));
if (back.searchParams.get("state") !== state) throw new Error("State mismatch");
if (back.searchParams.get("error")) throw new Error(back.searchParams.get("error_description") || back.searchParams.get("error"));
await save(await tokenRequest({
grant_type: "authorization_code", code: back.searchParams.get("code"),
redirect_uri: REDIRECT, client_id: CLIENT_ID, code_verifier: verifier,
}));
}
// 2. A fresh access token. Refresh tokens change on EVERY use, and using an old
// one signs the app out (we treat it as stolen). So refresh one at a time.
let refreshing = null;
async function accessToken() {
const { tokens } = await chrome.storage.local.get("tokens");
if (!tokens) throw new Error("Not connected");
if (Date.now() < tokens.expiresAt - 60_000) return tokens.access_token;
refreshing ??= tokenRequest({ grant_type: "refresh_token", refresh_token: tokens.refresh_token, client_id: CLIENT_ID })
.then(async (t) => { await save(t); return t.access_token; })
.finally(() => { refreshing = null; });
return refreshing;
}
// 3. Call the API.
async function api(method, path, body) {
const r = await fetch(BASE + "/api/v1" + path, {
method,
headers: {
Authorization: "Bearer " + (await accessToken()),
...(body ? { "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID() } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const j = r.status === 204 ? null : await r.json();
if (!r.ok) throw new Error(j?.error?.message || r.statusText);
return j;
}
// Example: record 7.5 hours for Rita today.
// await api("POST", "/hours", { projectId: "p_robot", email: "rita@acme.example",
// date: new Date().toLocaleDateString("en-CA"), hours: 7.5, description: "Grip force tests" });
// 4. Disconnect.
async function disconnect() {
const { tokens } = await chrome.storage.local.get("tokens");
if (tokens) await fetch(BASE + "/api/oauth/revoke", { method: "POST", body: new URLSearchParams({ token: tokens.refresh_token, client_id: CLIENT_ID }) });
await chrome.storage.local.remove("tokens");
}
Server app sample
# Server app: same flow, plus your client secret on every token request.
# 1. Send the person to /oauth/authorize (as above) with your own PKCE verifier + challenge.
# 2. They come back to your redirect address with ?code=...&state=... (check state).
# 3. Swap the code (within 10 minutes, once):
curl -s https://app.sredify.ca/api/oauth/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=authorization_code \
-d code="$CODE" \
-d redirect_uri="https://yourapp.example.com/sred/callback" \
-d code_verifier="$VERIFIER"
# -> {"access_token":"sred_at_...","token_type":"Bearer","expires_in":3600,
# "refresh_token":"sred_rt_...","scope":"projects:read hours:write"}
# 4. When the access token expires (1 hour), refresh. Store the NEW refresh token.
curl -s https://app.sredify.ca/api/oauth/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=refresh_token \
-d refresh_token="$REFRESH_TOKEN"
# 5. Call the API.
curl -s https://app.sredify.ca/api/v1/projects -H "Authorization: Bearer $ACCESS_TOKEN"Permissions
Ask only for what you need. People see each one in plain words before they approve.
- company:read
- Company details. Business info, CRA numbers, addresses, SR&ED profile.
- people:read
- People. Names, titles, employment type and dates, R&D role, qualifications. No pay.
- pay:read
- Pay. Pay type and amount per person, salary amounts in claims. Sensitive: only an Admin can approve it, and it stops if that person is no longer an Admin.
- projects:read
- Projects. Projects, their repos, team and Jira projects.
- assessments:read
- Assessments. Saved assessments, eligible work, criteria, commit and Jira references.
- hours:read
- Hours. Hours per person per project: estimated, recorded, and which counts.
- claims:read
- Claims. Contractors, materials, equipment, funding. Salary amounts and totals need Pay too.
- evidence:read
- Evidence. Recorded time, notes, links and supporting evidence, with where each came from.
- hours:write
- Record time. Add, change and remove its own time entries. Never changes estimates.
- evidence:write
- Add notes, links and supporting evidence. Add, change and remove its own notes and links, and supporting evidence on eligible work (meetings, references, engineering steps, tests) that a person confirmed. Supporting evidence backs up hours, it never adds to them.
- assessments:write
- Change hours. Change eligible work hours and each person's share of them, with a reason. Every change shows the app's name and can be undone.
Rules
- Who you act as. A key acts as the Admin who created it, an app as the Admin who approved it, with their current role and never more than the permissions given. If that person becomes View only, writes stop. If they leave the company, the key or app stops.
- Labels. Everything you add shows in the app with your key's or app's name and the Admin behind it, for example “Acme Timer · approved by Ann Admin”.
- Recorded time replaces the estimate for that person on that day; it is never added on top. Estimates can't be changed through the API.
- Who time can be recorded for. Someone on the company's People list and on the project: ticked on its Team, or linked to a GitHub contributor with commits in its repos.
- Limits per day. More than 0 and at most 24 hours per entry; a person's recorded total for the day, across all projects, can't go over the company's daily maximum (
GET /org,sred.maxHoursPerDay). No future dates. - Your own items only. You can change or remove only what your key or app added. Removing keeps the item in history with your reason.
- Revoking. An Admin can revoke a key or app at any time, and can also remove everything an app added.
Requests and responses
- Dates are
yyyy-mm-dd; times are ISO 8601 UTC. Money is in Canadian dollars. - Lists that can be long return
{ data, nextCursor }. Pass?cursor=for the next page;nextCursorisnullon the last one.?limit=1 to 200, default 50. - Syncing evidence:
GET /evidence?since=with the time of your last sync returns only changed items (oldest first), including removals withincludeRemoved=true. - Caching: every GET returns an
ETag. Send it back asIf-None-Matchand get304 Not Modifiedwhen nothing changed. - Retries: send an
Idempotency-Keyheader on POST. Sending the same key again within 24 hours returns the first result instead of adding twice. - Request limits: 120 a minute per API key, 600 a minute per app (across all companies). Over that you get
429withRetry-After. - Browsers: CORS is open, so web pages and extensions can call the API directly. Sign-in cookies are never accepted, only tokens.
- Every call is logged for 90 days (which key or app, address, result, time). Admins see the usage of each key and app.
Errors
Errors are { "error": { "code", "message" } }. Branch on code; show message to people. The token endpoint follows the OAuth standard instead: { "error", "error_description" } with codes like invalid_grant and invalid_client.
- 400 bad_json
- The body isn't valid JSON.
- 400 bad_range
- from/to missing one, not yyyy-mm-dd, or from after to.
- 400 bad_cursor
- The cursor isn't one we gave you.
- 400 bad_since
- since isn't a date.
- 401 unauthorized
- No Authorization header, or an unknown API key.
- 401 key_revoked
- An Admin revoked the key.
- 401 key_expired
- The key reached its expiry date.
- 401 invalid_token
- Unknown or revoked app access token.
- 401 token_expired
- The app access token is over 1 hour old. Use the refresh token.
- 401 app_revoked
- The company revoked your app.
- 403 missing_scope
- The key or app doesn't have that permission.
- 403 role_too_low
- The Admin behind the key or app now has a role that doesn't allow it (for example View only can't write).
- 403 creator_removed
- The person who created the key left the company.
- 403 approver_removed
- The Admin who approved the app left the company.
- 403 app_suspended
- Sredify suspended the app.
- 403 company_suspended
- Sredify suspended the company.
- 403 account_suspended
- Sredify suspended the person who made the key or approved the app.
- 403 not_yours
- You tried to change an item someone else added.
- 404 not_found
- No such item in this company.
- 409 removed
- The item was already removed.
- 409 conflict
- Someone changed the same item at the same moment. Try again.
- 409 changed_since_read
- You sent the version you read and the report changed since. Read it again.
- 404 nothing_to_undo
- Your app or key has no hours changes on that item.
- 422 not_past_work
- Hours per person can only change on past work reports.
- 422 unknown_person
- No one with that personId on People.
- 422 not_on_project
- The person isn't on the report's project (Team or a GitHub contributor in its repos).
- 422 person_not_counted
- The person has no GitHub author linked on People, so a share for them can't count in the claim.
- 422 below_recorded
- A share less than the person's logged Jira time on the item.
- 422 shares_over_item
- The shares on an item add up to more than its hours.
- 422 invalid
- The data breaks a rule (unknown person, not on the project, too many hours, future date, ...). The message says which.
- 429 rate_limited
- Too many requests. Wait for Retry-After seconds.
- 500 save_failed
- Something went wrong saving. Safe to retry with the same Idempotency-Key.
Endpoints
In the samples, $TOKEN is an API key or an app access token.
/api/v1/orgThe company
Company profile and SR&ED settings.
Permission: company:read
curl -s "https://app.sredify.ca/api/v1/org" \
-H "Authorization: Bearer $TOKEN"{
"id": "cmorg1",
"name": "Acme Robotics Inc.",
"business": {
"legalName": "Acme Robotics Inc.",
"operatingName": "Acme Robotics",
"businessNumber": "123456789",
"industry": "Robotics",
"naics": "333249"
},
"sred": {
"ccpc": true,
"fiscalYearEnd": "12-31",
"claimedBefore": false,
"maxHoursPerDay": 10
}
}/api/v1/peoplePeople
Everyone on the company's People list. Pay is included only with pay:read.
Permission: people:read
- Add pay:read to get each person's pay type and amount.
curl -s "https://app.sredify.ca/api/v1/people" \
-H "Authorization: Bearer $TOKEN"{
"data": [
{
"id": "per_rita",
"name": "Rita Robotics",
"email": "rita@acme.example",
"title": "Engineer",
"department": "R&D",
"employmentType": "full-time",
"startDate": "2024-01-08",
"endDate": null,
"province": "ON",
"stillHere": true,
"endDateEstimated": false,
"rdRole": "technical",
"sredPercent": 80,
"specifiedEmployee": false,
"armsLength": true,
"degree": "BEng",
"field": "Mechatronics",
"yearsExperience": "6",
"githubAuthor": "Rita R",
"githubAuthors": [
"Rita R",
"rita-laptop"
],
"jiraAccountId": null
}
]
}/api/v1/projectsProjects
Every project in the company.
Permission: projects:read
curl -s "https://app.sredify.ca/api/v1/projects" \
-H "Authorization: Bearer $TOKEN"{
"data": [
{
"id": "p_robot",
"name": "Robot arm",
"description": "",
"repos": [
{
"key": "acme/robot",
"language": "TypeScript"
}
],
"teamIds": [
"per_rita"
],
"jiraProjectKeys": [
"ARM"
],
"createdAt": "2026-01-04T17:00:00.000Z",
"updatedAt": "2026-03-01T17:00:00.000Z"
}
]
}/api/v1/projects/{id}One project
A single project.
Permission: projects:read
curl -s "https://app.sredify.ca/api/v1/projects/p_robot" \
-H "Authorization: Bearer $TOKEN"/api/v1/assessmentsAssessments
Saved assessments, newest first, with hours per piece of eligible work (past) or per angle and task (future). Long text (files, implementation and testing notes, conclusion, caveats) is left out here; get one assessment for that. Source code is never included.
Permission: assessments:read
Query
- projectId
- Only assessments of this project's repos.
- repo
- Only this repo (owner/repo).
- kind
- past or future.
- limit
- Items per page, 1 to 200 (default 50).
- cursor
- nextCursor from the previous page.
curl -s "https://app.sredify.ca/api/v1/assessments" \
-H "Authorization: Bearer $TOKEN"/api/v1/assessments/{id}One assessment
A single assessment with the full report: for past work the eligible work with changed file names, verdict, conclusion, not-eligible list and caveats; for future plans the picked angles with each task's hours, implementation and testing notes, files and reasoning. Source code is never included.
Permission: assessments:read
- Hours: hours = after edits made in the app (Adjust hours), originalHours = what the assessment first found, adjustmentReason = the reason typed with the edit (null when not edited). totalHours sums them.
- Future (planning) assessments: angleDetails lists only the angles picked for the report, in report order, each with its tasks (features).
- Past assessments: verdict, eligibleWork (with evidence: commits, file names, Jira keys), contributors, conclusion, notEligible and caveats instead of angleDetails.
- Past assessments: each eligibleWork item lists its people (who worked on it, with commits and hours). Their hours come from the same split as the claim, so they match its salary split. When the SR&ED profile's hours split is off, hours are null and only commits are given.
curl -s "https://app.sredify.ca/api/v1/assessments/as_123" \
-H "Authorization: Bearer $TOKEN"{
"id": "as_plan",
"repo": "acme/robot",
"projectIds": [
"p_robot"
],
"kind": "future",
"title": "Sub-second re-planning",
"createdAt": "2026-03-01T17:00:00.000Z",
"since": null,
"until": null,
"adjusted": true,
"summary": "Two lines of experimental work aimed at re-planning 10,000 stops in under a second.",
"angles": [
"angle-1"
],
"totalHours": 180,
"originalTotalHours": 200,
"angleDetails": [
{
"id": "angle-1",
"title": "Incremental graph re-routing",
"summary": "Only re-solve the parts of the route graph a traffic change touches.",
"eligibility": {
"eligible": true,
"criterion": "Technological uncertainty",
"why": "No known method met the latency target at this scale."
},
"hours": 180,
"originalHours": 200,
"features": [
{
"name": "Partitioned route graph",
"hours": 100,
"originalHours": 120,
"adjustmentReason": "Reuse the existing partitioner",
"hoursRationale": "Three schemes to build and benchmark.",
"implementation": "Split the graph into regions and cache boundary costs.",
"testing": "Benchmark re-plan latency at 1k, 5k and 10k stops.",
"proposedFiles": [
"src/graph/partition.rs"
],
"reasoning": {
"necessity": "A full re-solve takes 9 s at 10k stops.",
"efficiency": null,
"latency": "Target under 1 s.",
"optimization": null
}
},
{
"name": "Change propagation",
"hours": 80,
"originalHours": 80,
"adjustmentReason": null,
"hoursRationale": null,
"implementation": "Push traffic deltas only into affected regions.",
"testing": "Compare against a full re-solve on recorded traffic days.",
"proposedFiles": [
"src/graph/propagate.rs"
],
"reasoning": null
}
]
}
],
"conclusion": "The angle shows a clear technical unknown and a planned set of experiments, which is what SR&ED looks for."
}/api/v1/assessments/{id}/work/{workId}Change eligible work hours
Change a past report's eligible item: its total hours, each person's share of them, or both. The change counts in the claim right away and shows your app's name and reason in Sredify and on the claim PDF.
Permission: assessments:write
- Admins only: assessments:write is a sensitive permission, like pay:read. It stops working if the Admin who approved the app is no longer an Admin.
- Newest change wins, whoever made it (a person in Sredify or an app). What it replaced is kept (replaced[]) and can be restored in Sredify.
- A person's share: they must be on People, on the project (ticked on its Team or a GitHub contributor in its repos), and have a GitHub author linked (canHaveShare). Not less than their logged Jira time on the item (minHours).
- Shares on an item can't add up to more than its hours. Everyone without a share is scaled to fit what is left, so hours by authors not on People can be given to the team.
- More than the activity supports (over minHours + roomHours) is accepted and flagged: overActivity in the answer, overActivityHours on the person, and a note in the app and PDF.
Body (JSON)
- reason *string
- Why the hours change. Required. Shown in Sredify and the claim PDF.
- hoursnumber
- The item's total hours. Leave out to keep it.
- peoplearray
- Each person's share of the item. Leave out people you don't change.
- versionnumber
- Optional: the report version you read (GET /assessments/{id}: version). If it changed since, you get 409 changed_since_read instead of overwriting. Send it here, not as an If-Match header.
curl -s -X PATCH "https://app.sredify.ca/api/v1/assessments/p_robot/work/{workId}" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"reason":"Placed 12 h of unplaced hours by activity: Rita +4.75 h, Omar +7.25 h","people":[{"personId":"per_rita","hours":22.75},{"personId":"per_omar","hours":17.25}]}'/api/v1/assessments/{id}/work/{workId}/changesUndo your hours changes
Undo your app's (or key's) current changes on an eligible item: each value goes back to what it replaced.
Permission: assessments:write
- Only changes your own app or key made. If someone changed the item after you, you get 403 not_yours (newest change wins).
curl -s -X DELETE "https://app.sredify.ca/api/v1/assessments/p_robot/work/{workId}/changes?reason=Entered%20twice" \
-H "Authorization: Bearer $TOKEN"/api/v1/hoursHours
Hours per person per project: estimated, recorded, and what counts. Recorded time replaces the estimate for that person on that day; it is never added on top.
Permission: hours:read
Query
- projectId
- Only this project.
- from
- Start day, yyyy-mm-dd. Send with to. Default: the project's claim dates (last fiscal year).
- to
- End day, yyyy-mm-dd.
curl -s "https://app.sredify.ca/api/v1/hours" \
-H "Authorization: Bearer $TOKEN"{
"from": "2025-01-01",
"to": "2025-12-31",
"rule": "Recorded time replaces the estimate for that person on that day; it is never added on top.",
"hours": [
{
"projectId": "p_robot",
"personId": "per_rita",
"name": "Rita Robotics",
"onTeam": true,
"estimated": 412.5,
"replacedByRecorded": 6,
"recorded": 7.5,
"recordedDays": 1,
"total": 420,
"recordedByDay": {
"2025-03-14": {
"estimateReplaced": 6
}
}
}
]
}/api/v1/hoursRecord time
Record time a person worked on a project. It counts in the claim right away, labelled with your key or app.
Permission: hours:write
- The person must be on the company's People list and on the project: ticked on its Team, or linked to a GitHub contributor with commits in its repos.
- Hours: more than 0 and at most 24. The person's recorded total that day, across all projects, can't go over the company's daily maximum (GET /org: sred.maxHoursPerDay, default 10).
- Not in the future. Recorded time replaces that day's estimate for the person.
Query and headers
- Idempotency-Key
- Any unique string (for example a UUID). Sending the same key again within 24 hours returns the first result instead of adding twice.
Body (JSON)
- projectId *string
- The project id (GET /projects).
- personIdstring
- The person's id. Or send email instead.
- emailstring
- The person's email on the People list.
- date *string
- The day the work happened. Not in the future.
- hours *number
- More than 0, at most 24. Rounded to 0.1.
- description *string
- What they worked on (at most 300 characters).
- notesstring
- Optional longer notes (at most 5000 characters).
curl -s -X POST "https://app.sredify.ca/api/v1/hours" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"projectId":"p_robot","email":"rita@acme.example","date":"2026-03-14","hours":7.5,"description":"Grip force tests on prototype 3"}'{
"id": "ev_mf2k1q_a8c3d1",
"projectId": "p_robot",
"type": "time",
"date": "2026-03-14",
"personId": "per_rita",
"hours": 7.5,
"title": "Grip force tests on prototype 3",
"body": null,
"url": null,
"source": {
"kind": "app",
"id": "cmuabc123",
"label": "Acme Timer",
"approvedBy": "Ann Admin"
},
"createdAt": "2026-03-14T23:10:00.000Z",
"updatedAt": "2026-03-14T23:10:00.000Z",
"removed": null
}/api/v1/hours/{id}Change a time entry
Change a time entry your key or app added. Send only the fields to change. The person can't change: remove the entry and add a new one.
Permission: hours:write
- Only items your own key or app added (403 not_yours otherwise).
Body (JSON)
- datestring
- The day the work happened.
- hoursnumber
- More than 0, at most 24.
- descriptionstring
- What they worked on.
- notesstring | null
- Send null or empty to clear.
curl -s -X PATCH "https://app.sredify.ca/api/v1/hours/ev_mf2k1q_a8c3d1" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"hours":6}'/api/v1/hours/{id}Remove a time entry
Remove a time entry your key or app added. It stops counting and is kept in history with the reason.
Permission: hours:write
Query and headers
- reason *
- Why it's removed (shown in the app's history).
curl -s -X DELETE "https://app.sredify.ca/api/v1/hours/ev_mf2k1q_a8c3d1?reason=Entered%20twice" \
-H "Authorization: Bearer $TOKEN"/api/v1/claims/{projectId}A project's claim
The claim numbers for a project, calculated exactly as on the Claim screen.
Permission: claims:read
- Salary amounts, totals and the credit estimate need pay:read too. Without it you get hours and shares only, plus a note.
Query
- from
- Start day, yyyy-mm-dd. Send with to. Default: the project's claim dates (last fiscal year).
- to
- End day, yyyy-mm-dd.
curl -s "https://app.sredify.ca/api/v1/claims/p_robot" \
-H "Authorization: Bearer $TOKEN"/api/v1/evidenceRecorded time, notes, links and supporting evidence
Everything added as evidence (in the app, by keys and by apps), oldest change first. Use since= to sync only what changed.
Permission: evidence:read
Query
- projectId
- Only this project.
- type
- time, note, link, meeting, reference, step or test.
- assessmentId
- Supporting evidence of this report only.
- workId
- Supporting evidence of this eligible work item only.
- since
- Only items changed at or after this ISO date or date-time.
- includeRemoved
- true to include removed items.
- limit
- Items per page, 1 to 200 (default 50).
- cursor
- nextCursor from the previous page.
curl -s "https://app.sredify.ca/api/v1/evidence" \
-H "Authorization: Bearer $TOKEN"/api/v1/evidenceAdd a note, link or supporting evidence
Add a note or a link (a design doc, a test report, a ticket) to a project, or supporting evidence (a meeting, research reference, engineering step or test) to one eligible work item of a past report.
Permission: evidence:write
- Links must be full https:// (or http://) addresses.
- Supporting evidence: send the WorkEvidenceInput shape (assessmentId, workId, type meeting | reference | step | test). Example: { assessmentId: "as_1", workId: "grip-force", type: "meeting", date: "2025-03-11", title: "Review soft-object crush results", minutes: 45, personIds: ["per_rita"], result: "Try a torque cap next", suggestion: { confirmedAs: "edited", confirmedByPersonId: "per_rita" } }.
- Supporting evidence backs up the item's hours. It never adds to them or replaces them, and is shown under the item in Sredify and in the claim PDF.
- Only send what a person confirmed. If your app suggested it, send suggestion with who confirmed it and whether they edited it.
- Checked: the date is inside the report's dates and not in the future, people are on People and on the project, minutes 1 to 480, commits belong to the item.
Query and headers
- Idempotency-Key
- Any unique string (for example a UUID). Sending the same key again within 24 hours returns the first result instead of adding twice.
Body (JSON)
- projectId *string
- The project id (GET /projects).
- type *string
- One of: note, link.
- date *string
- The day it's about. Not in the future.
- title *string
- Short title (at most 300 characters).
- bodystring
- Optional text (at most 5000 characters).
- urlstring
- Links only: a full https:// address.
curl -s -X POST "https://app.sredify.ca/api/v1/evidence" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"projectId":"p_robot","type":"link","date":"2026-03-14","title":"Grip test report","url":"https://docs.acme.example/grip-tests"}'/api/v1/evidence/{id}One evidence item
A single item, removed or not.
Permission: evidence:read
curl -s "https://app.sredify.ca/api/v1/evidence/ev_mf2k1q_a8c3d1" \
-H "Authorization: Bearer $TOKEN"/api/v1/evidence/{id}Change a note, link or supporting evidence
Change an item your key or app added: send only the fields to change (null clears one). Supporting evidence can't move to another item or type. Time entries need hours:write instead.
Permission: evidence:write
Body (JSON)
- datestring
- A day, yyyy-mm-dd.
- titlestring
- Short title.
- bodystring | null
- Send null or empty to clear.
- urlstring
- Links only.
curl -s -X PATCH "https://app.sredify.ca/api/v1/evidence/ev_mf2k1q_a8c3d1" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"Grip test report v2"}'/api/v1/evidence/{id}Remove a note, link or supporting evidence
Remove an item your key or app added. Kept in history with the reason. Time entries need hours:write instead.
Permission: evidence:write
Query and headers
- reason *
- Why it's removed (shown in the app's history).
curl -s -X DELETE "https://app.sredify.ca/api/v1/evidence/ev_mf2k1q_a8c3d1?reason=Entered%20twice" \
-H "Authorization: Bearer $TOKEN"