Developer docs
API Documentation
Everything you need to call TimeSpan's scheduling optimization endpoints from your own stack.
Quick start
There are two ways to call TimeSpan's solve endpoints:
- API key (server-to-server, stateless). Create a TimeSpan account, generate a key from Dashboard → API keys, then send your full dataset in the request body with an
Authorization: Bearerheader — nothing is read from or written to your account, you get the solved result straight back. - Dashboard session (stateful). Sign in, add employees/shifts (or load demo data) in the dashboard, then call the same endpoint from your signed-in browser session — the result is saved to your account and shows up in the UI too.
- Either way, read back the score, constraint breakdown, and metrics in the JSON response.
Base URL: https://www.timespan.online. A machine-readable OpenAPI 3.1 spec of every endpoint is at /openapi.json — import it into Postman, Insomnia, or your codegen tool of choice.
Authentication
API keys — pass Authorization: Bearer ts_live_... on any solve endpoint. Keys are created and revoked from Dashboard → API keys, are shown in full only once at creation, and stored as a hash — TimeSpan can't recover a lost key, only issue a new one. API-key calls run in stateless mode: send the full dataset, get the result back, nothing persisted. Rate limits scale with your plan.
Dashboard session — a signed-in Supabase session (email/password or Google OAuth), used by the web UI and any server-side code forwarding that session cookie. This path reads/writes your stored employees, shifts, jobs, and results, scoped by row-level security to your own data or your organization's.
Requests without a valid key or session return 401 Unauthorized.
Employee Shift Scheduling API
Solves shift assignment against skills, availability, overlap and fairness constraints for the employees and shifts currently stored on your account.
POST /api/solve
API key (stateless)
curl -X POST https://www.timespan.online/api/solve \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ts_live_..." \
-d '{
"employees": [
{ "id": "amy", "name": "Amy", "skills": ["barista"], "max_shifts": 5, "unavailable_days": [] }
],
"shifts": [
{ "id": "sat-am", "label": "Saturday AM", "day": 5, "start_hour": 8, "end_hour": 12, "required_skill": "barista" }
]
}'Dashboard session (stateful)
curl -X POST https://www.timespan.online/api/solve \
-H "Content-Type: application/json" \
-H "Cookie: <your session cookie>" \
-d '{
"label": "Week 32 schedule",
"weights": {
"unassignedPenalty": 10,
"sameDayOverlapPenalty": 1,
"fairnessPenalty": 1
}
}'Response
{
"schedule": { "id": "...", "name": "Week 32 schedule", "status": "feasible", ... },
"assignments": [
{ "shift_id": "...", "employee_id": "..." }
],
"score": { "hard": 0, "soft": -14 },
"explanation": [
"Feasible: all hard constraints (skills, availability, overlaps, max load) satisfied.",
"Soft score -14 after 3200 local-search moves (fairness + rest optimization)."
],
"breakdown": [
{ "code": "H1", "label": "Skill mismatch", "severity": "hard", "count": 0, "impact": 0 },
{ "code": "S1", "label": "Fairness deviation", "severity": "soft", "count": 4, "impact": -4 }
],
"metrics": { "coverage": 100, "fairnessIndex": 88, "utilization": 76 }
}weights is optional — omit it to use platform defaults, or pass a saved Config Profile's weights from the scheduler UI.
Task Scheduling API
Solves job-to-resource assignment against skills, dependency ordering, and daily resource capacity for the resources and jobs currently stored on your account.
POST /api/solve-tasks
Same two auth modes as above. API-key calls pass { "resources": [...], "jobs": [...] } in the body instead of reading stored data.
curl -X POST https://www.timespan.online/api/solve-tasks \
-H "Content-Type: application/json" \
-H "Cookie: <your session cookie>" \
-d '{ "label": "Sprint 14 plan" }'Response
{
"run": { "id": "...", "name": "Sprint 14 plan", "status": "feasible", ... },
"assignments": [
{ "job_id": "...", "resource_id": "...", "assigned_day": 2 }
],
"score": { "hard": 0, "soft": -6 },
"breakdown": [
{ "code": "H2", "label": "Dependency ordering violation", "severity": "hard", "count": 0, "impact": 0 },
{ "code": "S2", "label": "Priority-weighted tardiness (days late, summed)", "severity": "soft", "count": 2, "impact": -2 }
],
"metrics": { "coverage": 100, "onTimeRate": 86, "utilization": 71 }
}Field Service Routing API
Assigns and sequences field jobs (site visits) across your technicians, using real drive-time estimates to build feasible, low-travel routes. Solves skill matching, time windows, and shift length together.
POST /api/solve-field-service
API-key calls pass { "technicians": [...], "jobs": [...] } in the body instead of reading stored data.
curl -X POST https://www.timespan.online/api/solve-field-service \
-H "Content-Type: application/json" \
-H "Cookie: <your session cookie>" \
-d '{ "label": "Tuesday routes" }'Response
{
"run": { "id": "...", "name": "Tuesday routes", "status": "feasible", ... },
"assignments": [
{ "job_id": "...", "technician_id": "...", "sequence": 0, "eta_hour": 9.25 }
],
"score": { "hard": 0, "soft": -142 },
"breakdown": [
{ "code": "H2", "label": "Time-window violation", "severity": "hard", "count": 0, "impact": 0 },
{ "code": "S2", "label": "Total drive time (minutes)", "severity": "soft", "count": 142, "impact": -142 }
],
"metrics": { "coverage": 100, "onTimeRate": 100, "totalDriveMinutes": 142, "distanceSource": "google" }
}Drive times come from Google Maps' Distance Matrix API when the platform is configured with a maps key. Without one, the solver falls back to straight-line distance estimates and flags it in the response (distanceSource: "estimated") rather than failing.
Pickup & Delivery Routing API
Classic vehicle routing with pickup-delivery pairs (VRPPD): routes vehicles through pickup and delivery stops while respecting vehicle capacity, pickup-before-delivery precedence on the same vehicle, and time windows on both ends of each job.
POST /api/solve-pickup-delivery
API-key calls pass { "vehicles": [...], "jobs": [...] } in the body instead of reading stored data.
curl -X POST https://www.timespan.online/api/solve-pickup-delivery \
-H "Content-Type: application/json" \
-H "Cookie: <your session cookie>" \
-d '{ "label": "Morning delivery run" }'Response
{
"run": { "id": "...", "name": "Morning delivery run", "status": "feasible", ... },
"assignments": [
{ "job_id": "...", "vehicle_id": "...", "pickup_sequence": 0, "delivery_sequence": 2,
"pickup_eta_hour": 8.5, "delivery_eta_hour": 10.1 }
],
"score": { "hard": 0, "soft": -203 },
"breakdown": [
{ "code": "H2", "label": "Vehicle capacity exceeded", "severity": "hard", "count": 0, "impact": 0 },
{ "code": "S2", "label": "Total drive time (minutes)", "severity": "soft", "count": 203, "impact": -203 }
],
"metrics": { "coverage": 100, "onTimeRate": 92, "totalDriveMinutes": 203, "distanceSource": "google" }
}Copilot API
Ask questions about a solve result in plain language. Copilot is given the constraint breakdown, metrics and score from the run you specify — it never invents facts outside that context.
POST /api/copilot
Authenticate with the same API key as the solve endpoints (Authorization: Bearer ts_live_...), or with a dashboard session cookie. Copilot is a paid feature with a monthly allowance per plan: plans without Copilot (and callers over their monthly limit) receive 402; Enterprise is unlimited. If the service itself is ever unavailable it returns 503 with a Retry-After header, and solve endpoints are unaffected.
curl -X POST https://www.timespan.online/api/copilot \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ts_live_..." \
-d '{
"question": "Why is my score negative?",
"context": { "score_hard": 0, "score_soft": -142, "constraint_breakdown": [...], "metrics": {...} }
}'Response
{ "answer": "Your plan is fully feasible (0 hard violations)..." }SDK snippets
Ready-to-run integration code for every solver. Set TIMESPAN_API_KEY in your environment, pick your language, and paste. All endpoints share the same shape: POST JSON, receive score, assignments and a named breakdown.
Employee Shift Scheduling POST /api/solve
const res = await fetch("https://www.timespan.online/api/solve", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.TIMESPAN_API_KEY}`,
},
body: JSON.stringify({
"employees": [
{ "id": "e1", "name": "Amy", "skills": ["nurse"], "max_shifts": 5, "unavailable_days": [] },
{ "id": "e2", "name": "Ben", "skills": ["nurse"], "max_shifts": 4, "unavailable_days": [2] }
],
"shifts": [
{ "id": "s1", "label": "Mon early", "day": 0, "start_hour": 6, "end_hour": 14, "required_skill": "nurse" },
{ "id": "s2", "label": "Tue late", "day": 1, "start_hour": 14, "end_hour": 22, "required_skill": "nurse" }
]
}),
});
if (!res.ok) throw new Error(`Solve failed: ${res.status} ${(await res.json()).error}`);
const { score, assignments, breakdown, metrics } = await res.json();
console.log(score, assignments);Task Scheduling POST /api/solve-tasks
const res = await fetch("https://www.timespan.online/api/solve-tasks", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.TIMESPAN_API_KEY}`,
},
body: JSON.stringify({
"resources": [
{ "id": "r1", "name": "Dev A", "skills": ["backend"], "capacity_hours_per_day": 8 }
],
"jobs": [
{ "id": "j1", "label": "API build", "target_day": 0, "duration_hours": 4, "priority": 2, "required_skill": "backend", "depends_on": [] },
{ "id": "j2", "label": "Tests", "target_day": 1, "duration_hours": 4, "priority": 1, "required_skill": "backend", "depends_on": ["j1"] }
]
}),
});
if (!res.ok) throw new Error(`Solve failed: ${res.status} ${(await res.json()).error}`);
const { score, assignments, breakdown, metrics } = await res.json();
console.log(score, assignments);Field Service Routing POST /api/solve-field-service
const res = await fetch("https://www.timespan.online/api/solve-field-service", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.TIMESPAN_API_KEY}`,
},
body: JSON.stringify({
"technicians": [
{ "id": "t1", "name": "Tech 1", "skills": ["hvac"], "start_lat": 51.50, "start_lng": -0.10, "shift_start_hour": 8, "shift_end_hour": 17 }
],
"jobs": [
{ "id": "f1", "label": "Repair", "lat": 51.52, "lng": -0.12, "required_skill": "hvac", "duration_minutes": 60, "window_start_hour": 9, "window_end_hour": 12, "priority": 1 }
]
}),
});
if (!res.ok) throw new Error(`Solve failed: ${res.status} ${(await res.json()).error}`);
const { score, assignments, breakdown, metrics } = await res.json();
console.log(score, assignments);Pickup & Delivery Routing POST /api/solve-pickup-delivery
const res = await fetch("https://www.timespan.online/api/solve-pickup-delivery", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.TIMESPAN_API_KEY}`,
},
body: JSON.stringify({
"vehicles": [
{ "id": "v1", "name": "Van 1", "capacity": 10, "start_lat": 51.50, "start_lng": -0.10, "shift_start_hour": 8, "shift_end_hour": 18 }
],
"jobs": [
{ "id": "p1", "label": "Parcel 1", "pickup_lat": 51.51, "pickup_lng": -0.11, "delivery_lat": 51.53, "delivery_lng": -0.13, "demand": 2, "priority": 2, "pickup_window_start_hour": 9, "pickup_window_end_hour": 12, "delivery_window_start_hour": 10, "delivery_window_end_hour": 16 }
]
}),
});
if (!res.ok) throw new Error(`Solve failed: ${res.status} ${(await res.json()).error}`);
const { score, assignments, breakdown, metrics } = await res.json();
console.log(score, assignments);Webhooks
Configure a webhook URL from the dashboard to receive a schedule.solved event every time /api/solve completes. Delivery is best-effort (fire-and-forget) and includes an optional shared secret in the X-Webhook-Secret header for you to verify.
POST <your webhook url>
Content-Type: application/json
X-Webhook-Secret: <your configured secret>
{
"event": "schedule.solved",
"schedule_id": "...",
"score": { "hard": 0, "soft": -14 },
"metrics": { "coverage": 100, "fairnessIndex": 88, "utilization": 76 }
}Rate limits
Dashboard (session) calls are capped at 20 requests per minute per account (plus a secondary per-IP cap). API-key calls are capped per plan instead: 5/min on Launch, 30/min on Team, 120/min on Enterprise. If you exceed the limit you'll get a 429 response with a Retry-After header telling you how many seconds to wait. Copilot is capped separately at 15 requests per minute per account.