Skip to content

Authentication

A Paros integration meets two situations, and they use different credentials. A new customer onboards through an emailed code. A user who already belongs to an organisation signs in with their wallet. Everything after that is a short-lived access token plus a refresh token.

This page covers both paths, then the Salt token that some custody operations need, and finishes with what a customer role actually is.


The credential model

Credential Where it goes What it is for
Access token Authorization: Bearer <jwt> Nearly every call. Fifteen-minute lifetime
Refresh token request body POST /api/auth/refresh and POST /api/auth/logout
Salt session token X-Salt-Token header The seven operations that reach the custody layer

Onboarding a new customer

Open in the reference ↗

Onboarding runs on an emailed one-time code. It starts before any user exists, which is why it cannot rely on a session.

POST /api/auth/email/referral-code/validate
{ "referralCode": "ABC123" }

POST /api/auth/email/otp/request
{ "email": "ops@yourfirm.com", "referralCode": "ABC123" }

POST /api/auth/email/otp/verify
{ "email": "ops@yourfirm.com", "code": "123456" }

Validate the referral code first so a bad code fails in the form rather than midway through signup. referralCode is optional on the request call. otp/request accepts either no credential or a bearer token, so the same call works before a session exists and again from inside an authenticated app.

Open the diagram full screen ↗

The verify field is code, not otp

POST /api/auth/email/otp/verify takes code. Sending otp fails validation, and the error does not always name the field, so check this first.

Status Operation When
400 request Malformed email, or a referral code that fails validation
409 request A code is already pending for that email, or the email already belongs to an organisation
429 request, verify Rate limited
401 verify Code wrong or expired

The code path carries the account until the first owner exists. Once that owner is created, the organisation has an identity and later members join by invitation rather than by signing up again.


Logging in

Open in the reference ↗

An existing user authenticates with their wallet. The flow is challenge, sign, verify, and it ends in the same access token plus refresh token pair.

POST /api/auth/siwe/challenge
{ "walletAddress": "0x...", "referralCode": "ABC123" }

POST /api/auth/siwe/verify
{ "message": "<the exact challenge string>", "signature": "0x..." }

challenge returns a message containing a nonce. The wallet signs that message. verify takes the message and the signature back unchanged, so keep the exact string you were given; changing it between the two calls, even whitespace, fails the signature check.

Open the diagram full screen ↗

Status Operation When
400 challenge Malformed wallet address
403 challenge Wallet not permitted for this referral or organisation
409 challenge A challenge is already outstanding for that wallet
502 verify Upstream wallet or RPC failure. Retry it

Keeping the session alive

POST /api/auth/refresh
{ "refreshToken": "eyJ..." }

POST /api/auth/logout
{ "refreshToken": "eyJ..." }

GET  /api/auth/profile

The access token lives fifteen minutes by default. When a call comes back 401, refresh and retry instead of sending the user back to the wallet.

Sign out needs the refresh token in the body

POST /api/auth/logout reads the refresh token from the request body. Sending it only as a bearer header leaves the session live, so the token keeps working after the user has signed out.

GET /api/auth/profile returns the caller's user record. There is no public PUT for the profile, so profile updates are not part of the API surface today.


The Salt session token

Open in the reference ↗

Operations that ask the custody layer to co-sign carry a Salt session token alongside the access token.

X-Salt-Token: <salt-session-token>

It appears on exactly seven operations.

Operation Why custody is involved
POST /api/customers Creates the customer together with its Salt organisation
PUT /api/customers/{id} Mutates a record bound to the Salt organisation
POST /api/customers/{id}/members Writes a signer-adjacent directory entry
PUT /api/customers/{id}/members/{memberId} Same
DELETE /api/customers/{id}/members/{memberId} Same
POST /api/managed-accounts Creates the account from a Salt account
PUT /api/managed-accounts/{id} Changes signer-relevant account fields

403 on these calls comes from signing policy

The header is optional at schema level on all seven operations. Whether a call succeeds without it depends on your organisation's signing policy. A 403 means the policy refused the request, so rewriting the payload will not help. Check the status codes below first.

Status Meaning
401 Header missing on an operation that requires it
403 Session not wallet-authenticated, or the Salt identity does not match the wallet
419 Token expired. Re-authenticate with Salt and retry
502 Salt authentication service unavailable
504 Salt or a downstream RPC timed out

Which credential for which job

You are doing Send
Calling a read endpoint Authorization: Bearer <access-token>
Creating or updating a customer or team member Authorization: Bearer <access-token> plus X-Salt-Token
Creating or updating a managed account Authorization: Bearer <access-token> plus X-Salt-Token
Refreshing or signing out Refresh token in the body
Onboarding before any user exists The email code endpoints, no token needed

Authorization

Customers have two roles: owner and member.

An owner can change the organisation: members, access levels, and the customer record itself. A member works inside the organisation but cannot change who belongs to it. Paros refuses to remove or demote the last active owner, so an organisation always keeps someone who can administer it.

The role belongs to the person's membership of the organisation, keyed by wallet address, not to the token. It is returned on the member object as accessLevel, a number, with role as its string form. A new member is created through POST /api/customers/{id}/members and becomes active once they accept the invitation; an owner cannot force that transition.

owner and member are not the same thing as the platform roles user, viewer and admin. Those are Paros staff roles, and a customer integration always runs as user. A 403 can come from either layer: your platform role, your organisation role, or whether you own the record being changed.