Getting started
This page takes you from your first request to your first balance sync. You do every step in the sandbox environment.
API key
Every operator request authenticates with a tenant API key:
Authorization: Bearer pmk_sandbox_...The key has the form pmk_<environment>_<32 characters>. The environment is sandbox or live. Our team gives you the key during onboarding. We show the plain-text key once and we do not store it. If you lose it, we issue a new one.
You can rotate your key yourself on the settings screen of the tenant portal. The new key takes the scopes and the environment of the old key. Both keys work during the changeover. When you confirm the changeover, we revoke the old key. This portal does not issue or show keys.
Use the key from your server only. Do not put it in a browser, a mobile app or the widget configuration. The widget uses a short-lived session token (POST /v1/widget/sessions).
Incoming requests carry no extra signature, timestamp or nonce. TLS and the Bearer key are enough.
Environments
| Environment | Address | Key |
|---|---|---|
| Sandbox | https://sandbox.api.predixum.net | pmk_sandbox_* |
| Live | https://api.predixum.net | pmk_live_* |
The contract is the same in both environments: the same endpoints, the same error envelope, the same webhook signature. When you go live, only the address and the key change.
A key from one environment does not work in the other. A sandbox key on the live address returns 401 invalid_api_key, and the reverse is also true. When you go live, we revoke your sandbox keys and issue your live key.
Sandbox and live trade the same markets. A position that you open in sandbox also moves the price that live users see. Test with small amounts in sandbox.
Scopes
Every key carries one or more scopes. A call without the scope returns 403 insufficient_scope.
| Scope | Access it gives |
|---|---|
write:sync | User registration, balance sync (single and batch) and user reads. |
write:positions | Opening a position, and reads of positions, transactions and rejected attempts. |
read:markets | Market Feed: the market catalog and the prices. |
read:reports | Requesting, polling and downloading operator reports. |
write:tickets | Opening a support ticket from your own tools. |
First user registration
Register a user before you sync a balance or open a position for them:
PUT /v1/users/u-10492
Authorization: Bearer pmk_sandbox_...
Content-Type: application/json
{ "status": "active" }external_user_id is your own user id, and it is opaque to us. It has 1 to 64 characters and contains only A-Z a-z 0-9 . _ ~ -. Do not send an email address or personal data. If your id is an email address, map it to a pseudonymous id first. We store only the id and the status for a user.
The request is naturally idempotent and takes no Idempotency-Key. The status value is active or blocked. A blocked user cannot open a new position, but balance sync is not blocked.
You can read the user and the virtual balance with GET /v1/users/{external_user_id}.
First balance sync
When the balance of a user changes on your side, you send us the difference. We never pull data from your system.
POST /v1/balance-sync
Authorization: Bearer pmk_sandbox_...
Content-Type: application/json
Idempotency-Key: 3f0c9a52-6a8e-4f5b-9a1e-2b7d4c8e1f60
{
"external_user_id": "u-10492",
"direction": "credit",
"amount": "100.00",
"resulting_balance": "100.00"
}directioncarries the sign:creditordebit.amountis always positive.- Amounts are JSON strings (
"100.00"). An amount that you send as a number returns400 validation_failed. resulting_balanceis optional. It is the balance that your system shows after the movement. Reconciliation uses this value as its basis. Accounts for which you never send aresulting_balanceare reported as unverifiable in reconciliation, so we recommend that you send it.Idempotency-Keyis required. Use the same key for the same intent and a new key for a new intent. The key has at most 128 characters and cannot contain:. We recommend a UUID.
The response returns the id of the ledger transaction and the resulting virtual balance:
{ "transaction_id": "0192a0b1-7c2e-7d3f-8a4b-5c6d7e8f9a0c", "balance": "100.00" }Compare this balance with your own record in every response. This way you see a drift in the request that causes it, and you do not wait for the scheduled reconciliation run.
A request that you send again with the same key and the same body returns the first response byte for byte, and it applies nothing a second time. If the same key arrives with a different body, the response is 422 idempotency_key_reused and nothing is applied.
To send up to 500 movements in one request, use POST /v1/balance-sync/batch. The items apply independently, and the response carries a separate result for each item.
Error envelope
Every error response has the same body:
{
"error": "would_overdraw",
"message": "...",
"details": null,
"request_id": "...",
"source": "...",
"ts": "2026-09-26T12:00:00Z"
}Branch on the error value. The Error codes page lists all the codes. Quote the request_id and source values in a support request. request_id also arrives in the X-Request-Id response header.
All time values are UTC in RFC 3339 format with a Z suffix, in requests and in responses.
Next steps
- For the position endpoints and all the schemas, read the API reference.
- To receive events, read the Webhook guide.
- To show the markets on your own page with the widget, read the Widget guide.
- For a live key, read the Go-live checklist.