Skip to main content
The archal CLI hides a small detail that matters when you start sending requests from anything else: twin endpoints live behind the Archal control-plane proxy, which requires two auth headers instead of one. This guide documents the pattern so anyone building a non-CLI integration (Lambda, Cloudflare Worker, Python script, browser-based test, polyglot CI job) can reach the same twin your CLI session is already using.

When you need this

  • You ran archal twin start github and now want to hit the printed URL from something other than the CLI — a Lambda, a test harness, an SDK smoke test, a bash + curl pipeline, etc.
  • You’re self-hosting the Archal control plane and want to verify the proxy from outside the monorepo.
  • You’re writing a fidelity harness that pokes twin REST endpoints directly without loading the full @archal/runtime package.
If your integration is a normal Node.js script that can use @archal/runtime directly, use that instead — it handles both headers for you. This guide is specifically for environments where adding a runtime dependency is not practical.

The two-header pattern

Every request to a twin endpoint must carry:
Authorization: Bearer $ARCHAL_TOKEN
x-archal-upstream-authorization: Bearer <non-empty-upstream-token>
Both are required, and they mean different things:
  • Authorization authenticates your request to the Archal control plane. The token comes from archal login (stored in ~/.archal/credentials.json) or a long-lived token you created from the dashboard. The control plane uses it to verify that the sessionId in the URL belongs to you and that the session has not expired.
  • x-archal-upstream-authorization is what the twin actually sees as Authorization after the control plane forwards your request. The proxy strips the original Authorization header, rewrites x-archal-upstream-authorization into Authorization, and passes the whole thing to the twin worker. Twins that enforce bearer-token auth (like the github twin’s requireGitHubBearerToken) check this new Authorization header for a non-empty token.
The upstream token can be any non-empty string — twins accept it without a token database, because the whole point is to simulate the real service’s behavior without actually contacting the real service. The existing github twin tests use ghp_test_bootstrap_token as the placeholder value.

Example (curl)

Given a running twin session:
archal twin start github
# → Session: env_12345
# → github twin ready: https://api.archal.ai/runtime/env_12345/github/api
You can hit the twin’s REST surface directly:
export ARCHAL_TOKEN=$(cat ~/.archal/credentials.json | jq -r .token)

# Create a repo on the twin — works exactly like real github.
curl -X POST https://api.archal.ai/runtime/env_12345/github/api/user/repos \
  -H "Authorization: Bearer $ARCHAL_TOKEN" \
  -H "x-archal-upstream-authorization: Bearer ghp_test_bootstrap_token" \
  -H "Content-Type: application/json" \
  -d '{"name": "test-repo", "private": false}'
Omit x-archal-upstream-authorization and the twin will return a real github-shaped 401:
{
  "message": "Requires authentication",
  "documentation_url": "https://docs.github.com/rest/overview/resources-in-the-rest-api#authentication",
  "status": "401"
}
Omit the outer Authorization and the control plane returns 401 before the request ever reaches the twin.

Example (Python urllib)

import json
import os
import urllib.request

session_id = "env_12345"
base_url = f"https://api.archal.ai/runtime/{session_id}/github/api"

req = urllib.request.Request(
    f"{base_url}/user/repos",
    method="POST",
    data=json.dumps({"name": "test-repo", "private": False}).encode(),
    headers={
        "Authorization": f"Bearer {os.environ['ARCHAL_TOKEN']}",
        "x-archal-upstream-authorization": "Bearer ghp_test_bootstrap_token",
        "Content-Type": "application/json",
    },
)
response = urllib.request.urlopen(req)
print(json.loads(response.read()))

Example (AWS Lambda / Cloudflare Worker)

export async function handler(event: { body: string }) {
  const sessionId = process.env.ARCHAL_SESSION_ID!;
  const baseUrl = `https://api.archal.ai/runtime/${sessionId}/github/api`;

  const response = await fetch(`${baseUrl}/user/repos`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.ARCHAL_TOKEN}`,
      'x-archal-upstream-authorization': 'Bearer ghp_test_bootstrap_token',
      'Content-Type': 'application/json',
    },
    body: event.body,
    // Always cap twin requests with a timeout. Without it, a hung twin
    // worker keeps the Lambda invocation alive until the platform kills
    // it — burning budget and producing no useful failure signal (#2169).
    signal: AbortSignal.timeout(15000),
  });
  return { statusCode: response.status, body: await response.text() };
}

Header semantics, precisely

The proxy’s header rewrite lives in infra/api/routes/session-events.ts. The order of operations is:
  1. Validate the outer Authorization: Bearer <archal-token> against the caller’s Archal session. Requests with no token, an expired token, or a token belonging to a different user are rejected with 401 or 403 at this stage — the twin never sees them.
  2. Capture the value of x-archal-upstream-authorization (if present).
  3. Copy the request headers forward, stripping a hardcoded list of sensitive headers including x-archal-upstream-authorization itself.
  4. Set the forwarded Authorization header to the captured upstream value (or leave it blank if none was supplied).
  5. Forward the rewritten request to the twin worker.
That means:
  • The twin ALWAYS sees Authorization as whatever you sent in x-archal-upstream-authorization. Your real Archal token never reaches the twin.
  • Sending an Authorization header that’s NOT the Archal token — for example, trying to smuggle a real github PAT through the outer header directly — fails at step 1 because the control plane validates the outer header first. Always use the two-header pattern.

See also