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

# Register a new account

> Have Sente create an account at an app whose terms permit it — driving the signup form, completing email verification from the account's own inbox, and vaulting the credentials. Includes what reliably does not work.

Register creates a **new** account at a third-party app. Sente drives a real browser through the
signup form, completes email verification from the account's own inbox — the code or link is
extracted server-side and applied inside the run, you never touch it — and leaves you with a durable
account: credentials in the vault, session persisted, ready to re-use.

If the account can exist already, [Connect](/accounts/connect) it instead. Connect is the safer
path — no terms question, no fraud wall, no CAPTCHA at the door. Register is for the case where
there is genuinely no account yet and the target's terms allow one to be created this way.

<Warning>
  Register only where the target's terms of service permit automated account creation. Where the terms
  require a person to form the agreement, use `confirmBeforeSubmit` so a human clicks the final submit
  themselves — and where automated form-filling is prohibited outright, don't point a run at the site
  at all. See [Acceptable use](/trust/acceptable-use).
</Warning>

## Read this before you pick a target

Automated signup does not work everywhere, and pretending otherwise wastes your time:

* **Phone-verified signups cannot complete.** Sente identities are email-only. A site that demands
  an SMS code at signup — `x.com` is the canonical example — blocks with `PHONE_REQUIRED` and waits
  for a human to supply a number in the live view. If nobody does, the run fails after the hold.
  Check for a phone step before you aim at a site.
* **CAPTCHAs stop the run.** Sente does not complete CAPTCHAs. The run blocks with
  `CAPTCHA_REQUIRED` and a person solves it in the interactive live view, then resumes. That is the
  design, not a gap.
* **Fraud and abuse walls are real.** Some apps silently reject or shadow-block signups they judge
  automated — sometimes before any verification email is sent. Those runs end `failed`. Sente cannot
  make a site accept a signup it has decided to refuse.
* **Payment walls stop the run.** `PAYMENT_REQUIRED`; Sente never enters payment details.

The scope that works well: apps where signup is a form plus an emailed code or link, and whose terms
permit it.

<Note>
  A failed run does **not** consume your run quota — only non-failed runs count against the daily and
  monthly caps. It does consume time and browser minutes, which is reason enough to check the target
  first.
</Note>

## Register an account

`POST /v1/registrations` with the signup URL. `identityId` is optional — omit it and the account's
email address is provisioned from the app's hostname (`https://example.com/signup` →
`example@sente.run`). Credentials are optional too: supply a username and password, or let Sente
derive a username and generate a strong password.

<CodeGroup>
  ```bash CLI theme={null}
  sente register https://app.example.com/signup --autonomous
  # Run it in a terminal without --autonomous / --confirm-before-submit and it asks which submit mode
  # you want. In scripts and CI always pass one explicitly (non-TTY defaults to autonomous).
  #
  # The CLI waits for the run and exits non-zero unless it completed; a blocked run prints its
  # live-view URL and `sente run resume <runId>`.

  sente credentials <registrationId>   # → the vaulted username / password / origin
  ```

  ```ts TypeScript theme={null}
  import { Sente } from "@sente-labs/sdk";

  const sente = new Sente({ apiKey: process.env.SENTE_API_TOKEN! });

  // No identityId — the account's email address is provisioned from the app's hostname.
  const { registration, run } = await sente.registrations.register({
    appUrl: "https://app.example.com/signup",
    // credentials: { username: "acme-bot" },   // optional; password is generated if omitted
  });

  const settled = await sente.runs.waitForRun(run.id);   // timeout in ms, default 180000
  if (settled.status === "blocked") {
    // A person clears the one step at settled.liveViewUrl, then:
    //   await sente.runs.resume(run.id);
    throw new Error(`${settled.error?.code}: a human is needed at ${settled.liveViewUrl}`);
  }
  if (settled.status !== "completed") throw new Error(`register failed: ${settled.error?.code}`);

  const creds = await sente.registrations.getCredentials(registration.id);
  // { username, password, origin: "generated" | "supplied" }
  ```

  ```python Python theme={null}
  import os
  from sente import Sente

  sente = Sente(api_key=os.environ["SENTE_API_TOKEN"])

  # No identity_id — the account's email address is provisioned from the app's hostname.
  registration, run = sente.registrations.register(
      app_url="https://app.example.com/signup",
      # credentials={"username": "acme-bot"},   # optional; password is generated if omitted
  )

  settled = sente.runs.wait_for_run(run.id)              # timeout in SECONDS, default 180
  if settled.status == "blocked":
      # A person clears the one step at settled.live_view_url, then:
      #   sente.runs.resume(run.id)
      raise RuntimeError(f"{settled.error['code']}: a human is needed at {settled.live_view_url}")
  if settled.status != "completed":
      raise RuntimeError(f"register failed: {settled.error}")

  creds = sente.registrations.get_credentials(registration.id)
  # { "username": ..., "password": ..., "origin": "generated" | "supplied" }
  ```
