Choose your starting point
Trade in the app
Set up your identity and account with browser passkey approvals you control.
Trading agents
Connect an owner-approved API key, with optional MCP tools.
Market makers
Use a server-issued bot key with REST and WebSockets. MCP is optional.
Shared API reference
Two public surfaces. Both generated from the same schemas the engine validates against.REST API
82 operations. CCXT /v2 has 17 backed methods and
7 explicit HTTP 501 stubs. Edel is perpetual-only; spot is not offered.
WebSockets
18 channels. Order book, trades, marks, funding, and
your own fills, positions and margin — pushed, never polled.
Base URLs
The Production venue on mainnet has not launched, so it publishes no hostnames yet. They appear here when the public venue opens.Authentication
Start here — every private endpoint and account channel depends on a verified DFNS EndUser. Signing in to the private demo app itself never identifies a trader and never creates an Edel account.Registration is available only when
socialRegistrationProviders advertises a provider.
Where offered, the direct flow is provider OIDC → DFNS social registration → WebAuthn
passkey (FIDO2). Authentication creates a DFNS EndUser credential, a delegated signing key,
and the durable Edel account mapping. It does not create a DFNS wallet or a Canton Party.Read what this venue offers
GET /v1/auth/capabilities names the sign-in methods this venue offers now. It needs no
credential. Read it at startup and again before you show a sign-in method, because a venue can
change what it offers. Never cache it as a permanent answer.
Passkey login for an existing user can be available while registration is closed. The public
API exposes no API-key, personal-access-token, or passkey credential-management routes. Login
returns a DFNS EndUser bearer. Only short-lived realtime tickets can then be minted, rotated,
and revoked through Edel’s public API.
Register only through an advertised provider
1
Create one stable browser attempt
Generate one opaque
Idempotency-Key and retain it in tab-scoped session storage across
refreshed Google tokens. The key identifies an attempt; the verified Google subject owns it.2
Start direct social registration
Call The response is Pass the full returned WebAuthn options to the browser; the abbreviated response above
shows the fields guaranteed by the public contract plus representative DFNS options.
POST /v1/auth/social/register only when socialRegistrationProviders contains
google. The public request contains the Google OIDC ID token and stable attempt key only:external-auth-register-init/v1 with user,
temporaryAuthenticationToken, and the DFNS WebAuthn creation challenge. The API supplies
the configured organization and fixed Oidc provider kind.3
Create and submit the passkey
Use
navigator.credentials.create() with that challenge. Convert the returned WebAuthn
attestation to the exact public request below and call POST /v1/auth/register/complete.
Re-send a fresh Google token for the same subject. The wire literal is Fido2:referralCode is an optional 16-character lowercase hex field. The API resolves it before
DFNS writes a credential. An invalid code reaches zero DFNS mutations; when affiliate-public
policy is disabled or unreadable, a request containing the field is hidden with 404.4
Persist the returned identity
Success returns
external-auth-register-complete/v1 with the DFNS user and registered
username. The API accepts only the initiated DFNS user and configured organization, marks
credential completion durable before key provisioning, then creates exactly one
DFNS-user-to-Edel-account mapping. The legacy walletState response field is not evidence
that registration created a DFNS wallet or Canton Party.Log in an existing user
1
Choose one composed login
For a passkey, send the exact registered DFNS After
username to POST /v1/auth/login/init,
sign its challenge with navigator.credentials.get(), then send challengeIdentifier and
a firstFactor whose kind is Fido2 to POST /v1/auth/login/complete. A caller-chosen
username alone never registers or authenticates a new EndUser.navigator.credentials.get(), submit this public request shape:2
Use Google when advertised
When Passkey completion and social login both return the same session shape:
socialLoginProviders contains google, the returning user may exchange the Google
OIDC ID token directly for the same DFNS session:3
Run the shortest API test
The live snapshot above offers passkey login to an already registered user. After passkey
completion returns Use the bearer—not the short-lived WebSocket ticket—for authenticated REST calls:Rotate the short-lived ticket before Replace
external-auth-login/v1, send its DFNS EndUser token as
Authorization: Bearer <token> on every private REST request. Market-data routes need no
credential. Minting a realtime ticket proves the bearer and returns the mapped accountId:expiresAt; the ticket itself authorizes rotation:REALTIME_TICKET with the returned ticket. Revoke the ticket family when finished;
presenting the current or immediately previous ticket returns 204 No Content:Retry and recovery
These routes apply only to a registration attempt started while its provider was advertised; they do not provide lost-passkey credential recovery.- A conclusive rejection before an irreversible vendor result can retry with fresh Google or
WebAuthn proof. A User Action failure before
POST /keyscreates no key-attempt fence. - An ambiguous registration result must not replay registration. Call
POST /v1/auth/social/register/recoverwith the sameIdempotency-Keyand a fresh Google token. It uses matching DFNS login evidence and never repeats registration or passkey completion. - After DFNS returns the matching user and organization, key-provisioning failures never reopen
registration. A conclusive
POST /keysrejection may retry through authenticated login. An unknown result remainssubmitted_unknown: recovery lists only the exact owner/name and stays pending for manual resolution if absent; it never issues a blind second POST. - If only account mapping persistence failed,
POST /v1/auth/mapping/recoverre-introspects the bearer and rebuilds the deterministic mapping without accepting a client-supplied user or account id. Lost-passkey recovery is not implemented by either mapping recovery route.
Expected errors
Failures use theexternal-rest-error/v1 envelope. Branch on code, retryable, and
recoveryActions; retain requestId when reporting a failure. Ticket failures also include
realtimeTicketFailure with invalid, replayed, replaced, expired, revoked, or
store_unavailable.
A withdrawal payout claim is signed against a ledger time the venue fixes when it issues the
passkey challenge, and the ledger accepts it only for about 60 seconds after that.
Complete the claim (
POST /v1/withdrawals/:withdrawalId/confirm with action: "complete")
within 30 seconds of the challenge. A later complete does not fail: it answers
200 with status: "approval_required" and a new challenge for a fresh acceptance, which you
sign and complete the same way. A complete that does fail answers 503 service_degraded
with retryable: true and a withdrawalFailure naming the cause:
A refused acceptance becomes claimable again once the venue proves it never reached the ledger,
at most 10 minutes after its challenge. Until then
initiate answers the
withdrawal without a new challenge; read the status and start again later. A payout whose claim
deadline passes unclaimed reads status: "offer_expired" and can no longer be claimed.
WebSockets
Sockets do not take your bearer token. A long-lived connection would park a long-lived credential, so account channels use short-lived, account-scoped tickets instead. Market channels need no ticket at all.1
Mint a ticket
Call
POST /v1/realtime/ticket with the bearer your REST calls send: the DFNS EndUser
token, or an API key with read scope. The response carries the ticket, the accountId
it is bound to and expiresAt in epoch milliseconds, and is never cacheable.2
Authenticate the socket
Open the socket at the WebSocket URL under Base URLs and send the ticket in
an auth frame:Wait for A ticket authenticates a socket once: sending it again, on any socket, is refused as
auth_ok before you subscribe. It names the account the socket is now bound to
and the expiresAt at which this socket session ends:replayed. A refused auth frame answers an error frame instead of auth_ok:unauthorized: the ticket was refused;realtimeTicketFailurenames why, with the values listed under Expected errors.rate_limited: the socket sent too many auth frames in a short window. Wait before sending another.auth_unavailable: the venue could not check the ticket just now. Try again shortly.
1008. If it does, open a new
socket and start again with a new ticket.3
Subscribe after auth_ok
Only after A subscribe sent before
auth_ok arrives, send the subscribe envelope for each account channel, with the
accountId from auth_ok:auth_ok arrives, even one sent right after the auth frame, is
refused with a realtime-error/v1 frame whose code is unauthorized. A subscription
exists only once its realtime-subscription-accepted/v1 frame arrives; each channel page
shows that frame.Keep the socket authenticated
A socket session lasts until theexpiresAt in its latest auth_ok. Tickets expire in
minutes, so read expiresAt rather than assuming a lifetime.
In the last 30 seconds before expiresAt, the socket sends one auth_expiring frame:
expiresAt:
- Call
PUT /v1/realtime/ticketwith the current ticket as its body. It takes no bearer: the ticket itself is the credential, so a long-running client never re-presents your session. It returns the next ticket of the same family. Always rotate with the newest ticket: a ticket that has been rotated can neither be rotated again nor authenticate a socket (replaced). - Send the new ticket in a new auth frame on the same socket. The new
auth_okcarries the laterexpiresAt, and the account subscriptions stay open. Rotating without this step does not extend the socket session.
expiresAt passes first, the socket stays open and market subscriptions keep flowing, but
it delivers no more account frames. It removes every account subscription and sends:
expired. Mint a new one with
POST /v1/realtime/ticket, send it in an auth frame on the same socket, wait for auth_ok
and subscribe again.
DELETE /v1/realtime/ticket revokes the whole ticket family at once: the switch to reach for
if a client is compromised. Every later rotation or auth frame with a ticket from that family
is refused as revoked.
private_subscriptions_dropped carries one of these reasons. Each one removes every account
subscription on the socket and leaves the socket open.
Why build here
Collateral stays on the ledger
Deposits and withdrawals settle on Canton. Your balance is backed by an on-chain
position, not an exchange IOU.
State you can replay
Every accepted order, fill and funding accrual is journaled in sequence. Venue state is
reconstructed from that journal, not from a mutable row someone can edit.
Types that cannot drift
Request, response, envelope and channel payloads are projected from the venue’s own
schemas. There is no handwritten copy to fall out of date with the running engine.
Fails closed, on purpose
Stale or disagreeing market data blocks risk-increasing orders instead of filling them
against a price nobody trusts.
The trading model
Two order types, deliberately.limit executes at your price or better; market executes
within the venue’s protective price band. Optional postOnly makes a limit order maker-only.
reduceOnly guarantees an order can only shrink a position, never grow or flip it. Attach
autoClose prices at placement and take-profit and stop-loss are armed together as an OCO
pair — the first to fire cancels its sibling.
Time-in-force is not a public field. The venue derives IOC for market orders and GTC for limit
orders; postOnly keeps a limit maker-only.
Every monetary value crosses the wire as an integer atom string, with a formatted decimal
sibling for display. No floating point touches a balance.
Rate limits and back-pressure
600 authenticated requests per 60 seconds by default, for each API key or signed-in user. The window starts with your first request and resets 60 seconds later; it does not roll. Minting a realtime ticket counts against the same budget. A venue can set a different limit; a staging or production venue never allows more than 6,000 per window. Exceed it and you get429 with the code rate_limited, retryable: true, and a
retry_after recovery action. The response has no Retry-After header: wait for your window
to reset, at most 60 seconds, then retry the same request, with the same
Idempotency-Key when it has one. Never retry an order with a new clientOrderId because of a
rate limit.
Requests whose credential fails verification are also limited: 600 per
60 seconds by default from one network address. A request that authenticates does not
count against that limit.
Every rejection is a typed error rather than an opaque status, so a client can branch on the
code instead of parsing prose.
Two habits that keep you well inside any limit:
- Stream instead of polling. Order books, marks, fills and margin all arrive on WebSocket channels. Repeatedly polling REST for state that is already pushed is the most common way integrators hit a limit.
- Reuse one socket. Subscribe to many channels on a single connection rather than opening one per channel.
retryable tells you whether
a retry can ever succeed. A validation failure is not retryable no matter how long you wait.
WebSocket channels
Connect at/streams/v1/ws.
Every channel, with its auth, snapshot source and source status, is listed by
GET /streams/v1/contract on the same host, also served at /streams/v1/catalog.
Market data — open to everyone.
market.trades · market.book · market.ticker · market.price · market.funding · market.status · market.candles
Your account — authenticated.
account.orders · account.fills · account.durability · account.positions · account.pnl · account.margin · account.balance · account.withdrawals · account.reconciliation · account.alerts · account.earnings
Open the WebSockets tab for the payload shape, subscribe frame and example of each
channel. Account channels need a ticket: see WebSockets.
Contract versions
- REST — Edel PERPS External REST API 0.1.0, 82 operations.
- CCXT /v2 — 17 backed methods and 7 HTTP 501 stubs. Stubbed methods remain
has[method] = false; spot is not offered. - WebSockets — Edel PERPS Realtime WebSocket API realtime-contract/v1, 18 channels.
Built by Edel.