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) andsent_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:200with"duplicate": trueand the one on record. If that entry is pending review, it is a202with"duplicate": trueand 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, the200returns that one; in another minute, the submission counts: the one recorded by hand is voided (still visible) and the200with"duplicate": truereturns the entry recorded with theclient_uuidsent. - Location (
latitude,longitude,accuracy): only if the company has it enabled (geolocation_modein/config); otherwise it is discarded. Withobligatoria, an entry made at that moment without a location gets422. - 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 asnull. If it does not match that format, it is discarded: the entry is recorded anyway and the response carries"verification_discarded": true(never422); 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/configor 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 is200with"duplicate": trueand the entry already recorded. Withoutknown_msthe 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:
422is final (show the reason);401,429,5xxor no network, keep the entry and retry with the sameclient_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 /ficharresponds403(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