> ## Documentation Index
> Fetch the complete documentation index at: https://usefoil.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Foil Gate

> Add Foil Gate to your product so developers and their AI coding agents can sign up via npx signup. Register your service and implement one provisioning webhook.

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](https://dashboard.usefoil.com/gate/new), fill in the fields below, and click Create.

**From the API** — send the same fields as JSON:

```bash theme={"dark"}
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, `3`–`32` 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](/api-reference/gate-services) 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:

```typescript theme={"dark"}
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:

* `foil` → `FOIL_GATE_AGENT_TOKEN`
* `acme-prod` → `ACME_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:

```typescript theme={"dark"}
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:

```json theme={"dark"}
{
  "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
    }
  }
}
```

| Field                  | Description                                                                              |
| ---------------------- | ---------------------------------------------------------------------------------------- |
| `type`                 | Always `gate.session.approved` for this event.                                           |
| `data.service_id`      | The registry service being provisioned. Use it if one webhook handles multiple services. |
| `data.gate_session_id` | Stable idempotency key for this signup attempt. Persist and reuse it on retries.         |
| `data.gate_account_id` | Stable identifier for this signup. Use it to link to your internal account.              |
| `data.account_name`    | The name the developer chose (defaults to their project directory name).                 |
| `data.metadata`        | Optional key-value pairs passed during session creation.                                 |
| `data.delivery`        | CLI-provided encrypted delivery metadata. Always encrypt your env map to this key.       |
| `data.foil.verdict`    | `human`, `bot`, or `inconclusive`.                                                       |
| `data.foil.score`      | Risk 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:

```json theme={"dark"}
{
  "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:

```json theme={"dark"}
{
  "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.
