HubstaffDeveloper Portal

Getting started

Authentication

Hubstaff APIs accept three kinds of credentials. Pick the one that fits how your integration runs:

  • Personal access tokens (PAT) — for server-side scripts, CI jobs, internal tooling. Scoped to a single user. No browser flow.
  • OAuth applications — for multi-tenant integrations where end users sign in with their Hubstaff account. Standard Authorization Code + PKCE flow.
  • Organization access tokens — long-lived hsoat_ bearer tokens for organization-wide server-side automation. Created by an admin in the Hubstaff app and run as a chosen member. No browser flow, no token exchange.

Personal access tokens and OAuth apps are built on OpenID Connect over OAuth 2.0 — both issue JWT access tokens from the same token endpoint, signed with RSA (verify against jwks_uri in the discovery document). The JWT payload carries iat, exp, and the granted scope string. Organization access tokens skip that exchange entirely — the hsoat_ secret is sent directly as the bearer.

Endpoints

EndpointURL
OIDC Discoveryhttps://account.hubstaff.com/.well-known/openid-configuration
Authorizehttps://account.hubstaff.com/authorizations/new
Token (issue & refresh)https://account.hubstaff.com/access_tokens
User infohttps://account.hubstaff.com/user_info
JWKShttps://account.hubstaff.com/jwks.json

Use the discovery document at runtime; cache it for up to 1 week. All other URLs above are derived from it.

Personal access tokens

Create a PAT in Account → Personal access tokens. Pick the scopes you need (see Scopes). On creation you receive a single string — this is a refresh token, not an access token. Store it securely; we can only show it once.

Step 1 — Exchange the refresh token for an access token

Before your first API request, swap the PAT refresh token for a short-lived access token. PATs do not have a client_id or client_secretdo not send credentials; the request is rejected if you do.

bash
curl -X POST https://account.hubstaff.com/access_tokens \
  -d grant_type=refresh_token \
  -d refresh_token=<your_pat_refresh_token>

Successful response:

json
{
  "token_type": "bearer",
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "expires_in": 86400,
  "refresh_token": "eyJhbGciOiJSUzI1NiIs..."
}
Replace your stored refresh token
Every refresh response returns a new refresh_token JWT string. The old string stops being accepted because its exp claim changes. Replace what you have on disk on every refresh, or your next refresh will fail.

Step 2 — Call the API

Send the access token in the Authorization header on every request:

bash
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...

If the token is bound to a DPoP key (see DPoP), also send a fresh DPoP header on every request.

Step 3 — Refresh when the access token expires

When you receive 401 or the expires_in window is close, call the same endpoint again with the current refresh token — exactly the same request as Step 1.

Token lifetimes

TokenDefault lifetimeNotes
access_token24 hours Short-lived. Use it until expires_in runs out, then refresh.
refresh_token90 days Sliding window — every successful refresh resets expiry to 90 days from now.

OAuth applications

Create your app in Account → OAuth apps to get a client_id and client_secret. Scopes are requested per authorize call (Step 1 below), not pre-registered.

Step 1 — Build the authorize URL and redirect the user

Generate a unique nonce per request, URL-encode each parameter, then send the user's browser to the authorize URL via HTTP 302 redirect (or a plain <a href>). Example URL shape — line-wrapped for readability, but in practice it's one continuous URL:

bash
https://account.hubstaff.com/authorizations/new
  ?client_id=<your_client_id>
  &response_type=code
  &redirect_uri=<url_encoded_redirect_uri>
  &scope=openid+profile+email+hubstaff:read
  &state=<opaque_csrf_value>
  &nonce=<unique_per_request>
  &code_challenge=<derived_challenge>
  &code_challenge_method=S256

Your backend builds this URL and redirects the user there. Hubstaff shows the login and consent screens, then redirects back to your redirect_uri with ?code=...&state=....

PKCE is optional but recommended
Hubstaff accepts the Authorization Code flow with or without PKCE (RFC 7636). If you use it, generate a high-entropy code_verifier (43–128 URL-safe characters: A-Z a-z 0-9 - . _ ~) and derive the challenge:
bash
code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))  # no '=' padding

PKCE adds defence-in-depth against authorization-code interception, and is required by OAuth 2.1. Skip code_challenge / code_challenge_method here (and code_verifier in Step 2) if you choose not to use it.

ParamRequiredDescription
client_idyesFrom the OAuth app registration.
response_typeyes Must be code.
redirect_uriyes Must exactly match a redirect URI registered on the app.
scopeyesSpace-separated list. See Scopes below.
nonceyes Unique per request. Prevents replay; echoed back in the id_token.
code_challengeoptional PKCE S256 transformation of the verifier. Strongly recommended; effectively required if you can't store client_secret securely (mobile / SPA).
code_challenge_methodoptional Required only if code_challenge is sent. Use S256.
staterecommended Opaque CSRF token; we echo it back on the redirect.

The user sees a consent screen listing your app and the requested scopes. After they approve, we redirect to redirect_uri with ?code=<...>&state=<...>.

Step 2 — Exchange the code for tokens

Validate state matches what you sent. Then POST the code to the token endpoint. Authenticate with HTTP Basic using client_id / client_secret (or send them in the body as client_secret_post). If you set code_challenge in Step 1, also send the matching code_verifier; otherwise omit it.

bash
curl -X POST https://account.hubstaff.com/access_tokens \
  -u <client_id>:<client_secret> \
  -d grant_type=authorization_code \
  -d code=<received_code> \
  -d redirect_uri=<same_redirect_uri> \
  -d code_verifier=<original_verifier>          # optional, only if PKCE was used at /authorize

Successful response:

json
{
  "token_type": "bearer",
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "expires_in": 86400,
  "refresh_token": "eyJhbGciOiJSUzI1NiIs...",
  "id_token": "eyJhbGciOiJSUzI1NiIs..."
}

