JUST-IN API
An official REST API for connecting external systems to JUST-IN — real-time read access to attendance and organizational data, delivered securely and with full auditability.
Core security principle: every API token belongs to the company itself — not to a user within it. The system identifies the company directly from the token, so there is no way for an external system to request (accidentally or intentionally) another company's data. Full details in Security & Privacy.
Quickstart
Three steps from your first login to your first JSON response:
1. Create an API Token
Log in to JUST-IN as a company admin → Company Settings → API tab → Create Token. Give it a descriptive name (e.g. the name of the connecting system) and select the required abilities. The full token is shown only once — store it somewhere safe.
2. Send your first request
curl -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" \ https://just-in.co.il/api/v1/employees
const res = await fetch('https://just-in.co.il/api/v1/employees', { headers: { 'Authorization': `Bearer ${TOKEN}`, 'Accept': 'application/json' } }); const json = await res.json(); console.log(json.data);
import requests res = requests.get( "https://just-in.co.il/api/v1/employees", headers={"Authorization": f"Bearer {TOKEN}", "Accept": "application/json"} ) print(res.json()["data"])
3. Read the response
Every successful response is wrapped in a consistent envelope with data, and for list endpoints also meta and links for pagination. Full details further down this page.
Authentication
The API uses Bearer Tokens. Every request must include the following headers:
| Header | Value | Required |
|---|---|---|
Authorization | Bearer <token> | Required |
Accept | application/json | Required |
There is no endpoint for creating tokens via the API itself. Tokens are created and revoked exclusively through the admin interface (Company Settings → API) by an authenticated company admin — this is an intentional safeguard.
A request with no token, an invalid token, or a revoked token immediately returns 401 Unauthorized.
Abilities
Every token is granted specific abilities when it's created. A request to an endpoint the token isn't authorized for returns 403 Forbidden — even if the token itself is valid.
| Ability | Grants access to |
|---|---|
| attendance:read | GET /attendance |
| attendance:location:read | Adds the location object (GPS coordinates) to GET /attendance records — requires attendance:read to also be granted. See Attendance. |
| employees:read | GET /employees, GET /employees/:id, GET /departments, GET /branches |
| employees:write | POST /employees, PATCH /employees/:id, POST /employees/:id/deactivate, POST /employees/:id/activate — a separate ability from employees:read, so an integration that only needs to create/freeze employees doesn't also need blanket read access to your whole roster. |
| leave:read | GET /leave |
| shifts:read | GET /shifts |
attendance:location:read is intentionally a separate ability from attendance:read, not a field that's always included. A system that only needs work hours (e.g. payroll) doesn't need to also receive precise employee location data — grant it only to integrations that actually require it.
Write safety: employees:write can never set or change an employee's role — every employee created through the API is a plain employee, never a company admin. Status changes (freeze/activate) are only possible through the dedicated /deactivate and /activate endpoints, never as a side effect of a generic update.
Versioning
The API is versioned starting from v1 — it appears in the base URL itself (/api/v1/...). Backward-incompatible changes (removing a field, changing the meaning of a parameter) will appear under a new version (/api/v2) and will never break an existing integration against v1. Adding a new field to a response is not considered a breaking change.
Rate Limits
Every token is limited to 60 requests per minute. The limit applies per token, not per IP address — multiple external systems can operate from the same network address without affecting each other's quota.
| Response Header | Meaning |
|---|---|
X-RateLimit-Limit | Number of requests allowed in the current window (60) |
X-RateLimit-Remaining | Requests remaining in the current window |
Retry-After | Present only on a 429 response — seconds to wait before retrying |
Exceeding the quota returns 429 Too Many Requests. We recommend implementing exponential backoff rather than retrying immediately — see Best Practices.
Pagination
Endpoints backed by potentially large collections — /attendance, /employees, /leave — are automatically paginated. The default is 50 records per page, controllable via the per_page parameter (maximum 200).
/departments, /branches, and /shifts are the exception: they return the full, unpaginated list in a plain data array with no links/meta envelope. These are small reference/lookup lists by nature (a company typically has a handful to a few dozen of each), so pagination would only add overhead — see the Departments, Branches, and Shifts examples below.
{
"data": [ /* array of records */ ],
"links": {
"first": "https://just-in.co.il/api/v1/employees?page=1",
"last": "https://just-in.co.il/api/v1/employees?page=3",
"prev": null,
"next": "https://just-in.co.il/api/v1/employees?page=2"
},
"meta": {
"current_page": 1,
"last_page": 3,
"per_page": 50,
"total": 142
}
}
Errors
Standard HTTP status codes. Errors always return in the shape {"message": "..."} (and sometimes a detailed errors object for 422s).
| Code | Name | Meaning |
|---|---|---|
| 200 | OK | The request succeeded |
| 401 | Unauthorized | Token missing, invalid, or revoked |
| 403 | Forbidden | The token is valid but lacks the ability required for this endpoint |
| 404 | Not Found | The record doesn't exist, or belongs to another company (intentionally indistinguishable) |
| 422 | Unprocessable Entity | An invalid parameter was supplied (e.g. a malformed date) |
| 429 | Too Many Requests | Rate limit exceeded |
{
"message": "The date from field must be a valid date.",
"errors": {
"date_from": ["The date from field must be a valid date."]
}
}
Attendance Records
Returns a list of attendance records (one working day per employee) with rich filtering options.
Query Parameters
| Name | Type | Description |
|---|---|---|
date_from | date | Start date, inclusive (YYYY-MM-DD) |
date_to | date | End date, inclusive (YYYY-MM-DD) |
employee_id | integer | Filter by a specific employee |
department_id | integer | Filter by the employee's department |
branch_id | integer | Filter by the employee's branch |
status | string | Filter by record status |
per_page | integer | Results per page — default 50, maximum 200 |
Example Request
curl -G https://just-in.co.il/api/v1/attendance \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" \ --data-urlencode "date_from=2026-08-01" \ --data-urlencode "date_to=2026-08-31"
const params = new URLSearchParams({ date_from: '2026-08-01', date_to: '2026-08-31' }); const res = await fetch(`https://just-in.co.il/api/v1/attendance?${params}`, { headers: { 'Authorization': `Bearer ${TOKEN}`, 'Accept': 'application/json' } });
res = requests.get( "https://just-in.co.il/api/v1/attendance", headers={"Authorization": f"Bearer {TOKEN}", "Accept": "application/json"}, params={"date_from": "2026-08-01", "date_to": "2026-08-31"} )
Record Shape
Example Response — 200 OK
{
"data": [
{
"id": 4821,
"employee_id": 45,
"date": "2026-08-15",
"check_in": "2026-08-15T08:02:00+03:00",
"check_out": "2026-08-15T17:05:00+03:00",
"status": "approved",
"absence_type": null,
"is_approved": true
}
],
"links": { "first": "...", "last": "...", "prev": null, "next": null },
"meta": { "current_page": 1, "last_page": 1, "per_page": 50, "total": 1 }
}
Example Response — with attendance:location:read
{
"data": [
{
"id": 4821,
"employee_id": 45,
"date": "2026-08-15",
"check_in": "2026-08-15T08:02:00+03:00",
"check_out": "2026-08-15T17:05:00+03:00",
"status": "approved",
"absence_type": null,
"is_approved": true,
"location": {
"check_in": { "lat": 32.0853, "lng": 34.7818, "accuracy_meters": 12 },
"check_out": { "lat": 32.0853, "lng": 34.7818, "accuracy_meters": 8 }
}
}
],
"links": { "first": "...", "last": "...", "prev": null, "next": null },
"meta": { "current_page": 1, "last_page": 1, "per_page": 50, "total": 1 }
}
Employee List
Returns the company's employee list, with basic contact details and organizational structure. Does not include salary or national ID number data — see Security & Privacy.
Query Parameters
| Name | Type | Description |
|---|---|---|
department_id | integer | Filter by department |
branch_id | integer | Filter by branch |
status | string | Filter by employee status |
per_page | integer | Results per page — default 50, maximum 200 |
Example Request
curl https://just-in.co.il/api/v1/employees \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
const res = await fetch('https://just-in.co.il/api/v1/employees', { headers: { 'Authorization': `Bearer ${TOKEN}`, 'Accept': 'application/json' } });
res = requests.get( "https://just-in.co.il/api/v1/employees", headers={"Authorization": f"Bearer {TOKEN}", "Accept": "application/json"} )
Record Shape
Salary fields, vacation/sick balances, and national ID number are never exposed by this endpoint, intentionally — even with full employees:read access.
Example Response — 200 OK
{
"data": [
{
"id": 45,
"name": "John Doe",
"email": "john.doe@example.com",
"phone": "0501234567",
"role": "employee",
"status": "active",
"department": { "id": 3, "name": "Sales" },
"branch": { "id": 1, "name": "Tel Aviv Branch" }
}
],
"meta": { "current_page": 1, "last_page": 1, "per_page": 50, "total": 1 }
}
Single Employee
Returns a single employee by ID. Same data shape as a record in the /employees list, wrapped under data with no pagination.
curl https://just-in.co.il/api/v1/employees/45 \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
Attempting to access an employee ID that belongs to another company returns 404 Not Found — not 403 — so as not to reveal even the existence of the record to anyone who shouldn't know about it.
Create an Employee
Creates a new employee in your company. role is always set to employee server-side and cannot be supplied by the caller. A login password is generated automatically and sent to the employee by SMS, exactly like creating an employee from the internal admin screen — it is never accepted as input and never appears in the API response.
Body Parameters
| Name | Type | Required | Description |
|---|---|---|---|
name | string | Required | Max 255 characters |
id_number | string | Required | Israeli national ID (8-9 digits). Validated against the Ministry of Interior checksum algorithm and must be unique. |
email | string | Required | Must be unique across the system |
phone | string | Required | 9-15 characters — the welcome SMS is sent here |
department_id | integer | Optional | Must belong to your own company |
branch_id | integer | Optional | Must belong to your own company |
salary_type | string | Required | One of hourly, global |
hourly_rate | number | Conditional | Required when salary_type is hourly |
global_salary | number | Conditional | Required when salary_type is global |
curl -X POST https://just-in.co.il/api/v1/employees \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"name":"John Doe","id_number":"123456782","email":"john.doe@example.com","phone":"0501234567","salary_type":"hourly","hourly_rate":45}'
{
"data": {
"id": 312,
"name": "John Doe",
"email": "john.doe@example.com",
"phone": "0501234567",
"role": "employee",
"status": "active",
"department": null,
"branch": null
}
}
The employee's login password is generated randomly on the server and delivered via SMS to the phone you supplied — exactly as if they'd been added through the admin UI. There is no way to set or retrieve the password through the API.
Update an Employee
Updates basic profile fields for an existing employee. All fields are optional — send only what you want to change. role, status, id_number, and salary fields can never be changed through this endpoint.
Body Parameters
| Name | Type | Description |
|---|---|---|
name | string | Max 255 characters |
email | string | Must remain unique |
phone | string | null | Max 20 characters |
address | string | null | Max 255 characters |
department_id | integer | null | Must belong to your own company |
branch_id | integer | null | Must belong to your own company |
curl -X PATCH https://just-in.co.il/api/v1/employees/312 \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"department_id":3}'
Freeze / Reactivate an Employee
Freezes the employee — they can no longer log in or punch in/out, but every historical record is preserved. This mirrors the internal "freeze employee" action, which until now was only available to a super-admin; it's now available to company admins as well, and by extension to the API.
Reverses a freeze — sets the employee's status back to active.
curl -X POST https://just-in.co.il/api/v1/employees/312/deactivate \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
{
"message": "העובד הוקפא בהצלחה.",
"data": { "id": 312, "status": "frozen", /* ...rest of Employee shape */ }
}
Departments
List of all departments in the company, unpaginated (flat list).
{
"data": [
{ "id": 1, "name": "Sales" },
{ "id": 2, "name": "Development" }
]
}
Branches
List of all branches in the company, unpaginated.
{
"data": [
{ "id": 1, "name": "Tel Aviv Branch", "address": "3 HaBarzel St." }
]
}
Leave Requests
Returns the company's leave/absence requests — vacation, sick leave, reserve duty, and similar. This is a separate ability from employees:read on purpose: a system that only needs organizational structure or attendance data doesn't necessarily need to know who requested sick leave.
Query Parameters
| Name | Type | Description |
|---|---|---|
date_from | string (date) | Only requests on or after this date |
date_to | string (date) | Only requests on or before this date |
employee_id | integer | Filter by employee |
status | string | One of pending, approved, rejected |
type | string | Filter by exact leave type value (see Record Shape below — e.g. מחלה) |
per_page | integer | Results per page — default 50, maximum 200 |
Example Request
curl -G https://just-in.co.il/api/v1/leave \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" \ --data-urlencode "status=approved"
Record Shape
Example Response — 200 OK
{
"data": [
{
"id": 312,
"employee_id": 45,
"date": "2026-08-20",
"type": "מחלה",
"status": "approved",
"manager_note": null,
"has_document": true,
"created_at": "2026-08-18T09:12:00+03:00",
"updated_at": "2026-08-18T14:30:00+03:00"
}
],
"links": { "first": "...", "last": "...", "prev": null, "next": null },
"meta": { "current_page": 1, "last_page": 1, "per_page": 50, "total": 1 }
}
Shifts (Wage-Rate Presets)
Returns the company's list of shift wage-rate presets, unpaginated.
Naming note: in JUST-IN today, "shifts" (משמרות) means a small list of named wage-rate presets — a name, a time range, and a wage percentage multiplier used for payroll (e.g. "Night Shift", 22:00–06:00, 150%). It is not an employee scheduling/roster feature — JUST-IN doesn't currently have shift assignment or a timetable of who's working when. This endpoint reflects that: it's a small reference list, the same shape as Departments and Branches.
Record Shape
{
"data": [
{ "id": 1, "name": "משמרת רגילה", "start_time": "08:00", "end_time": "16:00", "wage_percentage": 100 },
{ "id": 2, "name": "משמרת לילה", "start_time": "22:00", "end_time": "06:00", "wage_percentage": 150 }
]
}
Webhooks — Overview & Setup
Instead of polling the API on a schedule, you can register a URL to receive a POST request the moment something happens — a punch, a leave-request decision, or an employee change. This is push, not pull: lower latency, and far fewer wasted requests against your rate limit.
Webhook endpoints are registered only through the admin interface — Company Settings → Webhooks — by an authenticated company admin. There is intentionally no API endpoint to create or manage webhooks, the same safeguard already in place for API tokens themselves (see Authentication). This prevents a leaked read-only token from being able to redirect your event stream somewhere else.
When you register a webhook you choose a target URL (must be https://) and which event types to receive. You're immediately shown a signing secret — copy it right away, it's shown only once. Every request we send to your URL carries an X-JustIn-Signature header computed from that secret, so you can verify it really came from us (see Verifying Signatures below).
Delivery format
Every webhook call is an HTTP POST with a JSON body:
{
"event": "attendance.clocked_in",
"data": {
"employee_id": 45,
"attendance_id": 4821,
"check_in": "2026-08-15T08:02:00+03:00"
},
"sent_at": "2026-08-15T08:02:01+03:00"
}
Retries
If your endpoint doesn't respond with a 2xx status (including timeouts or connection errors), we retry automatically with increasing backoff — up to 5 attempts over roughly 40 minutes (10s, 30s, 2m, 10m, 30m). Every attempt, successful or not, is recorded and visible in the Webhooks settings page so you can diagnose delivery problems yourself.
Verifying Signatures
Every request includes an X-JustIn-Signature header in the form sha256=<hex digest> — an HMAC-SHA256 of the exact raw request body, keyed with your endpoint's signing secret. Recompute it on your side and compare (using a constant-time comparison) before trusting the payload.
const crypto = require('crypto'); function isValid(rawBody, header, secret) { const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex'); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header)); }
import hmac, hashlib def is_valid(raw_body: bytes, header: str, secret: str) -> bool: expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, header)
Event Catalog
| Event | Fired when | data fields |
|---|---|---|
attendance.clocked_in | An employee punches in (including returning from a break) | employee_id, attendance_id, check_in |
attendance.clocked_out | An employee punches out | employee_id, attendance_id, check_in, check_out |
leave_request.approved | A manager approves a leave/absence request | leave_request_id, employee_id, date, type, status, manager_note |
leave_request.rejected | A manager rejects a leave/absence request | Same shape as leave_request.approved |
employee.created | A new employee is created (via the admin UI or POST /employees) | employee_id, name, email, department_id, branch_id |
employee.updated | An employee's profile or status changes (via the admin UI, PATCH /employees/:id, or a freeze/activate call) | employee_id, name, email, and usually status and/or organizational fields depending on what changed |
Security & Privacy
The API was designed around one principle: complete isolation between companies. Here's how it's actually enforced, not just stated:
Best Practices
Handle 429s with backoff
If you receive a 429, wait according to the Retry-After header before retrying. Don't send repeated requests immediately — that only extends your effective block time.
Loop through all pages
Don't assume all results arrive on a single page. Track meta.current_page against meta.last_page, or simply keep following links.next until it's null.
Keep tokens secret
Don't embed tokens in visible client-side (frontend) code, and don't commit them to a code repository (git). Treat them like a password — if a token leaks, revoke it immediately through the interface and create a new one.
Use a separate token per system
Instead of one token for everything, create a dedicated token for each external system with only the minimum abilities it actually needs. That way revoking access for one system doesn't affect the others, and the audit log stays clear.
Changelog
New abilities, a Write API for employees, outbound Webhooks, and a Shifts endpoint.
Added /leave (leave/absence requests, behind a new leave:read ability). Added GPS coordinates to /attendance records, behind a new attendance:location:read ability. Added POST /employees, PATCH /employees/:id, POST /employees/:id/deactivate, and POST /employees/:id/activate, behind a new employees:write ability — including a company-admin employee freeze/activate capability that was previously super-admin only. Added GET /shifts (wage-rate presets) behind a new shifts:read ability. Added real-time outbound Webhooks for attendance.clocked_in/clocked_out, leave_request.approved/rejected, and employee.created/updated, configured from Company Settings → Webhooks with HMAC-SHA256 request signing. Published a machine-readable OpenAPI 3.0 spec covering every endpoint, importable directly into Postman/Insomnia. Clarified in these docs that /departments, /branches, and /shifts are intentionally unpaginated.
Initial release of the public API.
Added /attendance, /employees, /employees/:id, /departments, /branches. Company-scoped token authentication, precise abilities, rate limiting, and audit logging.