Skip to content

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

Open in the reference ↗

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

Open in the reference ↗

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

Open in the reference ↗

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

Open in the reference ↗

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

Open in the reference ↗

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

Open in the reference ↗

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

Open in the reference ↗

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.