id_token is only returned if openid was in the requested scopes.

Step 3 — Refresh the access token

Same endpoint, different grant. Send your client credentials and the current refresh token. Optionally pass scope to narrow the granted scopes — you cannot broaden them past the original grant.

bash
curl -X POST https://account.hubstaff.com/access_tokens \
  -u <client_id>:<client_secret> \
  -d grant_type=refresh_token \
  -d refresh_token=<your_refresh_token>

The response shape matches Step 2 (minus id_token). The returned refresh_token string is rotated — replace what you have on disk.

Token lifetimes

TokenDefault lifetimeNotes
access_token24 hours Short-lived. Use it until expires_in runs out, then refresh.
refresh_token30 days Sliding window — every successful refresh resets expiry to 30 days from now.
id_token5 minutes Use it once to identify the user; do not store long-term.

Rate limits & client best practices

Do not refresh on every API call
The token endpoint is rate-limited to 5 refresh attempts per hour per refresh token. Going over returns 400 with {"error":"rate_limit"}. Cache the access_token and only refresh when it actually expires (the expires_in window is up, or the API returns 401).

A workable refresh strategy:

  • Store access_token, refresh_token, and the absolute expiry (e.g. now + expires_in - 60s).
  • Reuse the cached access_token on every API call until that expiry passes — typically thousands of requests per refresh.
  • On 401 from the API, refresh once and retry the original request. Do not loop on repeated 401s.
  • Always persist the new refresh_token returned by the refresh call. Reusing the previous string will fail.

Per-API rate limits (for actual data requests) are documented under Pagination & rate limits.

Scopes

Request the narrowest set of scopes you actually need. On refresh you may pass a scope parameter to narrow further; you cannot broaden past the original grant.

ScopeGrants
openid Required to receive an id_token.
profileUser name and basic profile fields.
emailUser email address.
hubstaff:readRead access to the Hubstaff V2 API.
hubstaff:writeWrite access to the Hubstaff V2 API.
tasks:readRead access to the Tasks API.
tasks:writeWrite access to the Tasks API.

DPoP (Proof-of-Possession)

API V2 only
DPoP is currently supported by the Hubstaff API V2 only.

Hubstaff supports DPoP (RFC 9449) to bind a token to a client-held key pair. If a token leaks, an attacker who doesn't have the private key cannot replay it. Once a token is DPoP-bound, every request — including refresh — requires a fresh signed DPoP header alongside Authorization.

Generate an EC P-256 key pair (one time)

bash
openssl ecparam -name prime256v1 -genkey -out dpop_private.pem
openssl ec -in dpop_private.pem -pubout -out dpop_public.pem

Build the DPoP JWT

JWT header:

json
{
  "typ": "dpop+jwt",
  "alg": "ES256",
  "jwk": { <your public key as JWK> }
}

Required claims:

ClaimValue
htm Uppercase HTTP method — "GET", "POST", etc.
htu Full target URI: scheme + host + path. No query string, no fragment.
iat Unix timestamp now. Accepted within roughly ±1 minute of server time.
jti Unique per JWT, max 128 bytes. Reused values are rejected (replay protection).
athBASE64URL(SHA256(access_token)), no padding. Required whenever you're also sending Authorization: Bearer ...; omit for token refresh requests.

Send DPoP on every request

bash
Authorization: Bearer <access_token>
DPoP: <signed_dpop_jwt>

A new DPoP JWT must be generated for every request because htm, htu, and jti change.

Binding a personal access token
On the Personal access tokens page, you can paste a DPoP JWT when creating the PAT to bind it to your key pair. After that, the bound key is required for the initial refresh-token exchange and every subsequent API request — keep the private key safe.

Organization access tokens

Organization access tokens (prefixed hsoat_) are long-lived bearer credentials for server-side automation that runs without anyone signing in — CI jobs, internal integrations, scheduled scripts. Unlike a PAT or an OAuth app, the token is the credential: there is no token exchange, no refresh, and no browser flow. (They don't use OIDC scopes, refresh, or DPoP — the sections above apply to PATs and OAuth apps.)

An owner, manager, or Manage-IT member creates one in the Hubstaff web app under Settings → Organization → API tokens and assigns it to an existing organization member. The token then authenticates as that member, with exactly that member's current organization role and access — there are no separate token scopes to configure.

Managed in the Hubstaff app, not here
Organization access tokens are created, reassigned, and revoked only in the Hubstaff web app — there are no public API endpoints to manage them. The secret is shown once at creation and can never be retrieved afterward, so store it somewhere safe.

Call the API

Send the token directly in the Authorization header on every request. There is nothing to exchange or refresh, and these tokens are not DPoP-bound — use the secret as-is until the token expires, is revoked, or is reassigned.

bash
Authorization: Bearer hsoat_...

Lifecycle & reassignment

  • You choose an expiration when creating the token — Never (the default; the token never expires) or 30, 60, or 90 days. If it does expire, the token stops working and the API responds 401; create a new token to replace it.
  • The token keeps working as long as its assignee is an active member of the organization.
  • If the assignee is removed from the organization or their account is deleted, the token stops working and the API responds 401. Hubstaff notifies the organization owners so they can reassign it.
  • An admin can reassign the token to another member at any time — the secret stays the same, so running automation keeps working; only the member it acts as changes. (Only an owner may assign a token to an owner.)
  • Revoking a token in Settings makes it stop working immediately.
PAT vs organization access token
A PAT is a refresh token you exchange for short-lived access tokens, tied to your own user. An organization access token is sent directly as a bearer, stays valid until it expires (you choose 30/60/90 days or Never) or is revoked, and acts as a member an admin chooses — built for shared, long-running organization automation.