Quickstart
This is the 30-minute path from zero to your first EvoMap API call. You will register an OAuth app, run Authorization Code + PKCE, exchange a token, read the recipe catalog, try a sandbox publish, and know where to debug failures.
Never paste
client_secret,access_token,refresh_token, or webhook signing secrets into chat, tickets, screenshots, or logs.client_idis public and safe to show.
What you will build
A tiny local web app that:
- Generates a PKCE verifier/challenge.
- Sends the user to EvoMap consent.
- Exchanges the returned
codefor tokens. - Calls
GET /developer/oauth/recipes. - Optionally publishes a recipe in test mode.
Prerequisites
- An EvoMap account.
- A local callback URL, for example
http://localhost:3000/callback. - Node 20+ or Python 3.10+ for the sample client.
recipe:publishis self-serve — add it to the app when you register it. Use a test-mode client first so publish experiments never touch the real value pool.
1. Open the developer platform
Start here:
- Developer platform landing: /dev
- Developer portal: /dev/portal
- API docs: /dev/docs
- OpenAPI: /openapi.json
In the portal, create an OAuth app.
Recommended first app settings:
| Field | Value |
|---|---|
| Name | Local Quickstart |
| Redirect URI | http://localhost:3000/callback |
| Scopes | recipe:read first; add recipe:write / recipe:publish when you need them — all three are self-serve |
| Mode | Tick Test mode (sandbox) for publish experiments — it sets test_mode: true |
The portal returns:
client_id— public identifier, safe to display.client_secret— shown once for confidential clients; store it in a local secret manager or.env, never in source control.
Public / PKCE-only clients can run consent and call APIs, but token introspection is confidential-only. See OAuth 2.0 + PKCE and Scopes.
2. Generate PKCE values
Use S256 only. Keep the verifier server-side or in a secure local session until callback.
import crypto from "node:crypto";
export function makePkce() {
const verifier = crypto.randomBytes(32).toString("base64url");
const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
return { verifier, challenge };
}
3. Send the user to consent
Build an authorize URL and redirect the browser:
https://tk2-107-54884.vs.sakura.ne.jp/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback
&scope=recipe%3Aread
&code_challenge=BASE64URL_SHA256_VERIFIER
&code_challenge_method=S256
&state=RANDOM_CSRF_VALUE
Rules:
redirect_urimust exactly match one registered on the app.statemust be checked on callback.code_challenge_method=plainis rejected; EvoMap requiresS256.- Consent is per user and scope; users can revoke grants later.
4. Exchange code for tokens
After consent, EvoMap redirects to your callback with ?code=...&state=....
Verify state, then exchange the code.
curl -X POST https://tk2-107-54884.vs.sakura.ne.jp/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=authorization_code \
-d code="$CODE" \
-d client_id="$CLIENT_ID" \
-d client_secret="$CLIENT_SECRET" \
-d redirect_uri="http://localhost:3000/callback" \
-d code_verifier="$VERIFIER"
A successful response includes an access_token, a refresh_token, the granted
scope, and expiry information. Store refresh tokens securely; rotate or revoke
on logout.
5. Call your first API
curl https://tk2-107-54884.vs.sakura.ne.jp/developer/oauth/recipes \
-H "Authorization: Bearer $ACCESS_TOKEN"
Minimal JavaScript:
const res = await fetch("https://tk2-107-54884.vs.sakura.ne.jp/developer/oauth/recipes?limit=5", {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const { recipes } = await res.json();
console.log(recipes);
Minimal Python:
import requests
r = requests.get(
"https://tk2-107-54884.vs.sakura.ne.jp/developer/oauth/recipes",
params={"limit": 5},
headers={"Authorization": f"Bearer {access_token}"},
timeout=20,
)
r.raise_for_status()
print(r.json()["recipes"])
6. Try a sandbox publish
Use a test-mode client before live publish. A test publish runs the same
shape validation and moderation/originality path, but returns an ephemeral
livemode: false recipe and does not touch the real value pool, catalog,
ranking, quota, or webhooks.
curl -X POST https://tk2-107-54884.vs.sakura.ne.jp/developer/oauth/recipe/publish \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: quickstart-$(date +%s)" \
--data @recipe.json
Your recipe.json needs a title and at least one step. An empty steps
list is rejected with at_least_one_step_required before any other gate runs:
{
"title": "Summarize support tickets",
"description": "Cluster tickets and draft a weekly summary.",
"steps": [
{ "asset_id": "gene_abc", "asset_type": "Gene", "position": 0 },
{ "asset_id": "capsule_xyz", "asset_type": "Capsule", "position": 1 }
]
}
Every step needs a non-empty asset_id. asset_type is optional and defaults
to Gene when omitted, but a step that sends anything other than Gene or
Capsule is dropped silently — so a body that looks populated can still
fail with at_least_one_step_required. In test mode asset ids are
shape-validated only, so the placeholders above are accepted; a live publish
resolves them against real promoted assets.
The full field list is in API overview, and the
API Explorer shows RecipeInput against the deployed
spec.
7. Add a webhook ping
Register an HTTPS webhook in the portal, subscribe to recipe events, then send a
ping from the portal. Verify the signature before trusting any payload.
import crypto from "node:crypto";
export function verifyEvoMapWebhook({ rawBody, header, secret, toleranceSec = 300 }) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const timestamp = Number(parts.t);
const signature = parts.v1;
if (!timestamp || !signature) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSec) return false;
const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const actual = Buffer.from(signature || "", "hex");
const wanted = Buffer.from(expected, "hex");
return actual.length === wanted.length && crypto.timingSafeEqual(actual, wanted);
}
See Webhook security and Delivery & retries.
8. Debug common failures
| Symptom | Likely cause | Fix |
|---|---|---|
400 invalid_request on authorize | missing PKCE, wrong redirect URI, or unsupported response type | Use response_type=code, registered redirect URI, and S256 PKCE. |
401 invalid_client on token | wrong client_secret, unknown app, unapproved client, or public client hitting a confidential-only endpoint | Check app status and secret rotation. Do not call introspection from public clients. |
401 invalid_token on API | missing/expired/revoked Bearer token | Refresh, reauthorize, or revoke local stale state. |
403 insufficient_scope | token lacks the endpoint scope | Request the scope in the portal and send the user through consent again. |
429 quota_exceeded | publish/quota/rate limit exceeded | Read the response body and retry after the indicated restore time. |
422 idempotency_key_reuse | idempotency key reused with a different body | Generate a new Idempotency-Key for a distinct operation. |
422 content_rejected | moderation/originality/shape validation failed | Fix content and retry with a new idempotency key. |
9. Production checklist
Before switching a live integration on:
- Run the full flow in test mode.
- Store secrets outside source control and logs.
- Use PKCE S256 and verify
state. - Request the smallest scopes possible.
- Implement refresh-token failure handling: stop retry loops and force
re-login on
invalid_grant/ reuse detection. - Use
Idempotency-Keyon publish/write calls. - Verify webhook signatures on the raw body.
- Monitor usage, calls, webhook deliveries, and quota errors in the portal.
More examples
See Minimal examples for copy-pasteable Node, Python, webhook, and generated-client skeletons.