> ## 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.

# Signup & account creation

> Stop fake signups and automated account creation. Verify Foil sealed tokens at registration and block bots and AI agents before you create the account.

<Info>
  Signup is the highest-leverage place to run Foil. Every downstream abuse pattern — spam, scraping, ratings manipulation, trial farming — starts with a bot-created account. A hard block here is cheaper than cleaning up later.
</Info>

## The threat

Automated signup is the most common and most costly bot surface. Attackers create accounts in bulk to seed spam networks, farm free-trial credits, inflate referral payouts, reserve usernames, drain rate limits, or build a corpus of "aged" accounts for later sale. Because account creation is only occasional for legitimate users, you can afford a stricter policy here than on, say, a login page that a human might hit dozens of times a week.

Foil fits the shape of this problem well: you want a strong verdict *before* the row is written. The browser client streams observations the whole time a user is filling out the form, and `getSession()` flushes everything into a sealed handoff right at submit. Your backend verifies the token, checks the verdict, and decides whether to proceed — all before you touch the database.

Two detection categories matter most on this surface:

* **`automation`** — Playwright, Puppeteer, Selenium, Patchright, and related stealth tooling driving a headless browser.
* **`ai-agent`** — LLM-powered agents (browser-use, computer-use, etc.) doing end-to-end signup through a controlled browser.

Foil detects both. On a signup page, it is reasonable to hard-block either.

## The flow

<Steps>
  <Step title="Start Foil on page load">
    The browser client begins collection immediately so a full fingerprint is available by the time the user submits.
  </Step>

  <Step title="Wait for fingerprint readiness before enabling submit">
    `waitForFingerprint()` resolves when server-side fingerprinting has frozen. This is the safe moment to allow submission.
  </Step>

  <Step title="Request a sealed handoff at submit">
    `getSession()` flushes the latest observations and returns `{ sessionId, sealedToken }`.
  </Step>

  <Step title="Verify on the backend">
    Use `safeVerifyFoilToken()` with your secret key — local, no extra network call.
  </Step>

  <Step title="Apply policy">
    Hard-block on `bot`, step up on `inconclusive`, proceed on `human`.
  </Step>
</Steps>

## Client integration

Start Foil early and gate submission on fingerprint readiness. You can show the form the whole time — just disable the submit button until `waitForFingerprint()` has resolved.

```html theme={"dark"}
<script type="module">
  const foilPromise = import("https://cdn.usefoil.com/t.js").then(
    (Foil) =>
      Foil.start({
        publishableKey: "pk_live_your_publishable_key",
      }),
  );

  const submitButton = document.querySelector("#signup-submit");

  foilPromise.then(async (foil) => {
    foil.onError((error) => {
      console.error("Foil error", error.code, error.message);
    });

    await foil.waitForFingerprint();
    submitButton.disabled = false;
  });

  async function submitSignup(formData) {
    const foil = await foilPromise;
    const { sessionId, sealedToken } = await foil.getSession();

    return fetch("/api/signup", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        ...formData,
        foil: { sessionId, sealedToken },
      }),
    });
  }
</script>
```

If your signup form is complex enough that `waitForFingerprint()` might not have resolved by the time a fast user clicks submit, see [Checkout & payment](/use-cases/checkout) for the fallback pattern.

## Server verification

