EveryAppEveryApp

Context.dev for the Gateway

Step-by-step guide: a Context.dev key (CONTEXT_API_KEY) on the Gateway for OpenCRM and OpenSEO's rendered audits: the account, the key, the check, rotation.

Context.dev is a hosted web scraping API: it loads a page in a browser, with JavaScript rendering and anti-bot handling included, and returns the result. The Gateway holds one Context.dev API key as CONTEXT_API_KEY and sends it as a Bearer token in the Authorization header, to api.context.dev only, for apps that list capabilities.context (access read). Open CRM requires it (its research agent's brand data, LinkedIn profiles and website extraction); OpenSEO lists it optional: a site audit with Render JavaScript on renders pages through Context, and project setup reads rate-limited Shopify store pages through it. Deployed with everyapp deploy, OpenSEO has no browser of its own (the manifest has no browser resource), so every page a rendered audit loads is one Context credit; only OpenSEO's own Alchemy deploy binds Cloudflare Browser Run and sends Context just the pages that browser can't render. At the end the person has a Context.dev account (the Free plan is enough to start), one key on the Gateway, Open CRM open, and OpenSEO's Render JavaScript toggle enabled.

Settings

NAME required or optional what it holds
CONTEXT_API_KEY required by Open CRM; optional for OpenSEO (audits run without it; the Render JavaScript option says an admin sets it); a Gateway with neither leaves it unset One Context.dev API key, exactly as the dashboard shows it (a key from the agent flow in step 2 begins with ctxt_secret_). The Gateway trims spaces around it and sends it as a Bearer token to api.context.dev only.

Costs and plans

As of October 2026 (context.dev/pricing and docs.context.dev/account):

  • Free: $0, no credit card, 1,000 credits a month (refilled monthly), 2 requests in flight at a time (pricing page and API reference; the billing docs' plan table says 1; the dashboard shows the organization's effective limit), 2 API keys per organization.
  • Developer $19/month for 7,500 credits (10 in flight); Pro $99 for 125,000; Growth $299 for 500,000; Scale $499 for 1,000,000. Paid plans turn on Auto-Topup at checkout: below 1,000 credits it adds 1,000-credit blocks at the plan's rate ($2.20 per 1,000 on Developer).
  • A basic scrape is 1 credit; OpenSEO counts 1 credit per rendered page (an audit with rendering stops at 1,000 pages). Failed requests (401, 403, 408, 429, 5xx, a site that blocks Context) are not billed; a page that answers 404 is.
  • The usage call (the smoke check) costs 0 credits; 60 calls a minute.
  • The limit that matters is concurrency: on Free, 1-2 renders run at once, so a large rendered audit is slow or sees 429s. On Free, a run of failed requests can also get every key in the organization disabled (401 DISABLED, below), not only slow the audit.

Only a person can

  • Sign up or sign in at context.dev and accept its Terms of Service and Privacy Policy.
  • Choose a paid plan or add a card (the Free plan needs neither); in the Context organization only an Owner or Admin can.
  • Create the key in the dashboard and copy it; or open the setup link and enter the code in the agent flow (step 2).
  • Type or export the key (the setup rules: the three ways). It never goes through the chat.

Before you start

  • pnpm everyapp secret list crm --json (Open CRM) or pnpm everyapp secret list openseo --json, from any directory: a CONTEXT_API_KEY entry with holder gateway and setup setup/context.md means this guide applies (in the checkout: landing-page/content/agent/setup/context.md). For Open CRM it is in data.app.setup.unmet (names ["CONTEXT_API_KEY"], need.kind required) and the app doesn't open until it is set: the Gateway lists it as needing setup, sends a browser opening https://crm.<app base domain> to its setup dialog and answers anything else 503 app_needs_setup; data.app.setup.command is pnpm everyapp configure crm, which asks for every missing setting (the OpenRouter key among them while that is missing; data.next repeats it), data.gateway.needed has { "name": "CONTEXT_API_KEY", "required": true, "set": false }, data.gateway.missing names it, and status (under "crm needs setup") and secret list crm (under the unmet setting) print Guide: https://everyapp.com/docs/agent/setup/context.md: that is how an agent lands here. For OpenSEO it is in data.app.setup.settings with need.kind optional and set: false: the app opens, missing and the setup gate never name it (pnpm everyapp configure openseo offers it after the required ones), and its Render JavaScript toggle says an admin sets it. The entry with "set": true means it is set already: ask before replacing it (a value can't be read back, so "replace" means a new key). APP_NOT_FOUND ("<app> isn't deployed to this Gateway yet"): that app isn't deployed; set the key anyway (pnpm everyapp secret list --json from the every-app checkout then shows it in data.gateway.names; in the app's directory the same command reads its everyapp.config.ts and works before the first deploy too, as does pnpm everyapp configure there) and run the app-level check after that deploy.
  • pnpm everyapp status --json: data.installed and data.claimed are true; data.gatewayUrl gives <gateway host>, data.appBaseDomain the <app base domain>. The entry in data.apps with "id": "crm" (or "openseo"), "status": "active" and "bound": true carries the same setup; data.fixes has APP_NEEDS_SETUP for crm (severity warning, headline "crm needs setup", run the same command) while a required setting is missing, never for OpenSEO on this key's account.
  • Ask whether the person already has a Context.dev account or key (a password manager, a variable in their shell). An existing key skips steps 2 and 3. Context allows one Free organization per person: with an account already, they sign in to it (and the agent flow must use that account's email).
  • Ask how many pages a month they expect to render. More than 1,000 means a paid plan: their decision, never yours.

Steps

  1. You: Run the two commands above. Tell the person in one line what they get (Open CRM's research agent: brand data, LinkedIn profiles and website extraction; rendered site audits in OpenSEO) and what it costs (free up to 1,000 rendered pages a month), ask the two questions in Before you start, and stop: their answers pick the path (an existing key: step 4 alone; an account: sign in, not up; the agent flow in step 2 only on their say-so). Then steps 2-4 go in one message, in order; wait once, after step 4.

  2. Person: Sign up, or sign in. Page: https://www.context.dev/signup. Type their email in Email and click Continue (an email sign-up sets a password and asks them to verify the address by email), or pick Google or GitHub under "or continue with" (no verification step). Continuing accepts the Terms of Service and Privacy Policy. An existing account signs in from "Already have an account? Sign in" (https://context.dev/login). It worked when the dashboard opens.

    Alternative, only with their say-so (it registers a key in their name): Context.dev's agent flow (context.dev/auth.md, read today, not run). Everything in one shell, with EMAIL set to the address they gave (an existing account's email, when they have one). Register: reg=$(jq -n --arg email "$EMAIL" '{type:"service_auth",login_hint:$email,api_key_name:"EveryApp Gateway",client_name:"everyapp setup"}' | curl -sS -X POST https://www.context.dev/agent/identity -H 'Content-Type: application/json' -d @-); printf '%s' "$reg" | jq '{claim, offer}' Show them claim.verification_uri and claim.user_code in full, and that both expire in claim.expires_in seconds (600: ten minutes). If the response has offer, say it includes 1,000 extra credits (deadline offer.claim_by); promise nothing otherwise. A 429 rate_limit_exceeded from the registration means stop, no retry: step 3 instead. They open the link, enter the 6-digit code, and sign up or sign in with that email (a password sign-up also verifies the address). Then poll every claim.interval seconds, for ten minutes at most, until the key arrives or an error ends it, and pipe the key straight in (the setup rules' "a command produced it"); claim_token is read from $reg in the same shell, never typed (it is short-lived and not the key): CLAIM=$(printf '%s' "$reg" | jq -r .claim_token); INTERVAL=$(printf '%s' "$reg" | jq -r '.claim.interval // 5'); for i in $(seq 1 120); do r=$(curl -sS -X POST https://www.context.dev/oauth2/token -d "grant_type=urn:workos:agent-auth:grant-type:claim&claim_token=$CLAIM"); e=$(printf '%s' "$r" | jq -r '.error // empty'); case "$e" in '') break;; authorization_pending) ;; slow_down) sleep 10;; *) printf '%s' "$r" | jq '{error, error_description}'; break;; esac; sleep "$INTERVAL"; done; printf '%s' "$r" | jq -e .access_token >/dev/null && printf '%s' "$r" | jq -r .access_token | pnpm everyapp secret set CONTEXT_API_KEY --json authorization_pending means they haven't finished (email_verification_required: the address isn't verified yet); expired_token means the ten minutes passed: register again, only with their agreement; invalid_grant means stop and show its error_description. When the loop ends without a key, nothing is set and the printed error says why (none printed: curl itself failed). Never print $r: the key is in it. This replaces steps 3 and 4: go to Verify (nobody but the Gateway holds the key now, so its smoke check is skipped; 1 and 3 there are the check).

  3. Person: Create the key. Page: https://context.dev/dashboard/api-keys. Create a new key with the page's button (its label isn't in the docs); if the dialog shows the key once, copy it before closing. Keep Full access (the default; it covers the Data APIs the apps use and the usage read the smoke check uses), or choose Restricted with Data APIs = Use and "Account logs, usage, and browser profiles" = Read. Name it "EveryApp Gateway" when the form asks (renaming later needs a paid plan). It worked when the key is listed on that page. A Free organization holds at most two keys: with two there already, delete one they no longer use (whether a disabled key still counts toward the two isn't in the docs), or use the agent flow above, which needs no button.

  4. Person, then You: Put it on the Gateway. The setup rules' first way (the key is on a screen they are looking at): they open a second terminal in the every-app folder and run pnpm everyapp configure crm (or pnpm everyapp configure openseo; no --json: with it nothing prompts), which asks for this key among the app's other unset settings (for OpenSEO, which lists context in an optional feature, configure names pnpm everyapp provider add context under that feature instead of asking; once an app is ready, configure asks nothing, so a key already set changes with secret set CONTEXT_API_KEY), or pnpm everyapp secret set CONTEXT_API_KEY for just this one. The prompt is hidden (CONTEXT_API_KEY, under lines saying what it is for and where it comes from; secret set asks Value for CONTEXT_API_KEY); they paste, press Enter, and see Set CONTEXT_API_KEY on the Gateway (configure then says crm is ready. or what the app still needs, and Live now. No deploy needed.; secret set ends Live now on <gateway host>. No deploy needed.). Nothing is exported afterwards, so offer the smoke check (Verify 2, 0 credits) in that same terminal: the hidden read (the setup rules' third way: read -rs CONTEXT_API_KEY && export CONTEXT_API_KEY, paste, Enter), Verify 2's curl, they tell you its last line (200) and credits_remaining, then unset CONTEXT_API_KEY; declined, the app (Verify 3) is the check. The setup rules' third way from the start, when they read it in hidden instead (never a typed export …=, which the shell history keeps; run the smoke check below first): pnpm everyapp configure <app id> --from-env --json, whose done event has data.set ["CONTEXT_API_KEY"] and data.changed.gateway.names, or pnpm everyapp secret set CONTEXT_API_KEY --from-env CONTEXT_API_KEY --json, whose done event has data.gateway.names ["CONTEXT_API_KEY"]; then unset CONTEXT_API_KEY. Never on a command line, never in the chat (a key seen in the chat gets rotated: step 3 again). Wait for "done".

  5. You: Verify, below. Neither app needs a restart or redeploy: the Gateway reads the new value on its next request.

Verify

  1. pnpm everyapp secret list crm --json or pnpm everyapp secret list openseo --json, from anywhere: the CONTEXT_API_KEY entry in data.app.setup.settings has "set": true; for Open CRM it has also left data.app.setup.unmet, and ready is true once nothing else is missing (its OpenRouter key counts too; until then command and data.next name only what is left); for OpenSEO it was never in unmet (optional). In the older fields, data.gateway.names lists it, its data.gateway.needed entry has "set": true and it has left data.gateway.missing (before the app is deployed, the root pnpm everyapp secret list --json shows data.gateway.names and that is the check). Then pnpm everyapp status --json: the crm entry's setup.ready is true and data.fixes has no APP_NEEDS_SETUP for it, once everything else is set.
  2. Smoke check, only while the key is exported in the person's terminal (the setup rules' third way, before unset; or, after the hidden prompt, when they read it in hidden there for this check). After the agent flow nobody but the Gateway holds the key: skip to 3, which with 1 is the check. 0 credits; --config - keeps the key out of the process list: printf 'header = "Authorization: Bearer %s"\n' "$CONTEXT_API_KEY" | curl -sS --config - -w '\n%{http_code}\n' https://api.context.dev/v1/org/usage Expected: 200 on the last line, and JSON with credits_remaining (1,000 on a fresh Free plan; 2,000 when the agent flow's gift applied) and request_id, always present; period.credits_used and next_refill.at (null on a paused account); key_metadata.credits_consumed, when present, 0. The body's error_code is what the If it fails table keys on: 401 NOT_FOUND ("API key not found") or UNAUTHORIZED is the key, not the path, and an empty or unset variable gives the same 401; USAGE_EXCEEDED (402, or 401 on older deployments) is credits; 401 DISABLED a disabled key; 403 INSUFFICIENT_PERMISSIONS a Restricted key without the usage read (OpenSEO still works when Data APIs is Use). The Gateway's proxy answers only over an app's binding, so no curl goes through it.
  3. The app. Open CRM: https://crm.<app base domain> opens instead of the setup dialog at https://<gateway host>/?setup=crm (that dialog means a required setting is still unset: Verify 1 says which), and its research agent reads brand data, LinkedIn profiles and website pages through Context. OpenSEO: open https://openseo.<app base domain> (a browser sent to https://<gateway host>/?setup=openseo instead means another required setting is unset: dataforseo.md, openrouter.md), a project, then Site Audit (the URL is /p/, the project's id, /audit). The Render JavaScript toggle is enabled, with no "needs a Context.dev API key" note (reload if the page was open). Start a small audit of a JavaScript-rendered site (a React or other single-page app) with it on; the audit lists the pages with their content. Each rendered page spends 1 credit: run the usage call again and period.credits_used went up, or open the key's usage link on the API keys page. For Open CRM pnpm everyapp status --json checks this setting too: its APP_NEEDS_SETUP for crm goes once every required setting is set (Verify 1); OpenSEO's never names it.
  4. Logs: the Gateway logs every proxied call as integration.proxy.call with the app, path and status, and OpenSEO's own Worker logs site_audit:render with provider: "context", httpStatus, creditsUsed and outcome: the status only, never the body, so a 401's error_code comes from the smoke check, or from the key's usage link on the API keys page (its recent usage and request logs; whether those show error_code: unverified). Cloudflare dashboard, the setup rules' path: Workers & Pages → every-app-gateway → Observability (Logs on older accounts) → Live for a stream, or the stored Workers Logs on the same page (the Gateway's config turns them on). cf has no terminal tail: cf o11y telemetry query --help reads Workers Logs (read-only; filter on the Gateway's script) and cf o11y telemetry live-tail only prepares a websocket, so the dashboard is the simpler path; npx wrangler tail every-app-gateway streams them live (wrangler isn't in the checkout: unverified).

If it fails

error or symptom meaning what to do
A browser opening the app is sent to https://<gateway host>/?setup=<app id>; any other request gets 503 with the body {"error":"app_needs_setup","app":"<app id>"} The Gateway's setup gate: a required setting is still unset (for OpenSEO the DataForSEO or OpenRouter key, never this one; for Open CRM, at /?setup=crm, this key or OpenRouter's) data.app.setup.command, pnpm everyapp configure <app id> (step 4), then Verify 1
integration_not_configured (503, header x-everyapp-error); OpenSEO's toggle disabled with "needs a Context.dev API key"; AUDIT_RENDERING_UNAVAILABLE The Gateway holds no CONTEXT_API_KEY (OpenSEO, which lists context in an optional feature, opens and shows these; Open CRM, which requires it, stays behind the setup gate instead: the row above) Step 4; check the name with secret list --json; reload OpenSEO (no restart: that note is for Docker)
capability_not_requested (403) The deployed app's manifest doesn't list context Deploy a current OpenSEO or Open CRM
integration_method_not_allowed (405), invalid_integration_route (400) A method other than GET or POST, or a host other than api.context.dev An app bug, not a key problem
integration_upstream_failed (502) api.context.dev unreachable from the Gateway Retry later
SECRET_VALUE_REQUIRED secret set --json ran with nothing piped and no terminal to ask in One of the three ways (step 4)
SECRET_VALUE_REQUIRED from pnpm everyapp configure <app id> --json Nothing was piped (with --from-env, stdin isn't read), no variable of a missing name was found, and there is no terminal to ask in; the message names what the app still needs (for OpenSEO this key is optional, so it is never among them) The person's terminal (step 4), NAME=value lines on stdin, or the hidden read then its data.rerun, pnpm everyapp configure <app id> --from-env (which takes this key's variable too, as an unset optional setting)
SECRET_NOT_DECLARED from pnpm everyapp configure <app id> --json A piped NAME=value line whose name the app's manifest doesn't declare (a typo of CONTEXT_API_KEY; the name is echoed only when it looks like one, else line <n> (not shown: it may be a value)); nothing was written The name is exactly CONTEXT_API_KEY: pipe the lines again (a bare value goes to pnpm everyapp secret set CONTEXT_API_KEY, not to configure; a line that isn't NAME=value at all is PIPED_LINE_INVALID)
SECRET_SEVERAL_NAMES --from-env or a pipe with several names One name per call
SECRET_EMPTY The --from-env variable is empty or unset They export it first, in the terminal that runs the command
SECRET_NOT_DECLARED A typo (context_api_key, CONTEXTAPIKEY, another unknown name): data.didYouMean names the close one The name is exactly CONTEXT_API_KEY: rerun with it and the same value source (the pipe, or --from-env); data.rerun carries only the names, so run as is it stops with SECRET_VALUE_REQUIRED
SECRET_NOT_SETTABLE One of setup's own names (GATEWAY_URL, BETTER_AUTH_SECRET, …), never this one Check the command: it named another setting
SECRET_NAME_INVALID A character other than letters, digits and underscore The name is exactly CONTEXT_API_KEY
CONFIRMATION_REQUIRED secret unset --json without --yes: no terminal to confirm in Rerun with --yes only when the person asked
Provider 401 with error_code NOT_FOUND ("API key not found") or UNAUTHORIZED (the smoke check; the Gateway's log shows the 401 status only) Key wrong, pasted incompletely, or an empty variable in the smoke check; not the path New key (step 3), set again (step 4)
Provider 401 DISABLED Context disabled the key; on Free, every key in the organization after repeated request errors (a rendered audit of an erroring site can do it) Stop the audit; read the account email; inspect the key on https://context.dev/dashboard/api-keys and re-enable it once what was failing is fixed, or write support@context.dev. A new key alone doesn't fix the cause
Provider USAGE_EXCEEDED (402; 401 on older deployments) Credits used up; the request isn't run or charged Wait for error_data.next_refill_at, or the person upgrades or tops up at https://context.dev/dashboard/subscription (the URL the 402 body gives; a first paid plan from the pricing page's plan buttons). Only an Owner or Admin of the Context organization can; the billing docs say Auto-Topup, pauses and cancellation may need a message to Context
Provider 403 INSUFFICIENT_PERMISSIONS A Restricted key without Data APIs = Use (or without the usage read: smoke check only) A Full access key
Provider 429 with Retry-After Concurrency limit (Free: 1-2 in flight), a per-minute limit, or authentication protection after repeated calls with a bad key Not billed; smaller audits, or a higher plan; with a bad key, fix the key, don't retry. OpenSEO never retries a paid call by itself
Provider 408, or output REQUEST_TIMEOUT The page didn't load within the 30 s OpenSEO allows The site's speed, not the key; the audit goes on and logs outcome: "error" for that page
Output WEBSITE_BLOCKED The site served an anti-bot wall to Context too Not billed; nothing to fix on the Gateway
action_required cloudflare_login (waiting: true) from any command No Cloudflare login, or it expired; the command logs in by itself Show data.url and data.code to the person and keep reading; exit 2 means nobody approved in time: run the command again (pnpm everyapp login --json only to log in ahead of time)

Rerun, rotate, remove

  • Rerun: every step is safe to repeat. configure and secret set replace the value in one new Gateway version; the dashboard shows the keys that exist (Free: two at most, so delete an unused one before creating a third). After an interruption start at Verify and continue at the first step that isn't done.
  • Rotate: the person creates a replacement key (step 3), sets it (step 4; live at once, no deploy), you verify, then they disable the old key from its action menu on https://context.dev/dashboard/api-keys. Rotate too when a teammate who copied the key leaves, or when a key was ever pasted in a chat.
  • Remove: pnpm everyapp secret unset CONTEXT_API_KEY --json --yes, and --yes only when the person asked (with --json and no --yes there is no terminal to confirm in: CONFIRMATION_REQUIRED). Its done event's data.gateway.names lists the removed name; a name the Gateway doesn't hold ends done with data.removed: false and "isn't set on the Gateway. Nothing changed." Then they disable or delete the key in the dashboard: a key the Gateway no longer holds is still a key. Open CRM then needs setup again: the Gateway closes it until the key is back (status says so, with this guide's path). OpenSEO keeps opening, with the Render JavaScript toggle back to disabled and audits without rendering working.

Sources

On this page