REST API
75 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
These are the Development venue endpoints. Production hostnames will be published here when the public venue opens.Authentication
Start here — every private endpoint and account channel depends on a verified DFNS EndUser. Clerk/Vercel protects access to the private Demo only. Its subject, email, and session never identify a trader, call DFNS, or create thedfns_user_accounts mapping.
Registration is available only when
socialRegistrationProviders advertises a provider.
Where composed, 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.Configuration checklist
No secret belongs in browser code or this guide. The API requiresDFNS_API_URL, DFNS_ORG_ID,
DFNS_CRED_ID, DFNS_PRIVATE_KEY, and DFNS_AUTH_TOKEN together. Durable registration also
requires DATABASE_URL and EDEL_SECRETS_MASTER_KEY. Direct Google flows require
DFNS_SOCIAL_AUTH_PROVIDERS=google and GOOGLE_OIDC_CLIENT_ID; the Demo receives only the
public VITE_GOOGLE_CLIENT_ID. Its Vercel build also requires a non-browser
GOOGLE_OIDC_CLIENT_ID copied from the target API runtime; the build fails without both values or
when they differ. The DFNS organization must have Google social registration/login enabled, the
correct WebAuthn relying-party origins, and the reviewed User Action and Keys permissions.
GET /v1/auth/capabilities is the runtime source of truth. Existing-user passkey login may be
available while direct Google registration is hidden. Delegated username/email registration,
EndUser email OTP, and lost-passkey recovery are explicitly unavailable until their own product
identity and recovery authorities are selected.
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 Roman's 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.
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.1
Mint a ticket
POST /v1/realtime/ticket with your session. The response carries the ticket and its
expiry, and is never cacheable.2
Connect and subscribe
Present the ticket on the socket, then subscribe to your account channels. Market
channels need no ticket at all.
3
Rotate before expiry
PUT /v1/realtime/ticket exchanges a live ticket for the next one, and takes no bearer —
the ticket itself is the credential, so a long-running client never re-presents your
session.DELETE /v1/realtime/ticket revokes the whole family at once —
the switch to reach for if a client is compromised.
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 requests per minute per credential on authenticated endpoints, counted in a rolling 60-second window and keyed to your session token. Minting a realtime ticket counts against the same budget. Exceed it and you get429 with the canonical code rate_limited, retryable: true, and a
retry_after recovery action. 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.
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 Authentication.
Contract versions
- REST — Edel PERPS External REST API 0.1.0, 75 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.