<CodeGroup>
  ```javascript Node.js theme={"dark"}
  const { safeVerifyFoilToken } = require("@abxy/foil-server");

  app.post("/api/signup", async (req, res) => {
    const result = safeVerifyFoilToken(
      req.body.foil.sealedToken,
      process.env.FOIL_SECRET_KEY,
    );

    if (!result.ok) {
      // Fail closed on signup — no token, no account.
      return res.status(403).json({ error: "Verification failed" });
    }

    const { decision, attribution } = result.data;

    if (decision.verdict === "bot") {
      return res.status(403).json({ error: "Signup blocked" });
    }

    if (decision.verdict === "inconclusive") {
      // Hold the signup and require email verification before activating
      await createAccount({ ...req.body, status: "pending_verification" });
      await sendVerificationEmail(req.body.email);
      return res.json({ status: "verify_email" });
    }

    await createAccount(req.body);
    res.json({ status: "created" });
  });
  ```

  ```python Python theme={"dark"}
  from foil_server import safe_verify_foil_token
  import os

  @app.post("/api/signup")
  def signup(request):
      result = safe_verify_foil_token(
          request.json["foil"]["sealedToken"],
          os.environ["FOIL_SECRET_KEY"],
      )

      if not result.ok:
          return {"error": "Verification failed"}, 403

      verdict = result.data.decision.verdict

      if verdict == "bot":
          return {"error": "Signup blocked"}, 403

      if verdict == "inconclusive":
          create_account(request.json, status="pending_verification")
          send_verification_email(request.json["email"])
          return {"status": "verify_email"}

      create_account(request.json)
      return {"status": "created"}
  ```

  ```go Go theme={"dark"}
  import foil "github.com/abxy-labs/foil-server-go"

  func signupHandler(w http.ResponseWriter, r *http.Request) {
      var body struct {
          Email    string `json:"email"`
          Foil struct {
              SealedToken string `json:"sealedToken"`
          } `json:"foil"`
      }
      json.NewDecoder(r.Body).Decode(&body)

      result := foil.SafeVerifyFoilToken(
          body.Foil.SealedToken,
          os.Getenv("FOIL_SECRET_KEY"),
      )
      if !result.OK {
          http.Error(w, "Verification failed", 403)
          return
      }

      switch result.Data.Decision.Verdict {
      case "bot":
          http.Error(w, "Signup blocked", 403)
          return
      case "inconclusive":
          createPendingAccount(body)
          sendVerificationEmail(body.Email)
          json.NewEncoder(w).Encode(map[string]string{"status": "verify_email"})
          return
      }

      createAccount(body)
      json.NewEncoder(w).Encode(map[string]string{"status": "created"})
  }
  ```

  ```ruby Ruby theme={"dark"}
  require "foil/server"

  post "/api/signup" do
    body = JSON.parse(request.body.read)
    result = Foil::Server::SealedToken.safe_verify_foil_token(
      body.dig("foil", "sealedToken"),
    )
    halt 403, { error: "Verification failed" }.to_json unless result[:ok]

    case result[:data][:decision][:verdict]
    when "bot"
      halt 403, { error: "Signup blocked" }.to_json
    when "inconclusive"
      create_pending_account(body)
      send_verification_email(body["email"])
      { status: "verify_email" }.to_json
    else
      create_account(body)
      { status: "created" }.to_json
    end
  end
  ```

  ```php PHP theme={"dark"}
  use Foil\Server\SealedToken;

  $body = json_decode(file_get_contents("php://input"), true);
  $result = SealedToken::safeVerify(
      $body["foil"]["sealedToken"],
      getenv("FOIL_SECRET_KEY"),
  );

  if (!$result->ok) {
      http_response_code(403);
      echo json_encode(["error" => "Verification failed"]);
      exit;
  }

  $verdict = $result->data->decision["verdict"];

  if ($verdict === "bot") {
      http_response_code(403);
      echo json_encode(["error" => "Signup blocked"]);
      exit;
  }

  if ($verdict === "inconclusive") {
      createPendingAccount($body);
      sendVerificationEmail($body["email"]);
      echo json_encode(["status" => "verify_email"]);
      exit;
  }

  createAccount($body);
  echo json_encode(["status" => "created"]);
  ```

  ```bash cURL theme={"dark"}
  # Fallback path: fetch the durable session instead of verifying locally
  curl https://api.usefoil.com/v1/sessions/sid_... \
    -H "Authorization: Bearer sk_live_..."
  ```
</CodeGroup>

## Decisioning policy

| Verdict        | Recommended action on signup                                                                                        |
| -------------- | ------------------------------------------------------------------------------------------------------------------- |
| `human`        | Create the account.                                                                                                 |
| `inconclusive` | Create the account in a `pending_verification` state and require email (or phone) confirmation before unlocking it. |
| `bot`          | Return a generic error. Do not create the account.                                                                  |

Two policy choices worth making deliberately:

* **Fail closed when verification fails.** On a login page you might fail open (see [Login & credential stuffing](/use-cases/login)). On signup, returning an error is safe — a real user can refresh and retry, and you've prevented any attacker from bypassing Foil by sending a malformed token.
* **Do not leak detection to the client.** Return the same message for `bot` and a generic server error. The browser client never receives verdicts directly — keeping them invisible at the response layer too means attackers get no signal to iterate against.

If you run [Going to production](/going-to-production)'s report-only mode on signup, watch `decision.verdict === "bot"` and the `attribution.bot.facets.category.value` field (values: `automation`, `ai-agent`, `crawler`, `unknown`) to understand what you'd be blocking before you turn enforcement on.

## Gate: the opinionated alternative

If you'd rather not build the signup form, email verification, and bot policy yourself, **Gate** is Foil's passwordless signup flow with the detection stack built in. Your service registers once with a webhook URL, and Gate handles the UI, consent, Foil verification, and approver handoff on your behalf.

Choose Gate when:

* You're building a developer tool or API product where every user needs an account, but you don't want to own the signup UI.
* You want account creation to be agent-aware by default — Gate issues short-lived agent tokens alongside human logins.
* You'd rather receive a signed webhook than run your own form + verification pipeline.

Choose the integration on this page when:

* You already have a signup form and just want to stop bots from submitting it.
* You need fine-grained control over the `inconclusive` → email verification handoff.
* You're on a platform (native mobile, embedded, etc.) where Gate's hosted flow doesn't fit.

See [What is Gate](/gate/what-is-gate) for the full flow.

## What's next

<CardGroup cols={2}>
  <Card title="Server verification" icon="shield-check" href="/server-verification">
    Reference for the underlying verification primitive.
  </Card>

  <Card title="Going to production" icon="rocket" href="/going-to-production">
    Rollout plan — report-only, soft challenge, hard enforcement.
  </Card>

  <Card title="Login protection" icon="key-round" href="/use-cases/login">
    Protect the login form against credential stuffing.
  </Card>

  <Card title="What is Gate" icon="door-open" href="/gate/what-is-gate">
    Opinionated passwordless signup built on Foil.
  </Card>
</CardGroup>
