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¶
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¶
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¶
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.