Conventions
This page describes the conventions shared by all /api/v1 endpoints: base URL, response format, headers, pagination, dates, and filters. If a convention isn't overridden for a specific endpoint in the Endpoints reference, what's described here applies.
Base and versions
The API base URL is https://smengo.com/api/v1. The version is fixed in the path: breaking changes will ship as /api/v2 rather than as changes to the current version, so existing integrations don't break silently.
The API is HTTPS-only. Responses are not cached: every response is marked Cache-Control: private, no-store because data is scoped to a specific API key.
Response format
List of resources
A successful list response is an object with data (an array) and meta.next_cursor:
{
"data": [
{ "id": "9c1a2e34-5678-4abc-9def-0123456789ab", "...": "..." }
],
"meta": { "next_cursor": "eyJrIjoiSXZhbm92YSIsImlkIjoiOWMxYS4uLiJ9" }
}
meta.next_cursor is null when this is the last page.
Single object
Resources an organization has exactly one of (for example GET /org) return an object without meta:
{ "data": { "name": "Coffee & Co", "slug": "coffee-co", "timezone": "Europe/Kyiv" } }
Error
Every error is returned in a single envelope — regardless of the endpoint or the cause:
{
"error": {
"code": "validation_error",
"message": "Invalid query parameters.",
"hint": "from must not be later than to.",
"docs_url": "https://smengo.com/api-docs/reference/errors#validation_error",
"request_id": "8f14e45f-9e05-4f1a-8a3e-3d2b1c0f9b1b"
}
}
The full catalog of error codes and what to do about each one is on the Errors page. There are no plain-text responses — only JSON, always, including errors.
Response headers
X-Request-Id— a uuid v4, present on every/api/v1response, including errors. Include it when contacting support — it's how a specific request is found in the logs.Cache-Control: private, no-store— responses aren't meant to be cached by a browser or a proxy.
Pagination
List endpoints use cursor-based pagination:
?limit=— an integer1..200, default100.?cursor=— an opaque base64url string taken from the previous response'smeta.next_cursor. Don't construct it yourself — just pass it through as-is on the next request.- The sort order is fixed per resource and always includes
idas a tiebreaker (for example, employees are sorted byfull_name, id). The tiebreaker guarantees pages don't duplicate or skip rows when the primary sort field has equal values. - A cursor that's malformed or belongs to a different dataset (for example, one obtained with different filters) returns
400 invalid_cursor.
curl -s "https://smengo.com/api/v1/employees?limit=50&cursor=eyJrIjoiSXZhbm92YSIsImlkIjoiOWMxYS4uLiJ9" \
-H "Authorization: Bearer smg_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
Rate limiting
Each API key is limited to rate_limit_per_min requests per minute — 60 by default, configurable per key. The window is a sliding window (Cloudflare-style): when computing the current minute, a share of the previous minute's hits is still counted, so a sharp burst right at the minute boundary can't be used to double the effective limit.
Three headers are present on every /api/v1 response — both successes and errors:
| Header | Meaning |
|---|---|
X-RateLimit-Limit |
The key's limit, requests/min. |
X-RateLimit-Remaining |
How many requests are left in the current window. |
X-RateLimit-Reset |
Unix time (seconds) when the current window ends and the limit resets. |
Exceeding the limit returns 429 rate_limited with an additional Retry-After header (seconds until the next attempt):
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1784625300
Retry-After: 42
{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded.",
"docs_url": "https://smengo.com/api-docs/reference/errors#rate_limited",
"request_id": "8f14e45f-9e05-4f1a-8a3e-3d2b1c0f9b1b"
}
}
See Errors → rate_limited for more on the error code.
Requests rejected during authentication or scope checks (401 unauthorized, 403 missing_scope, 403 subscription_inactive) do not consume the limit — only requests that reach the limiter itself are counted.
Dates and time
- Day fields (
entry_date,date,period,from,to) —YYYY-MM-DDformat, in the organization's timezone, not UTC. - Instants (
check_in_at,created_at,updated_at, …) — ISO 8601 UTC with aZsuffix:2026-07-01T09:00:00.000Z. - The organization's timezone is available from
GET /org(thetimezonefield).
Filters
Applies to all time-scoped resources: schedule-entries, check-ins, timesheets, demand-signals.
from,to—YYYY-MM-DD, in the organization's timezone, boundaries inclusive.frommust not be later thanto; the maximum range is 366 days; violating either rule returns400 validation_errorwith an explanation inhint. When omitted, the default is the current calendar month in the organization's timezone, so a request without any parameters already returns meaningful data. Fortimesheets,from/tomatch againstperiod— the first day of the month a timesheet record belongs to.department_id— available wherever a resource has a department (includingemployees); the filter is an exact match.employee_id— available onschedule-entries,check-ins,timesheets.- An unknown
department_id/employee_id, including one belonging to another organization, is not an error — it just returns an empty list. This keeps tenant isolation from acting as an oracle for whether another organization's id exists. from/toon a resource with no time dimension (org,departments,employees,shift-presets,status-types) returns400 validation_error: an unexpected parameter isn't silently ignored, it's explicitly rejected.
What's next
- Errors — the error envelope format and the full code catalog.
- Scopes & permissions — which permission is required to grant a key access to each resource.