Skip to main content
Foil Gate is a hosted signup service that lets developers create accounts for your product using their coding agents (Claude Code, Cursor, Codex, etc.). Gate handles consent, bot detection, and credential delivery. Your product owns its service metadata in the Gate registry and implements a single provisioning webhook.

How it works

Developer runs:  npx signup yourproduct

CLI creates ephemeral delivery keypair

CLI opens browser → Consent page (Foil-scored)

User clicks Approve → Gate calls your webhook

Your webhook creates account → Returns encrypted env bundle

Gate adds its own encrypted outputs → CLI decrypts locally

CLI writes to .env → CLI sends receipt ack → Gate purges ciphertext

Quick integration

1. Register your service

Services are self-serve. First create a webhook endpoint subscribed to gate.session.approved, then register your service through the dashboard or by POSTing to /v1/gate/services with a secret key that has the gate:services:manage scope. Each organization can own up to 5 Gate services. From the dashboard — go to dashboard.usefoil.com/gate/new, fill in the fields below, and click Create. From the API — send the same fields as JSON:
curl -X POST https://api.usefoil.com/v1/gate/services \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "id": "yourproduct",
    "name": "Your Product",
    "description": "One-line description that appears on the consent screen.",
    "website": "https://yourproduct.com",
    "webhook_endpoint_id": "we_0123456789abcdef0123456789abcdef",
    "docs_url": "https://yourproduct.com/docs",
    "dashboard_login_url": "https://app.yourproduct.com/auth/gate",
    "env_vars": [
      { "name": "Publishable key", "key": "YOURPRODUCT_PUBLISHABLE_KEY", "secret": false },
      { "name": "Secret key",      "key": "YOURPRODUCT_SECRET_KEY",      "secret": true  }
    ],
    "branding": {
      "logo_url": "https://yourproduct.com/logo.svg",
      "primary_color": "#3B7DD8",
      "secondary_color": "#5B9CF5"
    },
    "consent": {
      "terms_url":   "https://yourproduct.com/terms",
      "privacy_url": "https://yourproduct.com/privacy"
    },
    "discoverable": true
  }'
Field rules worth flagging up front:
  • id is the public slug used everywhere, including npx signup <id>. Lowercase, 332 chars, a–z / 0–9 / _ / -, must start and end with a letter or number. Reserved IDs cannot be claimed: gate, registry, services, service, login, sessions, session, tokens, token, webhook, auth.
  • webhook_endpoint_id is required. Create and manage endpoints, subscriptions, signing secrets, test sends, and event history in Webhooks.
  • website is required.
  • env_vars declares the keys your webhook will deliver. The Gate-managed <ID>_GATE_AGENT_TOKEN is added automatically when dashboard_login_url is set — don’t declare it yourself.
  • discoverable: true is required for the service to appear in GET /v1/gate/registry.
See Gate services API for the full field list, including sdks entries, branding.ascii_art, and the PATCH shape for updates.

2. Implement your webhook

Gate calls your webhook when a user approves signup. Your webhook creates the account and returns an encrypted output bundle for the CLI’s public key. Webhook handlers must be idempotent by gate_session_id: retries must return the exact same encrypted bundle instead of creating a second account. Gate uses one mandatory delivery algorithm for every integration:
  • x25519-hkdf-sha256/aes-256-gcm
  • delivery.public_key is a raw X25519 public key encoded as base64url
  • delivery.key_id is the base64url SHA-256 fingerprint of that raw public key
Foil server SDKs expose the same helper APIs for Gate so integrators can:
  • verify the raw-body webhook signature
  • validate the approved webhook payload
  • return plaintext outputs and let the helper build the encrypted response
Here’s the recommended Node/Express shape:
import express from 'express';
import {
  createGateApprovedWebhookResponse,
  parseWebhookEvent,
  validateGateApprovedWebhookPayload,
  verifyGateWebhookSignature,
} from '@abxy/foil-server';

const app = express();

app.use(express.json({
  verify: (req, _res, buf) => {
    (req as express.Request & { rawBody?: string }).rawBody = buf.toString('utf8');
  },
}));

// POST /your-webhook-endpoint
app.post('/v1/gate/webhook', async (req, res) => {
  const rawBody = (req as express.Request & { rawBody?: string }).rawBody;
  const timestamp = req.header('x-foil-timestamp');
  const signature = req.header('x-foil-signature');

  if (!rawBody || !timestamp || !signature || !verifyGateWebhookSignature({
    secret: process.env.FOIL_WEBHOOK_SECRET!,
    timestamp,
    rawBody,
    signature,
  })) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const event = parseWebhookEvent(rawBody);
  if (event.type !== 'gate.session.approved') {
    return res.status(400).json({ error: 'Unexpected event' });
  }
  const payload = validateGateApprovedWebhookPayload(event.data);
  const {
    gate_session_id,
    gate_account_id,
    service_id,
    account_name,
    metadata,
    delivery,
  } = payload;

  const existing = await findProvisioningByGateSession(gate_session_id);
  if (existing) return res.json(existing);

  // 1. Create the account in your system
  const account = await createAccount(account_name);
  const apiKey = await createApiKey(account.id);

  // 2. Build only the outputs owned by your service.
  //    Gate-owned outputs (for example agent tokens) are added separately by Gate.
  //    service_id lets a multi-service webhook branch if needed.
  if (service_id !== 'yourproduct') {
    return res.status(400).json({ error: 'Unknown service' });
  }

  const { encrypted_delivery } = createGateApprovedWebhookResponse({
    delivery,
    outputs: {
      YOURPRODUCT_PUBLISHABLE_KEY: apiKey.publishableKey,
      YOURPRODUCT_SECRET_KEY: apiKey.secretKey,
    },
  });

  // 3. Persist the exact ciphertext for retries keyed by gate_session_id.
  await saveProvisioningResult(gate_session_id, { encrypted_delivery, gate_account_id, metadata });

  res.json({
    encrypted_delivery,
  });
});