</CodeGroup>

The generated password is 20 characters, mixed classes, with ambiguous glyphs (`0`/`O`, `1`/`l`/`I`)
excluded so it survives copy-paste into a signup form. Either way it is vaulted AES-256-GCM and
readable back through `getCredentials` — created accounts only, and every read is written to the
[audit trail](/events/audit-trail).

<Note>
  Don't call `waitForOtp` around a registration. Verification is completed **inside** the run: the
  inbound email is classified on arrival, the code or link is extracted, and the run controller applies
  it. Those helpers are for signups your own code drives — see
  [Verification codes](/email/verification-codes).
</Note>

## Confirm-before-submit

Pass `confirmBeforeSubmit: true` and the agent fills every field and ticks the terms checkboxes, then
**deliberately stops without pressing the final button**. The run blocks with
`SUBMIT_CONFIRMATION_REQUIRED` and a `liveViewUrl`. A person opens it, reviews the filled form,
clicks submit themselves — so a human, not an agent, forms the agreement with the site — and resumes
the run. Sente then finishes email verification as usual.

<CodeGroup>
  ```bash CLI theme={null}
  sente register https://app.example.com/signup --confirm-before-submit
  # → blocked (SUBMIT_CONFIRMATION_REQUIRED)
  #   live view: https://…      ← open it, click submit
  #   resume:    sente run resume <runId>
  sente run resume <runId>
  ```

  ```ts TypeScript theme={null}
  const { run } = await sente.registrations.register({
    appUrl: "https://app.example.com/signup",
    confirmBeforeSubmit: true,
  });

  const settled = await sente.runs.waitForRun(run.id);
  if (settled.status === "blocked" && settled.error?.code === "SUBMIT_CONFIRMATION_REQUIRED") {
    console.log("click submit here:", settled.liveViewUrl);
    // …after the human has clicked it:
    await sente.runs.resume(run.id);
    const done = await sente.runs.waitForRun(run.id);
  }
  ```

  ```python Python theme={null}
  registration, run = sente.registrations.register(
      app_url="https://app.example.com/signup",
      confirm_before_submit=True,
  )

  settled = sente.runs.wait_for_run(run.id)
  if settled.status == "blocked" and settled.error["code"] == "SUBMIT_CONFIRMATION_REQUIRED":
      print("click submit here:", settled.live_view_url)
      # …after the human has clicked it:
      sente.runs.resume(run.id)
      done = sente.runs.wait_for_run(run.id)
  ```
</CodeGroup>

<Warning>
  A blocked run holds for about **10 minutes**, then fails with `BLOCKED_TIMEOUT`. Confirm-before-submit
  is useless if nobody is watching — wire the [`run.blocked` webhook](/events/webhooks) or run
  `sente watch` so the page reaches a person inside the hold.
</Warning>

## Calling register twice

Registration is idempotent per (identity, app origin), so a retry is safe:

