JUST-IN API
v1
Base URL: https://just-in.co.il/api/v1

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.

v1.0.1
Current API version
READ + WRITE
Employee endpoints support create/update; everything else stays read-only
60/min
Rate limit per token
+ Webhooks
Real-time push notifications for key events
🔒

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:

HeaderValueRequired
AuthorizationBearer <token>Required
Acceptapplication/jsonRequired
⚠️

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.

AbilityGrants access to
attendance:readGET /attendance
attendance:location:readAdds the location object (GPS coordinates) to GET /attendance records — requires attendance:read to also be granted. See Attendance.
employees:readGET /employees, GET /employees/:id, GET /departments, GET /branches
employees:writePOST /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:readGET /leave
shifts:readGET /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 HeaderMeaning
X-RateLimit-LimitNumber of requests allowed in the current window (60)
X-RateLimit-RemainingRequests remaining in the current window
Retry-AfterPresent 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.

Response shape — pagination
{
  "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).

CodeNameMeaning
200OKThe request succeeded
401UnauthorizedToken missing, invalid, or revoked
403ForbiddenThe token is valid but lacks the ability required for this endpoint
404Not FoundThe record doesn't exist, or belongs to another company (intentionally indistinguishable)
422Unprocessable EntityAn invalid parameter was supplied (e.g. a malformed date)
429Too Many RequestsRate limit exceeded
Example — error response
{
  "message": "The date from field must be a valid date.",
  "errors": {
    "date_from": ["The date from field must be a valid date."]
  }
}

Attendance Records

GET /attendance attendance:read

Returns a list of attendance records (one working day per employee) with rich filtering options.

Query Parameters

NameTypeDescription
date_fromdateStart date, inclusive (YYYY-MM-DD)
date_todateEnd date, inclusive (YYYY-MM-DD)
employee_idintegerFilter by a specific employee
department_idintegerFilter by the employee's department
branch_idintegerFilter by the employee's branch
statusstringFilter by record status
per_pageintegerResults 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

id
integer
Unique identifier of the attendance record
employee_id
integer
Employee identifier (matches the id returned from /employees)
date
string (date)
Work day date, YYYY-MM-DD
check_in
string (ISO 8601) | null
Check-in time, with timezone
check_out
string (ISO 8601) | null
Check-out time, with timezone
status
string
Record status
absence_type
string | null
Absence type, if applicable
is_approved
boolean
Whether the record has been approved by a manager
location
object
Only present when the token also has the attendance:location:read ability (see Abilities). Shape: { check_in: { lat, lng, accuracy_meters } | null, check_out: { lat, lng, accuracy_meters } | null }. accuracy_meters is the device-reported GPS accuracy at the time of the punch and is informational only.

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

GET /employees employees:read

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

NameTypeDescription
department_idintegerFilter by department
branch_idintegerFilter by branch
statusstringFilter by employee status
per_pageintegerResults 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

id
integer
name
string
email
string
phone
string | null
role
string
status
string
department
object | null
{ id, name }
branch
object | null
{ id, name }

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

GET /employees/{id} employees:read

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

POST /employees employees:write

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

NameTypeRequiredDescription
namestringRequiredMax 255 characters
id_numberstringRequiredIsraeli national ID (8-9 digits). Validated against the Ministry of Interior checksum algorithm and must be unique.
emailstringRequiredMust be unique across the system
phonestringRequired9-15 characters — the welcome SMS is sent here
department_idintegerOptionalMust belong to your own company
branch_idintegerOptionalMust belong to your own company
salary_typestringRequiredOne of hourly, global
hourly_ratenumberConditionalRequired when salary_type is hourly
global_salarynumberConditionalRequired when salary_type is global
Example Request
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}'
Example Response — 201 Created
{
  "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

PATCH /employees/{id} employees:write

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

NameTypeDescription
namestringMax 255 characters
emailstringMust remain unique
phonestring | nullMax 20 characters
addressstring | nullMax 255 characters
department_idinteger | nullMust belong to your own company
branch_idinteger | nullMust belong to your own company
Example Request
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

POST /employees/{id}/deactivate employees:write

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.

POST /employees/{id}/activate employees:write

Reverses a freeze — sets the employee's status back to active.

Example Request
curl -X POST https://just-in.co.il/api/v1/employees/312/deactivate \
     -H "Authorization: Bearer YOUR_TOKEN" \
     -H "Accept: application/json"
Example Response — 200 OK
{
  "message": "העובד הוקפא בהצלחה.",
  "data": { "id": 312, "status": "frozen", /* ...rest of Employee shape */ }
}

Departments

GET /departments employees:read

List of all departments in the company, unpaginated (flat list).

id
integer
name
string
{
  "data": [
    { "id": 1, "name": "Sales" },
    { "id": 2, "name": "Development" }
  ]
}

Branches

GET /branches employees:read

List of all branches in the company, unpaginated.

id
integer
name
string
address
string | null
{
  "data": [
    { "id": 1, "name": "Tel Aviv Branch", "address": "3 HaBarzel St." }
  ]
}

Leave Requests

GET /leave leave:read

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

NameTypeDescription
date_fromstring (date)Only requests on or after this date
date_tostring (date)Only requests on or before this date
employee_idintegerFilter by employee
statusstringOne of pending, approved, rejected
typestringFilter by exact leave type value (see Record Shape below — e.g. מחלה)
per_pageintegerResults 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

id
integer
Unique identifier of the leave request
employee_id
integer
Employee identifier (matches the id returned from /employees)
date
string (date)
The requested date, YYYY-MM-DD
type
string
Leave type. Returned exactly as configured in the system (currently Hebrew labels — the same fixed set of values used for absence_type on attendance records, e.g. "חופש" vacation, "מחלה" sick leave, "מילואים" reserve duty, "אבל" bereavement, "היעדרות ללא תשלום" unpaid leave, and others). Treat this as an opaque string for filtering/display rather than parsing it — the exact value set may grow.
status
string
One of pending, approved, rejected
manager_note
string | null
Optional note left by the approving/rejecting manager
has_document
boolean
Whether a supporting document (e.g. a medical certificate) was attached to the request. The document file itself is not exposed through this API.
created_at / updated_at
string (ISO 8601)

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)