3. Gate-managed login token (optional)

If your service sets dashboard_login_url, Gate also delivers a Gate-owned login token alongside your customer env vars. The CLI writes it to .env under a derived key:
  • foilFOIL_GATE_AGENT_TOKEN
  • acme-prodACME_PROD_GATE_AGENT_TOKEN
That token is reserved for npx signup <service> login. It is Gate-managed, is not configured through your service env_vars, and is not exposed in the public registry as editable metadata.

4. Dashboard login (optional)

Gate also supports npx signup yourproduct login, which starts a fresh Gate approval flow for dashboard access. Set dashboard_login_url in your registry entry, then add one route to your dashboard:
import { Foil } from '@abxy/foil-server';

const foil = new Foil({
  secretKey: process.env.FOIL_SECRET_KEY!,
});

// GET /auth/gate?code=...
app.get('/auth/gate', async (req, res) => {
  const code = req.query.code;

  // Verify the short-lived dashboard login token with Gate
  const data = await foil.gate.loginSessions.consume({
    code: String(code),
  });

  // data.gate_account_id — look up the user in your system
  // Create a session however your auth system works
  // Redirect to your dashboard
});

Webhook payload

When a user approves signup, Gate sends:
{
  "id": "wevt_abc123...",
  "object": "webhook_event",
  "type": "gate.session.approved",
  "created": "2026-03-24T20:00:05.000Z",
  "data": {
    "service_id": "foil",
    "gate_session_id": "gate_abc123...",
    "gate_account_id": "gacct_abc123...",
    "account_name": "my-project",
    "metadata": null,
    "delivery": {
      "version": 1,
      "algorithm": "x25519-hkdf-sha256/aes-256-gcm",
      "key_id": "<base64url sha256 of public_key>",
      "public_key": "<base64url raw X25519 public key>"
    },
    "foil": {
      "verdict": "human",
      "score": 0.12
    }
  }
}
FieldDescription
typeAlways gate.session.approved for this event.
data.service_idThe registry service being provisioned. Use it if one webhook handles multiple services.
data.gate_session_idStable idempotency key for this signup attempt. Persist and reuse it on retries.
data.gate_account_idStable identifier for this signup. Use it to link to your internal account.
data.account_nameThe name the developer chose (defaults to their project directory name).
data.metadataOptional key-value pairs passed during session creation.
data.deliveryCLI-provided encrypted delivery metadata. Always encrypt your env map to this key.
data.foil.verdicthuman, bot, or inconclusive.
data.foil.scoreRisk score from 0 (human) to 1 (bot).
The expected output keys come from your registry-owned env_vars configuration. Gate keeps using that schema internally for CLI validation, but ownership stays internal to Gate and is no longer sent on webhook calls or public registry responses.

Webhook response

Your webhook must return:
{
  "encrypted_delivery": {
    "version": 1,
    "algorithm": "x25519-hkdf-sha256/aes-256-gcm",
    "key_id": "<same key_id>",
    "ephemeral_public_key": "<base64url raw X25519 public key>",
    "salt": "<base64url 32 bytes>",
    "iv": "<base64url 12 bytes>",
    "ciphertext": "<base64url>",
    "tag": "<base64url 16 bytes>"
  }
}
When decrypted by the CLI, the ciphertext must contain:
{
  "version": 1,
  "outputs": {
    "FOIL_PUBLISHABLE_KEY": "pk_live_...",
    "FOIL_SECRET_KEY": "sk_live_..."
  }
}
Gate stores only ciphertext. After approval, the CLI polls until it receives the encrypted bundle, decrypts it locally, writes the env vars to .env, and then acknowledges receipt so Gate can purge the stored envelopes. Onboarding links like docs_url are owned by the registry entry for the service, not by the webhook response.

Security

  • Webhook signatures: Every webhook is signed with HMAC-SHA256 over ${timestamp}.${rawBody}. Always verify both X-Foil-Timestamp and X-Foil-Signature against the raw request body.
  • Webhook idempotency: Treat gate_session_id as the provisioning idempotency key. Retries must return the same encrypted delivery bundle.
  • Agent tokens: Gate-owned agt_ tokens. Verified via Gate’s API. Can be revoked instantly.
  • Mandatory encrypted handoff: Gate never stores plaintext integrator credentials. Your webhook returns ciphertext for the CLI’s public key.
  • Ack-based delivery: Approved sessions can safely re-poll the same ciphertext until the CLI acknowledges receipt. Gate purges stored envelopes after a valid ack or after the 24-hour delivery TTL.
  • Foil scoring: Every signup is scored by Foil’s bot detection. Bot verdicts are automatically blocked.
  • Session expiry: Gate sessions expire after 15 minutes.