Sign in with OpenDunes — let your users connect their OpenDunes wallet to your app in one click.
With Sign in with OpenDunes, your app can get an API key on its user's wallet, with the user's approval — the user never pastes a key into your app. The flow is PKCE (Proof Key for Code Exchange).
https://opendunes.com/auth, with its callback_url and a code challenge.callback_url with a code.code at POST /v1/auth/keys for an API key.The key spends the user's wallet. The user sees it among their keys and can cap, pause or delete it at any time.
Generate a random code_verifier, keep it, and derive the code_challenge: the SHA-256 of the verifier, base64url-encoded.
async function createSHA256CodeChallenge(verifier: string) {
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
return btoa(String.fromCharCode(...new Uint8Array(hash)))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
const codeVerifier = crypto.randomUUID() + crypto.randomUUID();
const codeChallenge = await createSHA256CodeChallenge(codeVerifier);Then send the user to:
https://opendunes.com/auth?callback_url=<YOUR_SITE_URL>&code_challenge=<CODE_CHALLENGE>&code_challenge_method=S256| Parameter | Required | Effect |
|---|---|---|
callback_url | Yes | Where the user returns. HTTPS, or HTTP on localhost or 127.0.0.1 on any port — for CLI tools and local apps |
code_challenge | Yes | The challenge derived from your verifier |
code_challenge_method | No | S256 (the default, recommended) or plain |
key_label | No | Names the key the user will approve. Without it, the key takes your callback's host |
The user signs in if they are not already, sees your app's name and your callback address, may set a spending cap on the key, and chooses Authorize or Deny.
On Authorize, the user comes back to your callback with a code:
const code = new URLSearchParams(window.location.search).get("code");On Deny, they come back with error=access_denied and no code.
Exchange the code, with the verifier you kept, for a key:
const response = await fetch("https://opendunes.com/v1/auth/keys", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
code: "<CODE_FROM_QUERY_PARAM>",
code_verifier: "<CODE_VERIFIER>",
code_challenge_method: "S256",
}),
});
const { key, user_id } = await response.json();The exchange needs no key of its own. A code lives ten minutes and trades once.
Store the key securely — in the user's browser or your own database — and send it as a Bearer token on any request:
await fetch("https://opendunes.com/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "anthropic/claude-sonnet-5",
messages: [{ role: "user", content: "Marhba!" }],
}),
});A server that already holds an API key can create a code for its own account at POST /v1/auth/keys/code, with the same callback_url, code_challenge, code_challenge_method and key_label, and optionally limit (a cap in DA), usage_limit_type (daily, weekly or monthly) and expires_at. The answer's data.id is the code, exchanged as in Step 2. See the OAuth reference.
| Status | When |
|---|---|
400 | The code is unknown, expired or already used, or the verifier does not match its challenge |
400 | The request is missing code or code_verifier, or names a method other than S256 or plain |
422 | Creating a code with a callback_url that is neither HTTPS nor HTTP on localhost |
Every error comes in the error object.
code_verifier in memory only — never log or persist it.S256. plain sends the verifier's twin in the address bar.