</> Replatform Developers Platform API v1 Console →

The Platform API

One identity service behind every Replatform application — the console, the five reference products, and yours. This is the reference for calling it directly, and the SDKs that mean you usually don't have to.

Base URL: https://platform-api.replatform.co · every endpoint below is under /v1.

Quickstart

Five minutes from nothing to a verified request.

  1. Get a client registered

    Ask a platform admin to create your application in the console, or do it yourself if you're one. You'll get a client_id and, from that client's page, a client secret — shown once, so copy it immediately.

  2. Exchange it for a service token

    # the only call you make with the secret itself
    curl -X POST https://platform-api.replatform.co/v1/auth/token \
      -H "content-type: application/json" \
      -d '{"client_id": "your-app", "client_secret": "pcs_..."}'
  3. Call an endpoint

    curl https://platform-api.replatform.co/v1/clients/your-app/roles \
      -H "Authorization: Bearer <access_token>"
  4. That's it for the app-to-platform half

    For the user-facing half — someone signing in — see user tokens below. Most applications want the SDK rather than raw curl; see SDKs.

The one thing to understand first

Token issuance is central. Token verification is local.

The platform is called when a user signs in and when their token is refreshed — a couple of times per person per day. Every request after that is verified by your application, offline, against a public key it already has cached. Authorization travels inside the token, so the common case — an authenticated page view — costs no network call to the platform at all.

That's why the SDKs split cleanly into two kinds of call: the handful that reach the network (login, refresh, issuing a service token) and the one that never does (verify). If you're deciding what to call on every request, it's verify.

Authentication

Two credentials exist, because two different things authenticate.

Service tokens — your application, acting as itself

Used for audit writes, sending mail, and anything else your application does on its own behalf rather than a signed-in user's. Exchange your client id and secret for one:

POST/v1/auth/token

No auth header — the body is the credential.

{
  "client_id": "your-app",
  "client_secret": "pcs_...",
  "scope": "audit:write mail:send"  // optional — omit for everything you're granted
}

Returns a token good for 15 minutes, scoped to the intersection of what you asked for and what your client is granted in the console. There's no refresh token here — when it expires, ask again.

User tokens — someone signing in

POST/v1/auth/login
{ "email": "ada@example.com", "password": "...", "client_id": "your-app" }

Returns an access_token (15 minutes) and a refresh_token (30 days, single-use, rotated on every refresh). The account must have a confirmed email address — an unconfirmed one gets 403 email_unverified, not a silent failure.

POST/v1/auth/refresh

Trade a refresh token for a new pair. Single-use — presenting the same one twice revokes every session descended from it, on the assumption that a replay means it was stolen. Rotate it on every use and you'll never notice.

Verifying a token — the one that runs locally

Fetch /v1/auth/jwks.json once, cache it (the SDKs do this for a day and serve it stale on error), and verify the RS256 signature yourself. Check, in order:

ClaimWhat to check
audEquals your own client_id — never skip this. A token minted for another application must not verify for yours.
exp / iatNot expired; not issued in the future (allow ~60s clock skew).
issEquals https://platform-api.replatform.co.

The roles claim is scoped to your client only — it never reveals that the holder is an admin somewhere else. That's deliberate: a token handed to an external integrator shouldn't leak standing elsewhere in the estate.

SDKs

Both wrap the same rules: always check the audience, always serve stale keys rather than fail a sign-in, never let an audit write throw.

Python — replatform-platform

pip install -e packages/platform-client   # from the monorepo, for now
from replatform_platform import PlatformClient

platform = PlatformClient(
    base_url="https://platform-api.replatform.co",
    client_id="your-app",
    client_secret=os.environ["PLATFORM_CLIENT_SECRET"],
)

pair = platform.login(email, password)             # sign-in
who  = platform.verify(pair["access_token"])         # every request after — local, no network
platform.audit("timesheet.approved", actor_email=email)

Node.js — @replatform/platform-client

Dependency-free: global fetch and node:crypto, both in Node 18+. This is the SDK that let PassIsland — the one product in the estate that isn't Python — join the platform at all.

import { PlatformClient } from '@replatform/platform-client'

