---
title: "Confirm Verification"
method: POST
path: "/signup/verify"
tags: ["signup"]
---

# Confirm Verification

`POST /signup/verify`

## Request

- Base URL: `https://app.zeno-works.ch` (the document declares no server; this is the origin it was published from)
- URL: `https://app.zeno-works.ch/signup/verify`
- Auth: none declared

Create the account the link authorises, and — separately — decide about the client.

**`connect_client` is a second consent, not a formality.** Creating an account and
handing a credential to whoever asked for it are two different things, and one button
doing both is what makes "an attacker starts a signup at a victim's address" pay: the
victim's click would release a key to the requester as a side effect of getting an
account. Two buttons on the page, two outcomes here, and the default is no.

**The password is hashed before the transaction opens.** argon2id takes hundreds of
milliseconds *by design*, and that is the width of the window in which a decline can
race this. `complete_link_signup` takes the row lock; hashing outside it is what keeps
the lock short enough for the race to be decided rather than merely narrowed.

**This is where `user.subscribed` is reported** (doc/architecture.md §"Platform event
notifications") — the only self-serve account-creation path there is, so the only place
a subscription can be observed. Scheduled after the response like every send in this
module, and only on this success path: every refusal above raises first, so a declined
or replayed link reports nothing. The timing-oracle rule the module docstring states
does not bite here — it guards the two *start* routes, which are pre-proof and
enumerable, whereas reaching this line requires the 256-bit token we mailed.

## Request body

- VerifyIn: **Every field is bounded.** The retired OTP shape learned this the hard way: bare ``str`` meant an empty password created a real account that ``LoginRequest`` (min_length=1) could never authenticate — burning the address permanently — and a 300-character name overflowed ``User.first_name``'s ``String(100)`` and escaped as a 500, because ``DataError`` is not ``IntegrityError`` and nothing caught it.
  - `token` string, required
  - `first_name` string, required
  - `last_name` string, required
  - `password` string, required
  - `connect_client` boolean

## Response `200`

Successful Response

- VerifyOut
  - `status` string
  - `connected` boolean

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/zeno-works/apis/zeno-api.md) · [All operations](https://skmtc.dev/zeno-works/apis/zeno-api/llms.txt) · [OpenAPI document](https://skmtc.dev/zeno-works/apis/zeno-api/revisions/1eb93dde146c?raw)