| State                                  | What a second `register` does                                                    |
| -------------------------------------- | -------------------------------------------------------------------------------- |
| A run is in flight for this identity   | Returns the same account and **that** run. No second signup.                     |
| The account is `active`                | Returns it and its last run, and drives nothing. Use `login` to re-authenticate. |
| The account is `pending` or `failed`   | Queues a fresh run on the same row — a retry.                                    |
| A **connection** exists at that origin | `409 ALREADY_CONNECTED`. That app is the owner's account.                        |

<Warning>
  This holds *per identity*. Calling `register` twice with no `identityId` provisions a second identity
  and signs up a second time. Keep the returned `registration.id` (or pass `identityId`) when you mean
  "the account I already made".
</Warning>

## Failure paths

Call-time errors:

| Status | Code                      | Means                                                                        | Do                                                        |
| ------ | ------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------- |
| `409`  | `ALREADY_CONNECTED`       | A connected account already exists at that origin.                           | Use it — see [Connect](/accounts/connect).                |
| `429`  | `RUN_LIMIT_EXCEEDED`      | Daily or monthly run cap. Failed runs never count.                           | Wait for the window or upgrade ([Limits](/trust/limits)). |
| `429`  | `IDENTITY_LIMIT_EXCEEDED` | Auto-provisioning would exceed the identity cap.                             | Pass an existing `identityId`, or upgrade.                |
| `400`  | —                         | Invalid body — most often a malformed `appUrl` (it must be an absolute URL). | Fix and retry.                                            |

Run outcomes. Everything in the first group **blocks and waits for a person**; nothing is
circumvented:

| `error.code`                   | Means                                                                      | Do                                                                                 |
| ------------------------------ | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `SUBMIT_CONFIRMATION_REQUIRED` | You asked for it — the form is filled, waiting for a human click.          | Click submit in the live view, then resume.                                        |
| `CAPTCHA_REQUIRED`             | The app presented a CAPTCHA.                                               | Solve it in the live view, then resume.                                            |
| `PHONE_REQUIRED`               | The signup demands SMS verification.                                       | A human supplies a number in the live view, or abandon the target.                 |
| `MISSING_FIELD`                | The form wants something the run wasn't given (invite code, company name). | Fill it in the live view and resume — then supply it next time.                    |
| `PAYMENT_REQUIRED`             | The app asked for card details. Sente never enters them.                   | Handle it yourself in the live view, or abandon the target.                        |
| `LOGIN_UNCONFIRMED`            | The agent couldn't confirm the account was created.                        | Check the live view; often an interstitial or a silent rejection.                  |
| `VERIFICATION_TIMEOUT`         | No usable verification email arrived within the wait window.               | Check whether the app ever sent it — a silent fraud block looks exactly like this. |
| `BLOCKED_TIMEOUT`              | Nobody resumed within the \~10-minute hold.                                | Retry with a human standing by.                                                    |
| `AUTOMATION_STUCK`             | The agent could not proceed and named no known gate.                       | Inspect the run and retry.                                                         |

## After the run completes

* `sente credentials <registrationId>` / `getCredentials(id)` — the vaulted
  `{ username, password, origin }`.
* `sente relogin <registrationId>` / `registrations.login(id)` — re-authenticate later; returns a
  fresh `{ registration, run }`.
* `sente session open <registrationId>` / `getSession(id)` — a browser already signed in. See
  [Sessions](/accounts/sessions).
* The app's ongoing mail — receipts, notifications, password resets — keeps arriving in the account's
  inbox. Read it like any other [message](/email/inbox).

## Next steps

<CardGroup cols={2}>
  <Card title="Human takeover" href="/runs/human-takeover">
    The live view, the \~10-minute hold, and resuming a blocked signup.
  </Card>

  <Card title="Sessions" href="/accounts/sessions">
    Hand the new account to your automation as a CDP URL or `storageState`.
  </Card>

  <Card title="Acceptable use" href="/trust/acceptable-use">
    What we ask before you point a registration run at someone else's site.
  </Card>

  <Card title="Connect an account you own" href="/accounts/connect">
    The lower-risk path when the account can exist already.
  </Card>
</CardGroup>
