Skip to main content

Check-in API

Examples below show the wire-format Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE header. To compute the signature in your code, use the signedRequest helper in Authentication.
The Check-in API lets your backend read the duty presence state of your salespeople in SalesOS — did they check in today, where, on which shift, and until when it is valid. It was built for composing your own app screens (for example, a super-app home widget) before the user ever opens the SalesOS module. It is a server-to-server API: you look collaborators up by CPF or email, in batch, and SalesOS answers with today’s state computed in your company’s timezone. With the checkin:write scope it can also check a collaborator in and out.

How It Works

  1. You get an API Key with the checkin:read scope (Admin > Integrations > API Keys)
  2. Your backend asks for the check-in state of one or many collaborators (by CPF or email)
  3. SalesOS answers with today’s latest check-in per collaborator — plus optional week/month engagement counts
  4. Optionally, your backend checks people in and out with a key that also has checkin:write, relaying the GPS reading from the collaborator’s phone

Authentication

All requests require an API Key in the Authorization header:
See the Authentication page for details on creating and managing API Keys.

Environments

Base URL: https://api.play2sell.com

Endpoint Reference

The endpoint accepts these actions via the action field: Each collaborator is identified by CPF (any format — digits are normalized) or email. When both are sent, CPF wins.

Action: checkin_status

Today’s latest check-in per collaborator. “Today” is computed in your company’s timezone (returned as timezone / reference_date in the response) — never UTC.

Request Schema

string
required
Must be "checkin_status"
array
required
Array of collaborator references (max 500)
string
CPF, any format (52998224725 or 529.982.247-25). Must contain exactly 11 digits.
string
Email — used only when cpf is absent

Example

Response (200):

Response fields explained

boolean
Whether the CPF/email matched a collaborator in your company. An unknown collaborator is found: false — it is not an error, and the array keeps the same order as your request.
boolean
Whether the collaborator has at least one non-rejected check-in today (company timezone).
boolean
true when the latest check-in is active/approved and its shift has not expired — i.e. the collaborator is on duty right now.
object
Today’s most recent check-in. Omitted when there is none. mode is geo (in-person with GPS), home (home office) or offline. status may be active, approved, pending (awaiting leader approval), expired (shift ended normally) or checkout. location has the GPS reading stored with the check-in: lat, lng, accuracy (meters) and, for check-ins made through this API, captured_at (when the phone read the GPS). Absent when the check-in had no GPS (home office, checkout).
string
The collaborator’s membership in your company (active, inactive, …). A collaborator can be found and inactive.

Day track (include_day)

Send "include_day": true on checkin_status to get, beyond the current state, how the day is going shift by shift and when the button unlocks again. This is what a home screen needs to render the check-in block without opening the SalesOS module.
Response (200):
Shifts come back once at the top, not repeated per person. On a 500-collaborator batch that is the difference between a lean response and a multi-megabyte one.

Fields

A unit can have its own hours, and they win. The top-level shifts array is your company’s default. When someone checks in at a unit that has its own schedule, day.shifts follows that unit and day.shifts_source says so. Read the person’s track, not the top-level array, when drawing their screen.Before the first check-in of the day there is no unit to resolve, so the company default is used.
The number of shifts belongs to your company — it is not fixed. It may be two, three, or none, with its own labels and hours. Build the screen from the array; never assume three slots or specific times.
Read opens_at together with state. With state: "available" it is absent because a shift is open right now. With state: "waiting" and no opens_at, the day is over and there is no further opening today — the screen must not show a countdown crossing midnight.

Action: checkin_engagement

Everything checkin_status returns, plus week/month engagement counts per collaborator. Batches are capped at 100.
string
required
Must be "checkin_engagement"
array
required
Array of collaborator references (max 100)

Example

Each found collaborator gains an engagement object:
  • checkins — check-ins in the current week/month (company timezone; expired counts — it is the normal end of a shift)
  • presence_days — distinct days with at least one check-in

Writing check-ins

With the checkin:write scope, your backend can check a collaborator in and out — for example from a “Check in” button on your own home screen. The effect is the same as a check-in made in the SalesOS app: the collaborator enters the lead queue, gets the shift and its end time, earns points, and the checkin.confirmed / checkin.checked_out webhooks fire.
This API writes. Unlike the read actions above, checkin_create and checkin_checkout change who receives leads. They require a key with checkin:write; a key with only checkin:read answers 403 with code: "scope_insufficient".
Write actions handle one collaborator per request, sent as collaborator (an object, not a list). In a write, “not found” is an error — there is no partial write:

Action: checkin_options

What your screen can offer right now, so it only shows the button that will work. Read-only — requires checkin:read.
location is optional. When sent, locations come back sorted by distance and duty says whether any is within reach.
unavailable_reason uses the same codes a checkin_create would return (MODE_DISABLED, TOO_FAR, HOME_OUTSIDE_SHIFT, HOME_SHIFT_NOT_ALLOWED, PRIOR_GEO_REQUIRED, LOCATION_NOT_FOUND). Mode and shift labels are your company’s configuration — render them as they come.