GET /shifts shifts:read

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

id
integer
name
string
start_time
string | null
HH:MM
end_time
string | null
HH:MM
wage_percentage
integer
Wage rate multiplier as a percentage, e.g. 100 = regular rate, 150 = time-and-a-half
{
  "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

EventFired whendata fields
attendance.clocked_inAn employee punches in (including returning from a break)employee_id, attendance_id, check_in
attendance.clocked_outAn employee punches outemployee_id, attendance_id, check_in, check_out
leave_request.approvedA manager approves a leave/absence requestleave_request_id, employee_id, date, type, status, manager_note
leave_request.rejectedA manager rejects a leave/absence requestSame shape as leave_request.approved
employee.createdA new employee is created (via the admin UI or POST /employees)employee_id, name, email, department_id, branch_id
employee.updatedAn 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:

Token-based isolation
Every token belongs to the company itself, not to a user within it. The company's identity is derived directly from the token on every request — there is no company_id parameter accepted from the client at all.
Write scope is narrow
Only employee records can be created/updated via the API (behind the separate employees:write ability) — attendance, leave requests, and everything else remain strictly read-only. Even for employees, the write endpoints hard-code role to employee server-side and never accept it from the caller, so there is no path to creating or promoting an admin account through the API. Status changes go through dedicated /deactivate and /activate endpoints only, never a generic field update.
Precise abilities
Every token is limited to exactly the data it was approved for (attendance:read / attendance:location:read / employees:read / employees:write / leave:read / shifts:read) — there is no "all access" ability. More sensitive data (like GPS location) sits behind its own additional ability rather than being bundled into a broader one.
Webhook registration is UI-only
There is no API endpoint to create, list, or delete webhook endpoints — only an authenticated company admin in the web interface can do that. A leaked API token, even with every ability granted, can never redirect where your events are sent.
No salary data
National ID number, salary, and vacation/sick balances are never exposed by the API, on any endpoint, under any ability.
Audit log
Every API call is logged — who, when, which endpoint, from which IP address, and which status code was returned.
Instant revocation
Revoking a token through the interface blocks access immediately — no delay or caching.

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

v1.0.1

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.

v1.0.0

Initial release of the public API.

Added /attendance, /employees, /employees/:id, /departments, /branches. Company-scoped token authentication, precise abilities, rate limiting, and audit logging.