Request signing
Every API request is signed with an Ed25519 key that you generate. You register the public key with us and keep the private key. We never see it, so a full copy of our database gives an attacker nothing to forge a request with. The signature covers the method, the path, the query string, a timestamp and a hash of the body. Change any of them and the signature no longer verifies. A signed read cannot be turned into a write, and a signed order cannot have its size edited in transit. We verify the signature before we parse the body. A request that fails verification is rejected without its contents being read. Timestamps have to be within 30 seconds of our clock, so a captured request stops being useful quickly. If your clock is off, thetime endpoint returns ours and is the one call that does not need a timestamp.
The WebSocket handshake is signed the same way, with WS in place of the HTTP method. A handshake signature cannot be reused against a REST route, and vice versa.
Scope comes from the key
Each API key is created for one trader in one pod, and that pairing cannot be changed later. The request itself never names a pod or an organization./orders means your orders in your pod, because the key already says which pod that is. There is no way to write a request for someone else’s pod, so there is no permission check that could be misconfigured to allow one.
A key also carries a capability. A trade key can call venue endpoints; a data key can only use discovery and the stream.
See Traders and permissions.
Revocation
You can revoke at three levels, and each one is independent of the others:Revoke the key
One credential stops working. The trader’s other keys are unaffected.
Remove the trader
Every key the trader holds stops working, in every pod, and so does their sign-in.
Remove from a pod
The trader loses access to one pod and keeps the others.
401 UNAUTHORIZED. If the responses were different, someone without a valid key could work out which key IDs exist by watching the error codes. We record the actual reason on our side, so support can tell you what happened.
Data isolation
Isolation between pods is enforced in the database, not left to application code. Every table that holds orders, fills, positions, balances, wallets or withdrawal addresses has a row-level security policy. Each tenant-bound operation runs inside a transaction that is bound to one organization and one pod, and the database only returns or accepts rows that match. If no tenant is bound, a read returns nothing and a write is refused. The policy applies to the table owner as well. So on those operations, even if a query in our code forgot a filter, the database would still not return another pod’s rows.Venue credentials
When you connect a venue account such as Kalshi, the private key you supply is imported into a hardware security module as a non-extractable key. It cannot be exported afterwards, by us or anyone else. Our database stores a reference to that key and public metadata, and nothing that could be used to sign. Replacing a credential is done carefully. The new key is imported, test-signed, and checked against the venue with a real authentication before it takes over from the old one. If any step fails, the working credential is untouched. Removing a credential schedules the key for destruction. See Off-boarding and revocation. These credentials only let us place and cancel orders. Your cash stays at the venue, in your account.Pod wallets
Each pod has its own wallet for on-chain venues. It is created with the pod and cannot be moved to another pod, and no pod can spend from another pod’s wallet. Signing is handled by a custody provider. Our services hold a wallet reference and an address, never the key, so there is no private key in our databases, logs or servers to leak.Network
- All traffic to the API is over TLS.
- The database is in a private subnet with no route to the internet, and is encrypted at rest.
- Calls between our own services are signed and verified too. A service does not trust a request just because it came from inside the network.
Retries and duplicate orders
Order submissions require an idempotency key. If you send the same key with the same order again, you get the original response back. If you send the same key with a different order, you get a conflict error instead of a second order. This is what makes it safe to retry after a timeout, when you do not know whether the first attempt was received. Request rates are limited per organization. See Rate limits.What you should do
- Keep the private key in a secrets manager. Never commit it or ship it in a config file. Anyone who has it can trade the pod it belongs to.
- One key per trader per pod. Do not share keys between people or services, or you lose the ability to revoke one without affecting the others.
- Rotate keys on a schedule, and immediately if you think one has leaked. Rotation does not affect open positions.
- Keep your system clock in sync. Clock drift shows up as authentication failures that come and go.
- Send an
Idempotency-Keyon every order. - If a venue credential is compromised, revoke it at the venue as well as here.
To report a security issue, or if you believe a key has been compromised, email contact@parlayx.com.