Exchange a sign-in code for an API key, create a code, and exchange a workload identity token.
| Method | Path | Key | Does |
|---|---|---|---|
POST | /v1/auth/keys | none | exchanges a sign-in code for an API key |
POST | /v1/auth/keys/code | API key | creates a sign-in code for your own account |
POST | /v1/oauth/token | none | exchanges a workload identity token for an access token |
GET | /v1/oauth/jwks | none | the keys that sign access tokens |
All four are free. The flow the first two serve is Sign in with OpenDunes.
curl https://opendunes.com/v1/auth/keys \
-H "Content-Type: application/json" \
-d '{
"code": "<CODE_FROM_THE_CALLBACK>",
"code_verifier": "<YOUR_CODE_VERIFIER>",
"code_challenge_method": "S256"
}'| Field | Description |
|---|---|
code | Required. The code your callback received |
code_verifier | Required. The verifier the challenge was made from |
code_challenge_method | S256 or plain |
{
"key": "sk-…",
"user_id": "a2d14488-3337-4b5b-820c-034a4ed806dd"
}The key spends the user's wallet, and carries the name and cap the user approved. A code lives ten minutes and trades once; an unknown, expired or used code, or a verifier that does not match, is a 400.
With an API key, create a code for your own account — the same code the approval page issues:
curl https://opendunes.com/v1/auth/keys/code \
-H "Authorization: Bearer $OPENDUNES_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"callback_url": "https://soukbot.example/callback",
"code_challenge": "<CODE_CHALLENGE>",
"code_challenge_method": "S256",
"key_label": "Souk Bot",
"limit": 2000,
"usage_limit_type": "monthly"
}'| Field | Description |
|---|---|
callback_url | Required. HTTPS, or HTTP on localhost; anything else is a 422 |
code_challenge | Required |
code_challenge_method | S256 (default) or plain |
key_label | The new key's name; the callback's host without it |
limit | The new key's cap in DA |
usage_limit_type | daily, weekly or monthly |
expires_at | The new key's expiry, a date and time in the future |
{
"data": {
"id": "<CODE>",
"app_id": null,
"created_at": "2026-09-24T09:12:03+00:00"
}
}data.id is the code, exchanged at POST /v1/auth/keys.
A workload — a CI job, a cloud function — can trade a token its identity provider signed for a short-lived access token that acts as one of your API keys (RFC 8693 token exchange). The body is form-encoded:
curl https://opendunes.com/v1/oauth/token \
-d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \
-d subject_token_type=urn:ietf:params:oauth:token-type:jwt \
-d subject_token="$WORKLOAD_TOKEN"| Field | Takes |
|---|---|
grant_type | urn:ietf:params:oauth:grant-type:token-exchange |
subject_token_type | urn:ietf:params:oauth:token-type:jwt or urn:ietf:params:oauth:token-type:id_token |
subject_token | the workload's signed token |
{
"access_token": "eyJhbGciOiJSUzI1NiIs…",
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
"token_type": "Bearer",
"expires_in": 3600,
"scope": ""
}The access token travels as Authorization: Bearer <access_token> and carries its key's cap and guardrails. The token's issuer must be one OpenDunes trusts, with the audience and subject it was registered for; any other token is a 401. No issuer is trusted today, so every exchange answers 401, and GET /v1/oauth/jwks answers an empty key set:
{ "keys": [] }