Action: checkin_create

string
required
duty (on-site, requires location) or home (home office).
object
The device’s GPS reading, captured on the collaborator’s phone and relayed by your backend. Required for duty: lat, lng, accuracy (meters) and captured_at (ISO 8601 — when the phone read the GPS).
string
The on-site location the collaborator chose. Optional — without it, SalesOS picks the nearest location within its radius.
object
Your app and version (app, version). Stored with the check-in for audit.
Response (201):
location echoes the GPS reading that was stored — same shape as latest_checkin.location in checkin_status. Absent when no GPS was sent. A new check-in closes the collaborator’s previous active one, exactly as in the app. It stays valid until shift_expires_at — your app does not need to keep pinging.

Location requirements

The GPS reading comes from your backend, so SalesOS checks that it is recent and precise before trusting it:
Read the GPS when the user taps the button, not when the screen opens. A reading taken on screen load is often stale by the time it arrives, and an indoor reading is often imprecise — both are refused, and asking the user to step outside and try again fixes them.
A refused check-in writes nothing: the collaborator stays exactly as before.

Action: checkin_checkout

Ends the active check-in before the shift ends. The collaborator leaves the lead queue immediately and checkin.checked_out fires.
Response (200):
With no active check-in, the answer is still 200 with "already_checked_out": true and nothing is written — a double tap never breaks your screen.

Retries and Idempotency-Key

Send an Idempotency-Key header (8–128 characters; a UUID works) on checkin_create and checkin_checkout. Repeating the request with the same key within 24 hours:
  • returns the same response, with the header Idempotency-Replayed: true, and creates nothing new;
  • with a different body, answers 409 IDEMPOTENCY_KEY_REUSED;
  • while the first request is still running, answers 409 IDEMPOTENCY_IN_PROGRESS — retry in a few seconds.
A refused request (4xx) does not consume the key: after fixing the cause (a fresh GPS reading, for example) you can retry with the same key.
The signature timestamp is valid for 30 seconds, so a retry must be signed again — the Idempotency-Key is what ties the attempts together.

Messages for the collaborator

Write errors carry a message in Brazilian Portuguese, written to be shown directly to the collaborator (for example: “Você está a 180 m de Plantão Morumbi. Chegue a até 100 m para fazer o check-in.”). Branch your logic on code, never on the text.

Self mode (SSO user token)

If your app already holds the token of a user who signed in through SSO, the same endpoint answers for the token’s own user, with no API key:
  • Identity comes only from the verified token — do not send collaborators. A request with that field is rejected with SELF_MODE_NO_COLLABORATORS: user-level tokens can never look other people up.
  • The company scope comes from the session; the response has the exact same shape, with a single item in collaborators.
  • checkin_engagement works the same way ({ "action": "checkin_engagement" }).
  • Write actions and checkin_options are partner-only: with a user token they answer 400 SELF_MODE_NOT_SUPPORTED. Inside the SalesOS app, check-in is done by the app itself.
Use self mode for screens rendered inside the authenticated app; use the API key (S2S) when your backend composes screens before the user has a SalesOS session — like a super-app home widget.

Error Handling

All errors follow the shared structure:
Two shapes, not one. The block above is the endpoint error. Failures in the authentication layer answer before the endpoint runs, with error as a string:
Code that assumes error.code reads undefined on every auth failure. Branch on the type — see API Conventions.
Invalid body, batch over the limit, CPF without 11 digits, or an item with neither cpf nor email. The details array points to the item index.
API key missing, unknown or expired, or the signature does not match. Answered with error as a string and codes such as header_missing, api_key_not_found, signature_invalid, timestamp_too_old.
API key is valid but lacks the scope the action needs (checkin:read, or checkin:write for writes). Answered by the authentication layer: { "error": "...", "code": "scope_insufficient", "missing_scopes": ["checkin:write"] }.
Only POST is accepted.
Too many writes for the same collaborator (10 in 10 minutes). Wait the Retry-After seconds, then retry. Write actions have their own error table in Writing check-ins.
Internal server error. Retry with exponential backoff (2s, 4s, 8s).
An unknown collaborator is not an error. The request succeeds with found: false for that item, so one bad CPF never breaks a whole home-screen render.

Rate Limits


Security

  • Reading needs checkin:read; creating or ending check-ins needs checkin:write — issue write keys only to the backend that owns the button
  • Every check-in created through the API is stored with its origin, the key that created it, your client and the raw GPS reading, for audit
  • Each key is scoped to a single company — a CPF from another company answers found: false
  • Requests are HMAC-signed (P2S-SIGN-V1) and logged for audit; documents are never written to logs
Never expose your API key in client-side code. This API must only be called from your backend server.

Next Steps

Authentication

Learn how to create and manage API Keys

Support

Need help? Contact our support team