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/v1 response, 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 integer 1..200, default 100.
  • ?cursor= — an opaque base64url string taken from the previous response's meta.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 id as a tiebreaker (for example, employees are sorted by full_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-DD format, in the organization's timezone, not UTC.
  • Instants (check_in_at, created_at, updated_at, …) — ISO 8601 UTC with a Z suffix: 2026-07-01T09:00:00.000Z.
  • The organization's timezone is available from GET /org (the timezone field).

Filters

Applies to all time-scoped resources: schedule-entries, check-ins, timesheets, demand-signals.

  • from, toYYYY-MM-DD, in the organization's timezone, boundaries inclusive. from must not be later than to; the maximum range is 366 days; violating either rule returns 400 validation_error with an explanation in hint. When omitted, the default is the current calendar month in the organization's timezone, so a request without any parameters already returns meaningful data. For timesheets, from/to match against period — the first day of the month a timesheet record belongs to.
  • department_id — available wherever a resource has a department (including employees); the filter is an exact match.
  • employee_id — available on schedule-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/to on a resource with no time dimension (org, departments, employees, shift-presets, status-types) returns 400 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.
Was this article helpful?