const platform = new PlatformClient({
  baseUrl: 'https://platform-api.replatform.co',
  clientId: 'your-app',
  clientSecret: process.env.PLATFORM_CLIENT_SECRET
})

const who = await platform.verify(accessToken)   // local
if (!who.hasRole('vault_owner')) return res.status(403).end()
await platform.audit('vault.opened', { actorAccountId: who.accountId })

Migrating an existing app onto the platform

If you already have your own password check, don't rip it out — dual-run it. The Python SDK's dual_run_login tries the platform first and falls back to your own check only when the platform is genuinely unreachable, never when it gives a real answer (wrong password, locked, unconfirmed):

from replatform_platform.client import dual_run_login

result = dual_run_login(platform, email, password,
                         local_check=my_own_password_check,
                         on_path=lambda path: metrics.increment(f"login.{path}"))

When the fallback path serves zero sign-ins for a week, the import is done and the local check can come out.

API reference

/v1/auth

GET/v1/auth/jwks.json

The public signing keys. Cacheable — the only endpoint that is.

POST/v1/auth/logoutbearer

Ends the session named in the presented access token.

POST/v1/auth/revokebearer

Ends every session for the account. Existing access tokens still expire on their own schedule — up to 15 minutes — this can't recall one already handed out.

/v1/authz

POST/v1/authz/checkauthz:check

For the decisions a token can't carry — a grant that changed mid-session, or a resource-level rule. Not for every request: the token already says what roles the holder has here.

{ "subject": "pacct_...", "action": "approve", "session_id": "sess_..." }
// → { "allow": true, "roles": ["manager"], "reason": "role_grant" }

/v1/clients

GET/v1/clients/{id}/rolesusers:read

This application's role catalogue, as defined in the console — so your own role picker never drifts from what's actually granted.

GET/v1/clients/{id}/membersusers:read

Everyone holding a role in this client, with their platform account id.

/v1/users

GET/v1/users/{id}users:read

404s unless this account holds a role in your client — an application only ever learns about its own users.

GET/v1/users/{id}/grantsplatform only

Roles across every client. The one cross-application view, restricted to the platform's own client id.

/v1/audit

POST/v1/audit/eventsaudit:write

One event, or {"events": [...]} up to 100 at a time. The client_id comes from your token, never the body.

GET/v1/audit/eventsaudit:read

Your own recent events. Cross-application search lives in the console.

/v1/mail

POST/v1/mail/sendmail:send

Refusal is a 200, not an error: {"sent": false, "reason": "recipient_not_confirmed"} when the address hasn't confirmed its email. Pass template + variables to use a message defined in the console, or subject/text/html directly.

GET/v1/mail/templatesmail:send

What your application may send. Bodies aren't returned — that's the console's to show.

/v1/contact

POST/v1/contact/messagescontact:write

The public contact log. The sender's address is taken from the connection, never the request body.

Errors

Every error is JSON: {"error": {"code": "...", "message": "..."}}.

StatusCodeMeaning
400invalid_requestMissing or wrong-typed field. fields names which.
401invalid_credentialsWrong password, unknown account, or locked — deliberately indistinguishable.
401invalid_client / invalid_grantBad client secret, or a refresh token that's used, revoked or expired.
403email_unverifiedCorrect password, unconfirmed address. Only returned after the password checks out.
403insufficient_scopeYour token doesn't carry a scope this endpoint needs.
403invalid_scopeYou asked /v1/auth/token for a scope your client isn't granted.
404not_foundIncluding a real account your client has no relationship to — see /v1/users.
429too_many_attemptsRate limited — see below.

Rate limits & security

  • 20 failed sign-ins / 15 minutes per source address, across every account — catches password spraying, which per-account lockout can't.
  • 10 failed client-credential attempts / 15 minutes per client.
  • A WAF sits in front of everything: a 600 req/5min per-IP cap, plus AWS's managed rule sets for common attacks and known bad inputs, evaluated before a request reaches the API at all.
  • No CORS headers are ever returned. This API is server-to-server — a token belongs in your backend, never in a browser.
Report a vulnerability: email security@replatform.co. We'll acknowledge within a working day.