Developers
Borderless API
Read your team, their hours and their time off from Borderless in your own software, and receive each change as it happens through webhooks. The numbers are the ones on your hours screen and your invoice.
Before you start
- Every call carries your key: Authorization: Bearer bk_live_... An owner or admin creates it in the Borderless portal, under Settings, Integrations, API keys, and sees it once. Keep it on your server.
- A key reads one company and only reads: nothing in this API changes anything in Borderless.
- No rates, pay, bank details, identity documents or contracts are ever returned.
- At most 60 calls a minute per key. Past that you get 429 and a Retry-After header. To stay in sync, poll /time-entries with updated_since every few minutes rather than fetching whole months.
- Errors come back as { "error": { "code", "message" } } with the HTTP status: 400 for a parameter, 401 for a missing or revoked key, 429 for too many calls, 500 if something failed on our side.
- Dates are YYYY-MM-DD. Instants are ISO 8601 in UTC. The payment cycle runs from the 26th to the 25th and closes at the end of the 25th, Mexico City time.
curl -H "Authorization: Bearer $BORDERLESS_API_KEY" \ "https://www.borderlesssolutions.company/api/v1/time-entries?from=2026-09-26&to=2026-10-25"
GET/api/v1/me
Which company the key reads. Make this call first with a new key.
{
"company": { "id": "6f1c...", "name": "Strut Company", "timezone": "America/Denver" },
"rate_limit_per_minute": 60
}GET/api/v1/contractors
Your contractors, one row per engagement with your company.
- status
- active (default) or all, which adds engagements that have ended.
{
"data": [{
"id": "3c62...", "engagement_id": "875e...", "name": "Ana PΓ©rez",
"email": "ana@example.com", "country": "MX", "timezone": "America/Mexico_City",
"role": "Marketplace Specialist", "status": "active",
"start_date": "2026-03-01", "end_date": null,
"weekly_hours": 40, "pay_basis": "hourly"
}]
}GET/api/v1/time-entries
Each time entry between two days, as it counts on your invoice: when a contractor logged the same day in two trackers, that day counts once. Timers still running appear once they are stopped.
- from, to
- Required. YYYY-MM-DD, inclusive, at most 93 days apart. A day is the contractor's own calendar day.
- contractor_id
- Only this contractor.
- updated_since
- An ISO 8601 instant. Returns every entry created, edited or deleted since then, so you only fetch what changed. Deleted entries come back with deleted_at set: remove them on your side.
- limit, cursor
- Page size (default 500, at most 1000) and the next_cursor of the previous page.
{
"data": [{
"id": "191c...", "contractor_id": "3c62...", "engagement_id": "875e...",
"date": "2026-10-20", "start": "2026-10-20T14:00:00+00:00", "end": "2026-10-20T18:00:00+00:00",
"duration_seconds": 14400, "hours": 4, "description": "Seasonal review",
"source": "borderless", "manual": false, "billable": true,
"created_at": "...", "updated_at": "...", "deleted_at": null
}],
"next_cursor": "NTAw", "total": 812
}GET/api/v1/hours
Hours per contractor. For a payment cycle (the 26th to the 25th) it adds the hours expected (working days times their daily hours, less approved time off) and the hours past them. Every hour is billed at the same rate: overtime is a count, not a surcharge.
- cycle
- current (default), previous, or any YYYY-MM-DD inside the cycle.
- from, to
- Any range instead of a cycle. No expected hours then: they only exist for a whole cycle.
{
"from": "2026-09-26", "to": "2026-10-25", "cycle": true,
"data": [{
"contractor_id": "3c62...", "name": "Ana PΓ©rez", "hours": 152.5,
"borderless_hours": 152.5, "clockify_hours": 0, "days_worked": 19,
"expected_hours": 160, "overtime_hours": 0, "weekly_hours": 40
}]
}GET/api/v1/time-off
Time off and shift changes that touch a range. Hours of a window are on the contractor's own clock (windows_timezone).
- from, to
- Required. YYYY-MM-DD, at most 93 days apart.
- status
- all (default), pending, approved or rejected.
{
"data": [{
"id": "8a2f...", "contractor_id": "3c62...", "status": "approved",
"kind": "part_of_day", "start_date": "2026-10-15", "end_date": "2026-10-15",
"hours_off": 5, "off_windows": [{ "date": "2026-10-15", "start": "11:30", "end": "16:30" }],
"make_up_windows": [], "make_up_plan": null, "windows_timezone": "America/Mexico_City",
"reason": "Doctor's appointment", "requested_at": "...", "reviewed_at": "...", "review_note": null
}]
}Webhooks
Instead of asking us every few minutes, let us tell you: we send each event to your URL the moment it happens.
- An owner or admin adds your URL in the Borderless portal, under Settings, Integrations, Webhooks, picks the events, and gets a signing secret. The URL must be https and on the public internet.
- Each event is a POST with a JSON body. Answer any 2xx within 10 seconds; do the real work afterwards. Redirects are not followed.
- Anything else is retried after 1, 5, 30, 120, 360 minutes, 6 attempts in all. A retry carries the same body and the same Borderless-Delivery header.
- The same event can arrive twice. Keep the id of the events you have handled and skip one you have seen.
- Events can arrive out of order. Each one carries the whole thing after the change; when order matters, read it again from the API.
- Check the signature on every request before you trust it, as below. Reject a timestamp more than 5 minutes old.
- time_entry.created
- A contractor finished a timer or added an entry by hand. data: a time entry, as /time-entries returns it.
- time_entry.updated
- An entry was edited. data: the entry after the edit.
- time_entry.deleted
- An entry was deleted. data: the entry, with deleted_at set.
- time_off.requested
- A contractor asked for time off or a shift change. data: the request, as /time-off returns it.
- time_off.updated
- A request's dates or hours changed. data: the request after the change.
- time_off.approved
- A request was approved. data: the request.
- time_off.rejected
- A request was rejected. data: the request.
- time_off.withdrawn
- The contractor withdrew a pending request. data: the request.
- hours.over_expected
- A contractor went past the cycle's expected hours. Sent once per contractor and cycle. data: contractor_id, name, cycle_start, cycle_end, expected_hours, hours, overtime_hours, your_hours.
- invoice.sent
- An invoice was sent to your billing email. data: invoice_id, number, period_start, period_end, total, currency, due_date.
- ping
- A test, sent from the settings screen. data: { message }.
POST https://your-system.example.com/borderless
Content-Type: application/json
Borderless-Event: time_off.approved
Borderless-Delivery: 5d0c...
Borderless-Signature: t=1792934400,v1=6f2a...
{
"id": "evt_9b1e...",
"type": "time_off.approved",
"created_at": "2026-10-22T16:00:00.000Z",
"company_id": "6f1c...",
"data": { "id": "8a2f...", "contractor_id": "3c62...", "status": "approved", "kind": "full_days", ... }
}// Node.js. Read the body as raw text: re-serialising parsed JSON changes the bytes.
import crypto from "node:crypto";
function isFromBorderless(rawBody, header, secret) {
const m = /^t=(d+),v1=([0-9a-f]{64})$/.exec(header ?? "");
if (!m) return false;
if (Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(m[1] + "." + rawBody).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(m[2]));
}Questions or a field you need: contact@borderlesssolutions.company. Version 1, October 2026.