Bearer tokens

Every API request must include a bearer token in the Authorization header:

Authorization: Bearer stp_test_abc123...

Tokens are managed from Settings → API Tokens in the web UI. Create as many as you need — one per integration, per environment, per deployment stage.

Test vs live tokens

Tokens come in two flavours, distinguished by prefix:

Prefix Environment What it can do
stp_test_... Test / sandbox Read and write employers with environment: "evte". Submissions go to ATO EVTE, not production.
stp_live_... Live / production Read and write employers with environment: "production". Submissions go to ATO production.

A test token cannot see or modify live data, and vice versa. This is enforced at the database query level, not just the UI — an accidentally-leaked test token cannot trigger real ATO submissions, and a leaked live token won't collide with sandbox data.

The same base URL is used for both (https://stp.beeswaxapp.com/api/v1/). Only the token determines which environment you're talking to.

Creating a token

  1. Sign in → Settings → API Tokens
  2. Pick Test or Live
  3. Enter a memorable name (e.g. "Production Beeswax integration")
  4. Optionally set an expiry date — useful for rotating credentials
  5. Select scopes — the minimum set of capabilities this token needs
  6. Click Generate token

The full token value is shown exactly once, immediately after creation. Copy it to your secrets store right away. We only keep a hash, so there's no way to retrieve it later — if you lose it, revoke the old one and create a new one.

Scopes

Tokens can be scoped to limit what they can do. A CI script that only needs to read pay event status should not need write access to employer records.

Scope What it allows
employers:read GET /employers, /employers/:id
employers:write POST /employers, PATCH /employers/:id
employees:read GET /employers/:id/employees and individual employees
employees:write POST and PATCH employee records
pay_events:read GET /pay_events and individual pay events
pay_events:write POST, PATCH, mark-ready, submit, item operations
submissions:read GET /submissions
submissions:write Retry a rejected or errored submission, cancel in-flight lodgements
webhooks:read List webhook endpoints and recent deliveries
webhooks:write Create, update, rotate the signing secret on, and delete webhook endpoints
ytd:read GET /ytd summary
platform:act_on_behalf Platform-tier: act across many downstream customer employers on a single token (see Platform tokens below)

If a token doesn't have the required scope for an endpoint, the response is 403 Forbidden:

{
  "error": "Forbidden",
  "message": "This token does not have the 'pay_events:write' scope"
}

Expiry

Tokens can have an optional expiry date. Expired tokens return 401 Unauthorized:

{
  "error": "Unauthorized",
  "message": "Invalid or expired API token"
}

For long-lived production integrations, leave the expiry blank. For CI/CD or temporary work, set one explicitly.

Rotation

To rotate a token:

  1. Create a new token with the same scopes
  2. Deploy the new token to your server
  3. Verify it's working in your logs (check last_used_at in the UI)
  4. Revoke the old token

The new token's prefix (stp_test_ vs stp_live_) must match the environment of the integration it's replacing. You can't swap a test token for a live token mid-deployment.

Revoking a token

On the API Tokens page, click Revoke next to any token. Revocation is instant — the next request using the token returns 401. Revoked tokens are kept in the audit log but hidden from the active list by default.

Security best practices

  1. Treat tokens like passwords. Never commit them to git, even in test files. Use environment variables or a secrets manager.
  2. Scope tightly. Don't give a read-only integration employers:write. Scope creep leads to blast radius.
  3. Use test tokens for CI. If your CI leaks a test token, the damage is zero — nothing can reach production from it.
  4. Rotate regularly. Annual rotation for production tokens is a reasonable cadence. Rotate immediately if you suspect compromise.
  5. Monitor last_used_at. If a token hasn't been used in a while, revoke it.
  6. Set expiry on temporary tokens. Contractors, one-off migrations, and debugging sessions should all use expiring tokens.
  7. Never log tokens. Redact Authorization headers in your application logs.
  8. Use HTTPS. The API only accepts HTTPS; we reject HTTP at the proxy layer.

Browser (CORS) access

Each token carries an allowed origins whitelist (newline- or comma-separated). If you're calling the API directly from a browser — e.g. a customer-facing dashboard built on top of our lodgement — add every origin that'll send credentialed requests:

https://dashboard.fintech.example
https://staging.fintech.example

When the whitelist is empty, the token is server-to-server only — we emit no Access-Control-Allow-Origin header and the browser's CORS check fails. This is the secure default.

Entries must be exact-match origins (scheme + host + port). * is accepted if you really want to wildcard — we'll echo back whatever origin is on the request — but do this only for public unauthenticated data.

Preflight OPTIONS requests are answered permissively so the real request can reach the auth check; that's normal for this pattern.

Request signing (optional)

For high-trust integrations (neobanks, PSPs) you can turn on HMAC request signing per-token. When enabled, every request must carry two extra headers:

X-STP-Timestamp: 1712659200
X-STP-Signature: 5d41402abc4b2a76b9719d911017c592f3a8a8d5e...

Where X-STP-Signature is:

HMAC-SHA256(signing_secret, "{timestamp}.{raw_body}").hexdigest

The canonical string is literally "{timestamp}.{raw_body}" — the Unix timestamp, a single ASCII period, then the exact bytes of the request body as sent on the wire. Do not re-serialise, pretty-print, or sort JSON keys before signing; sign whatever you're about to transmit and send those same bytes.

For GET, DELETE, and any other bodyless request, raw_body is the empty string — the canonical string is just "{timestamp}." (timestamp, period, nothing after). This is the single most common source of signature_invalid errors; if you see that response on a read request, this is almost certainly why.

The timestamp must be within ±5 minutes of server time — older signatures are rejected as replay attempts.

Enable signing by setting Require signing on the token and rotating to get a signing secret. If the secret is ever rotated, shift traffic atomically — there's no dual-secret window.

require "openssl"

ts  = Time.now.to_i
body = '{"employer":{"legal_name":"Acme"...}}'
sig = OpenSSL::HMAC.hexdigest("SHA256", ENV["STP_SIGNING_SECRET"], "#{ts}.#{body}")

Net::HTTP.post(
  URI("https://stp.beeswaxapp.com/api/v1/employers"),
  body,
  "Authorization"     => "Bearer #{ENV["STP_TOKEN"]}",
  "Content-Type"      => "application/json",
  "X-STP-Timestamp"   => ts.to_s,
  "X-STP-Signature"   => sig
)

Signing is on top of Bearer auth, not instead — an unsigned request with a valid token gets 401 if the token has signing required, and a signed request with an invalid token still gets 401 for bad auth.

Platform tokens (acting on behalf of a downstream employer)

Most integrations have one token per employer: the token owner is the business. That's the default and the simplest model.

Some integrations sit one layer higher — a platform product (e.g. a fintech, a neobank, a payroll SaaS) calls the API on behalf of many downstream customer employers using a single token. When one token acts across many customers, our audit log needs to know which specific customer and which specific human triggered each request — that's the ATO OSF Control 1 (unique-user attribution) requirement.

Platform tokens solve this with two extra request headers.

Issuing a platform token

Grant the platform:act_on_behalf scope when you create the token. This scope is additive — combine it with the normal resource scopes (employers:write, pay_events:write, etc.) the platform needs.

Using a platform token

Every call must include both headers:

Authorization: Bearer stp_live_platform_abc123...
X-On-Behalf-Of-Employer: 4821
X-Acting-User: jane@acme.com.au
Header Meaning
X-On-Behalf-Of-Employer Integer ID of the customer employer this request is acting for. Must be an employer provisioned under the platform token's account.
X-Acting-User Free-form identifier of the human who triggered the request — email is cleanest. The platform is contractually responsible for populating this honestly.

The employer's environment (test/live) must match the token's. A live platform token cannot act against an evte employer and vice versa.

What gets enforced

  • The request scope narrows to the named employer — the platform can't accidentally read or write a sibling customer's data, even though the same token owns both.
  • Every audit log row stamps the platform token, the on-behalf-of employer, and the acting user. An ATO auditor asking "who lodged this pay event?" gets jane@acme.com.au at Acme Pty Ltd (employer 4821) via beeswax (token id 17).

Error cases

Response Cause
403 Forbidden with code on_behalf_of_not_permitted A token that doesn't have platform:act_on_behalf sent one of the headers. Stops a leaked regular token from forging attribution.
400 Bad Request with code on_behalf_of_required Platform token was used without both headers. Both are mandatory — there's no "default" employer.
403 Forbidden with code on_behalf_of_employer_unknown Employer ID doesn't exist under this token's account, or its environment doesn't match.

When not to use platform tokens

If your product is just one company's own integration (single ABN, single platform), use a regular per-employer token. Platform tokens exist for the explicit multi-customer case.

Error responses

Code When
401 Unauthorized Missing Authorization header, invalid token, expired token, revoked token, missing/invalid X-STP-Signature on a token with signing required
402 Payment Required Token is valid but the account isn't on the API plan (or is past the payment grace period)
403 Forbidden Token is valid but lacks the scope required for this endpoint, or on-behalf-of headers were sent/refused
400 Bad Request Platform token called without both on-behalf-of headers
429 Too Many Requests Token is valid but has exceeded rate limits — see Rate limits