Supporting Reads¶
Something goes wrong in trading: a strategy stays quiet, a run stops halfway, a balance disagrees with what you expected. These are the reads you reach for then. Sixteen operations, and only the two execution actions change anything.
Order matters. Read the execution first, because it carries the error. Read the activity second, because it carries the attempt.
Open the diagram full screen ↗
Executions¶
GET /api/executions
GET /api/executions/{id}
POST /api/executions/{id}/cancel
POST /api/executions/{id}/retry
List parameters: accountId, customerId, playlistId, triggerId, status, page,
limit.
An execution is one run of a playlist or strategy, so a failed run leaves a record here. Filter
the list down to the run you care about, then fetch GET /api/executions/{id} for that one. If
a scheduled strategy never produced an execution at all, there is nothing on this endpoint to
read: the question becomes whether it was attempted, and that is the activities read below.
cancel is for a run that is still in flight. retry is for one that failed. Both take an
optional body with reason, stepInTriggerCategory and stepInReason.
A 400 here is a state problem, not a network problem
Both actions return 400 when the execution is not in a state that permits the action, for
example retrying a run that already succeeded. Read the run's status, pick the action that
fits, and do not retry the request itself.
Activities¶
GET /api/activities
GET /api/activities/customer/{customerId}
GET /api/activities/execution/{executionId}
The broad read takes page, limit, executionId, accountId, strategyId, venueId,
protocol, actionType, status, chain, customerId, search, fromTs, toTs,
sortBy and sortOrder.
Activities record what the system did: deposits, orders, transfers, cancellations. Where the
execution tells you a run failed, the activity tells you how far it got. Pass the
executionId you just read and the feed narrows to that run.
fromTs and toTs bound the window. Set both: an unbounded feed over a busy account pages
through a lot of history to answer a question about one afternoon. The scoped variants,
/api/activities/customer/{customerId} and /api/activities/execution/{executionId}, narrow to
one customer or one run without hand-writing filters against the whole feed.
Balances¶
GET /api/balances?accountId={id}&address={address}
GET /api/balances/asset?chain={chain}&assetAddress={address}
The first returns balances for an account or address. The second returns one asset on one
chain. Both accept optional accountId and address; the second requires chain and
assetAddress.
These are custody-side balances
Hyperliquid balances are not here. They live on the Hyperliquid endpoints. A balance that looks wrong on this endpoint and correct on Hyperliquid means you are reading the wrong side of the custody boundary, not that funds moved.
Positions¶
GET /api/positions?account={address}&venueId={venueId}
GET /api/positions/allocation?accountId={id}&chain={chain}&assetAddress={address}
/api/positions requires account and takes an optional venueId. It returns open positions
for that account at that venue. Omit venueId and the venue filter goes away.
/api/positions/allocation requires accountId, chain and assetAddress, and reports how
that asset is split across venues. A 500 from this endpoint usually means the aggregation
failed because a venue timed out, so retry before you treat it as a data problem.
Venue timeouts look like server faults
The allocation read fans out across the venues holding the asset, and one slow venue is usually enough to fail the aggregation. Retry once, then check that venue's status through Venues if it keeps failing.
The catalogue: venues, products and assets¶
Read these before you build a request that needs a venueId, productId or assetId.
Venues¶
GET /api/venues
GET /api/venues/{id}
Parameters, all optional: page, limit, type, chain, status, search.
The Hyperliquid venue carries slug: "hyperliquid". Find it by search rather than hardcoding
an id, because ids are environment-specific and the one you saved from another environment
will not resolve.
GET /api/venues?status=active&search=Hyperliquid
Products¶
GET /api/products
GET /api/products/{id}
Parameters, all optional: page, limit, venueId, riskLevel, status, search,
chain.
Filter by venueId to list the tradable instruments on a venue, then narrow with riskLevel
or status. A product id looks like hyperliquid-perp, or a per-market symbol, depending on
the venue.
Assets¶
GET /api/assets?chainId={chain}&address={assetAddress}&includePriceOnly={bool}
POST /api/assets/prices
GET resolves an asset on a chain and returns its id, symbol and decimals. Every
parameter is optional, but supplying both chainId and address is how you get one
unambiguous asset instead of a list to pick through. This is the call that turns a USDC
address into an assetId during Hyperliquid deposit setup.
POST /api/assets/prices takes a body with tokenIds or tokens and returns prices. The
body is optional at schema level, so send at least one of the two or the call has nothing to
price.
The Salt layer¶
Paros never holds your keys. Salt does, and it signs on your behalf only when a wallet it recognises has authorised the request. That is the whole reason this layer exists: the calls that change custody have to carry proof that a signer approved them.
Open the diagram full screen ↗
Which calls need it¶
A read never needs a token. The writes below do, because each one either creates custody structure or moves who controls it:
| Call | What Salt does with it |
|---|---|
POST /api/customers |
Creates the custody organisation behind the customer |
POST /api/managed-accounts |
Binds a finished vault to a managed account |
PUT /api/managed-accounts/{id} |
Changes agentId or orchestrationContractAddress |
POST /api/customers/{id}/members |
Places a signer in the organisation |
PUT /api/customers/{id}/members/{memberId} |
Changes a signer's role or address |
If a call in that table goes out without a valid X-Salt-Token, the answer is 403 from the
custody boundary. The Paros session is fine; the custody half was never satisfied.
Getting a token¶
Three calls, and one of them happens in the wallet rather than over HTTP.
POST /api/salt/auth-message
POST /api/salt/authenticate
POST /api/salt/refresh
Ask for a message, sign it, exchange the signature for a token:
{ "walletAddress": "0xA1..." }
Paros returns a SIWE message built against Salt's nonce and the Salt chain. Sign it with the same wallet, then send back exactly what you received:
{ "walletAddress": "0xA1...", "message": "<the exact string>", "signature": "0x..." }
The response carries token, refreshToken and verifiedUser. Put token in
X-Salt-Token on every call in the table above.
The wallet must match the session
The signing wallet has to be the wallet on your Paros session. A message signed by a different address is refused, and so is a message whose domain, URI or chain has been edited. Send the string back unchanged.
Worked example: creating an account¶
The vault exists first. Then one authenticated call binds it:
POST /api/salt/auth-message
{ "walletAddress": "0xA1..." }
POST /api/salt/authenticate
{ "walletAddress": "0xA1...", "message": "app.salt.space wants you to sign in...", "signature": "0x9f..." }
{ "token": "eyJ...", "refreshToken": "eyJ...", "verifiedUser": { "address": "0xA1...", "publicKey": "0x04..." } }
POST /api/managed-accounts
Authorization: Bearer <paros session token>
X-Salt-Token: eyJ...
{ "saltAccountId": "3f7c1e9a-..." }
Paros verifies the token against Salt, reads the vault, confirms the organisation matches the
customer, and answers 202 with a jobId. Poll GET /api/managed-accounts/{id} until the
status settles.
When the token expires¶
Salt tokens are short-lived. The refusal is 419, which is distinct from 403 on purpose: it
means the token aged out rather than that the signer was rejected.
POST /api/salt/refresh
{ "refreshToken": "eyJ..." }
The refresh returns a fresh token and refreshToken. Treat a 401 from refresh as the end
of the session: ask for a new message and go through the flow again. Do not retry the original
call with the same token.
POST /api/salt/authenticate-token answers whether a token is still good, which is worth
calling before a batch of writes rather than discovering the expiry halfway through.
Reads that need no token¶
GET /api/salt/organisations
GET /api/salt/organisations/{organisationId}
GET /api/salt/organisations/{organisationId}/accounts
GET /api/salt/organisations/{organisationId}/customer
GET /api/salt/accounts/{accountId}
GET /api/salt/invitations
These tell you what Salt knows about your organisation, which account sits behind a customer,
and what invitations are outstanding. Useful when a write fails with 403 and you need to see
whether the signer is even in the organisation.