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

# Build the waiting screen

> From claim to proved: what to poll, what to show, and how to tell a wait from a mistake.

The screen between "I created the record" and "proved" is where most verification flows lose people.
This guide builds one that says something true at every moment.

## Claim, and show the record

```ts theme={null}
try {
  const domain = await ownsi.domains.findOrCreate(name)
  return await domain.claim()
} catch (error) {
  if (isOwnsiError(error) && error.code === "already_claimed") return useExistingClaim()
  throw error
}
```

The `record` on the claim is what to render — `host`, `name`, `type` and `value`. It is `null`
once the claim has ended, because there is nothing left to write.

```tsx theme={null}
<Field label="Type" value={claim.record.type} />
<Field label="Host" value={claim.record.host} copy />
<Field label="Value" value={claim.record.value} copy />
```

<Warning>
  Copy `host`, not `name`. Almost every panel appends the zone to what you type, and a copy button
  that hands over the fully qualified name is how people end up with
  [`domain_appended`](/diagnostics/catalogue#domain_appended). Offer `name` as a secondary
  "fully qualified" affordance for the panels that want it.
</Warning>

## Poll the verification, not the claim

The claim only moves once, at the end. What changes while someone waits is the process behind it,
and the claim knows which one that is.

```ts hooks/useVerificationState.ts theme={null}
export const useVerificationState = (claimId: string) =>
  useQuery({
    queryKey: ["verification", claimId],
    queryFn: async () => (await ownsi.claims.get(claimId)).verification(),
    refetchInterval: (query) => nextPoll(query.state.data),
  })

const RUNNING = new Set(["checking", "propagating", "needs_attention"])

const nextPoll = (verification?: Verification) => {
  if (!verification || !RUNNING.has(verification.status)) return false
  if (verification.waitEstimate) {
    return Math.min(verification.waitEstimate.secondsRemaining, 30) * 1_000
  }
  return 5_000
}
```

Deriving the interval from `waitEstimate.secondsRemaining` means the client asks roughly when there
is something new to hear, instead of every five seconds forever.

<Note>
  `POST /api/verifications/:id/runs` forces a run and is rate limited per verification. Wire it to
  a button the person presses, not to a timer. Nothing about asking more often makes DNS answer
  sooner, and the honest interval is already on the verification.
</Note>

## One message, from two fields

A verification that has not proved always carries exactly one of `diagnosis` or `waitEstimate`,
and they mean opposite things.

```ts theme={null}
export const whatNow = (verification: Verification) => {
  if (verification.status === "proved") return { kind: "done" as const }

  if (verification.diagnosis) {
    return {
      kind: "fix" as const,
      title: verification.diagnosis.cause,
      action: verification.diagnosis.fix,
      code: verification.diagnosis.code,
    }
  }

  return {
    kind: "wait" as const,
    title: waitTitle(verification.waitEstimate),
    seconds: verification.waitEstimate?.secondsRemaining ?? null,
  }
}
```

Render `cause` and `fix` verbatim — they already name your domain, your token and your nameservers.
Do not pattern-match on their text; they are product copy and change. Key anything behavioural off
`code`.

## Style the three groups differently

Not every diagnosis is your reader's fault, and treating them alike is the mistake worth avoiding.

<CardGroup cols={3}>
  <Card title="Fix it" icon="pen">
    `domain_appended` · `record_at_apex` · `value_formatted` · `record_on_www` ·
    `no_matching_record` · `record_absent` · `cname_conflict`

    Primary action, prominent. `fix` is a real instruction.
  </Card>

  <Card title="Wait" icon="hourglass">
    `negative_cache` · `not_published`

    Calm, with the countdown. **No action button** — there is nothing to press.
  </Card>

  <Card title="Escalate" icon="triangle-exclamation">
    `servfail` · `lame_delegation` · `foreign_token`

    Point at the provider or the other account. Not something retyping the record will fix.
  </Card>
</CardGroup>

## Status drives the frame around it

| `verification.status` | Frame                                                                              |
| --------------------- | ---------------------------------------------------------------------------------- |
| `checking`            | The record, and the first-run countdown.                                           |
| `propagating`         | "Your nameservers have it." Wait, not a fix.                                       |
| `needs_attention`     | The diagnosis, front and centre.                                                   |
| `proved`              | The date on `claim.endedAt`, and `domain.proof()` if the name proved before.       |
| `exhausted`           | The window closed. One button to claim again — a new token, one edit in the panel. |
| `stopped`             | The claim was canceled, or its domain archived. No action.                         |

`propagating` versus `needs_attention` is the split that earns the product its keep — both are "not
verified", but only one is your reader's problem.

## Do not tell people to delete the record

Leaving it in place is the supported state. A later claim on the same name proves in one run
because the record is already there, which is what keeps `lastConfirmedAt` moving, and [the token survives archiving and
reactivation](/concepts/claim-lifecycle#archiving-is-not-deleting) — a domain reactivated a year
later verifies without anyone opening a DNS panel.
