Skip to content
AccessTime

REST API v1

Updated on October 18, 2026

The AccessTime REST API is the same one our mobile app uses: everything the app does goes through here. You can also use it for small integrations of your own, for example checking the day's time entries from a script or requesting an absence from another tool.

The base URL is https://yourdomain/api/v1, all requests and responses are JSON and protected routes require the Authorization: Bearer {token} header.

1. Get a token

Authentication uses personal tokens (Laravel Sanctum), with no cookies or session. The token is obtained with the employee's email and password, the same ones they use to sign in to the web panel:

curl -X POST https://yourdomain/api/v1/login \
  -H "Content-Type: application/json" \
  -d '{ "email": "empleado@empresademo.com", "password": "********" }'

The response includes the token and the user's basic data:

{
  "token": "1|abcdef...",
  "user": { "id": 1, "name": "...", "email": "...", "role": "empleado", "company_id": 2 }
}

Store the token somewhere safe and don't share it: it grants access to that person's data. If the credentials are invalid you'll receive a 422, and the endpoint is limited to 10 attempts per minute.

To sign out, POST /api/v1/logout (authenticated) revokes the current token.

Tokens expire after 90 days: the login response includes the date in token_expires_at and, once it has passed, the API answers 401 and you need to request a new one. If you send device_name at login ("Ana's iPhone"), the token appears with that name in Profile → Devices with access, where the person can revoke it.

2. Available endpoints

Method Route What it does
POST /login Returns a token from email and password (with two-step verification enabled, a challenge)
POST /login/two-factor Exchanges the challenge and the verification code for the token
POST /logout Revokes the current token
GET /config Server time, time zone, the company's geolocation setting and what the person can do
POST /fichar Records a clock-in or clock-out
GET /fichajes/hoy Today's entries
GET /fichajes/semana This week's entries
GET /resumen/mensual?month=YYYY-MM Monthly summary of hours worked
GET /ausencias The user's last 50 absences
POST /ausencias Requests an absence
DELETE /ausencias/{id} Cancels one of your own absences, with the panel's rules
GET /ausencias/tipos The company's active absence types
GET /ausencias/saldo?year=YYYY Annual holiday balance
GET /historial-cambios Who has changed the person's data, when and with which role
POST · DELETE /devices Registers or removes the phone for push notifications

All endpoints work on the authenticated user: each person only sees their own entries, absences and balance. Messages (message) come in the person's language, and every 403 carries a code saying what is going on: solo_lectura, empresa_bloqueada, cuenta_desactivada, email_no_verificado or sin_empresa.

3. Clocking in from the API

Every entry carries a client_uuid: a UUID you generate once, before the first attempt, and repeat on every retry. That way, if the network drops and you retry, the entry is never duplicated (without it, 422). Send type (in or out) too; if you omit it, it alternates based on the last entry, just like the panel button (how clocking in works).

curl -X POST https://tudominio/api/v1/fichar \
  -H "Authorization: Bearer 1|abcdef..." \
  -H "Content-Type: application/json" \
  -d '{ "client_uuid": "3f2504e0-4f89-41d3-9a0c-0305e82c3301", "type": "in" }'

201 response with the entry (status, id, type, happened_at…). A retry with the same client_uuid returns 200 and the same body.

  • The time is the server's. Whatever was captured offline is resent with offline: true, captured_at (the phone's time when tapping) and sent_at (the phone's time when sending): the server corrects the clock offset. If it arrives more than 72 hours later, it becomes an issue pending review (202): it is not lost. If the first one on record after its time, less than 5 minutes later, is an entry of the same type (the person repeated it at the kiosk or in the dashboard while it waited in the queue), it is the same entry repeated: 200 with "duplicate": true and the one on record. If that entry is pending review, it is a 202 with "duplicate": true and that issue. And if what is on record less than 5 minutes away is an entry of the same type recorded by hand after the button was pressed (a forgotten entry approved or a correction accepted before the queue arrived): in the same minute, the 200 returns that one; in another minute, the submission counts: the one recorded by hand is voided (still visible) and the 200 with "duplicate": true returns the entry recorded with the client_uuid sent.
  • Location (latitude, longitude, accuracy): only if the company has it enabled (geolocation_mode in /config); otherwise it is discarded. With obligatoria, an entry made at that moment without a location gets 422.
  • Phone verification (verification, optional): only the result of the check done by the phone itself, never biometric data: {"method": "biometric_local" | "pin" | "none", "verified": true | false, "os_auth_type": "face" | "fingerprint" | "credential" | null}. It is what your app declares: the server cannot check it. Spaces are trimmed and an empty text counts as null. If it does not match that format, it is discarded: the entry is recorded anyway and the response carries "verification_discarded": true (never 422); other keys are ignored. It stays with the entry and the person sees it in «My record».
  • known_ms (optional): the time in milliseconds of the last entry your app knew of when the button was tapped, in server time (from /config or the last response), not the phone's. If another one of the same type is recorded after it that the server already had when the button was tapped (for example, at the kiosk) or that your app sent earlier from its offline queue, the right type is recorded, as the dashboard button does, unless that entry is less than 5 minutes old and does not come from that queue: it is the same entry repeated, and the response is 200 with "duplicate": true and the entry already recorded. Without known_ms the same happens if that entry of the same type, less than 5 minutes old, was made through another channel (the kiosk or the dashboard); but an outdated button older than that is not corrected, so always send it.
  • Errors: 422 is final (show the reason); 401, 429, 5xx or no network, keep the entry and retry with the same client_uuid.

Entries are stored in UTC and the "today" and "week" queries use your company's time zone. The monthly summary is calculated from the workdays consolidated every night, the same ones that feed the reports.

If the company's free trial has ended without a subscription, the account is read-only and POST /fichar responds 403 (solo_lectura). The query endpoints keep working (plans and billing).

4. Absences from the API

You can query the available types and the balance, and request an absence:

curl -X POST https://yourdomain/api/v1/ausencias \
  -H "Authorization: Bearer 1|abcdef..." \
  -H "Content-Type: application/json" \
  -d '{ "absence_type_id": 1, "starts_on": "2026-08-03", "ends_on": "2026-08-07", "reason": "Vacaciones" }'

The API applies the same validations as the panel: overlaps with your other absences, available balance for types that deduct days, and that the range contains working days (weekends and public holidays are excluded). If the type requires approval, the request enters the approval flow with pending status; if not, it's approved immediately. More detail in absences and holidays.

Absence types that require supporting documents can't be requested via the API: the request returns an error and must be made from the web panel, where the document can be attached.

What now?

  • If you haven't set up your account yet, start with getting started.
  • To clock in on a shared tablet you don't need the API: use the QR kiosk.

Can't find what you're looking for?

Write to us from the contact page and we'll help you directly.

Go to contact