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

# Node, Python and PHP

> Use server clients for widget verification, verdicts and phone challenges.

[Download the server examples](https://heretic.tech/downloads/heretic-server-examples.zip). The archive includes three clients, runnable form integrations, and local tests under the MIT license. Copy the client into your backend; no registry installation is required.

| Client | File                | Runtime                            |
| ------ | ------------------- | ---------------------------------- |
| Node   | `node/heretic.mjs`  | Node 18+ or a server fetch runtime |
| Python | `python/heretic.py` | Python 3.10+, standard library     |
| PHP    | `php/Heretic.php`   | PHP 8.1+ with cURL                 |

Keep the secret key and client on your server. Clients preserve the API response fields and validate its status fields. The [plain HTML](/integration/plain-html), [Next.js](/integration/nextjs), and [SvelteKit](/integration/sveltekit) guides include a complete form and protected write.

## Verify a widget token

Here, `attempt` is your server's saved pending action. Choose the hostname, site ID and policy in server configuration. Read `token` from the submitted `heretic-response` field.

<CodeGroup>
  ```js Node theme={"dark"}
  import { HereticClient } from './node/heretic.mjs';
  const heretic = new HereticClient(process.env.HERETIC_SECRET_KEY);
  const proof = await heretic.siteverify(token, {
    policy: 'uncontradicted',
    expected_hostname: 'shop.example',
    expected_site_id: 'site_0123456789abcdef01234567',
    expected_action: 'signup',
    expected_cdata: attempt.id,
    idempotency_key: attempt.id,
  });
  if (proof.success !== true || proof.test === true) {
    throw new Error('Verification did not pass');
  }
  ```

  ```python Python theme={"dark"}
  import os
  from heretic import HereticClient
  heretic = HereticClient(os.environ['HERETIC_SECRET_KEY'])
  proof = heretic.siteverify(
      token,
      policy='uncontradicted',
      expected_hostname='shop.example',
      expected_site_id='site_0123456789abcdef01234567',
      expected_action='signup',
      expected_cdata=attempt['id'],
      idempotency_key=attempt['id'],
  )
  if proof['success'] is not True or proof.get('test') is True:
      raise RuntimeError('Verification did not pass')
  ```

  ```php PHP theme={"dark"}
  require __DIR__ . '/php/Heretic.php';
  $heretic = new HereticClient(getenv('HERETIC_SECRET_KEY'));
  $proof = $heretic->siteverify($token, [
      'policy' => 'uncontradicted',
      'expected_hostname' => 'shop.example',
      'expected_site_id' => 'site_0123456789abcdef01234567',
      'expected_action' => 'signup',
      'expected_cdata' => $attempt['id'],
      'idempotency_key' => $attempt['id'],
  ]);
  if ($proof['success'] !== true || ($proof['test'] ?? false) === true) {
      throw new RuntimeException('Verification did not pass');
  }
  ```
</CodeGroup>

HTTP 200 can contain `success: false`. With `policy: "report"`, a verified proof can also carry a contradicted or refused verdict; evaluate it using your own rule. [Siteverify](/api/siteverify-endpoint) defines each policy and error code.

## Read a verdict

Use the request ID saved with your pending action. Check its site and age against that action before applying your [signup rules](/integration/writing-rules).

<CodeGroup>
  ```js Node theme={"dark"}
  const result = await heretic.verdict(requestId);
  const verdict = result.verdict;
  const findingIds = result.signals.map(signal => signal.id);
  ```

  ```python Python theme={"dark"}
  result = heretic.verdict(request_id)
  verdict = result['verdict']
  finding_ids = [signal['id'] for signal in result['signals']]
  ```

  ```php PHP theme={"dark"}
  $result = $heretic->verdict($requestId);
  $verdict = $result['verdict'];
  $findingIds = array_column($result['signals'], 'id');
  ```
</CodeGroup>

## Create and read a challenge

Take `account_ref` from your authenticated account record. Omit it for an anonymous ceremony. Save the returned `challenge_id` with the pending action before sending the URL to the browser.

<CodeGroup>
  ```js Node theme={"dark"}
  const created = await heretic.createChallenge({
    mode: 'phone', account_ref: account.id, ttl_s: 600,
  });
  // Persist created.challenge_id with your pending action.
  const outcome = await heretic.challenge(created.challenge_id);
  ```

  ```python Python theme={"dark"}
  created = heretic.create_challenge(
      mode='phone', account_ref=account['id'], ttl_s=600,
  )
  # Persist created['challenge_id'] with your pending action.
  outcome = heretic.challenge(created['challenge_id'])
  ```

  ```php PHP theme={"dark"}
  $created = $heretic->createChallenge([
      'mode' => 'phone', 'account_ref' => $account['id'], 'ttl_s' => 600,
  ]);
  // Persist $created['challenge_id'] with your pending action.
  $outcome = $heretic->challenge($created['challenge_id']);
  ```
</CodeGroup>

A new challenge normally reads `pending`. Show its URL with [`heretic.challenge()`](/api/challenge-mint#show-the-ceremony). On completion, your backend reads the saved ID, requires `status: "passed"`, and checks the saved account reference. A browser callback cannot authorize the write. See [what a pass establishes](/api/challenge-read#what-a-pass-means).

Challenge creation has no idempotency key. A timed-out mint may have created a challenge; the clients never retry it automatically.

## Handle errors and retries

Clients make one request per call. Node and PHP default to a six-second request deadline; Python defaults to a six-second socket timeout. Configure Node with `{ timeoutMs: 3000 }`, Python with `timeout=3`, or PHP with the third constructor argument `3000`. Redirects are refused.

`HereticError` exposes `code` in Node/Python or `kind` in PHP: `http`, `network`, `timeout`, or `invalid-response`. HTTP errors also carry `status`. A rate-limited response includes `retryAfter` in Node/PHP or `retry_after` in Python, plus `codes` where supplied. This is the server's `Retry-After` header. Error messages omit credentials and response bodies.

Keep the action pending on transport errors. Retry siteverify with the exact token, idempotency key, policy and expected context. Respect `Retry-After` on HTTP 429. An unavailable result cannot authorize the write.

Write the action and consume its attempt in one database transaction. A repeated verification returns the existing action result. A challenge outcome remains readable, so its saved ID needs the same single-use treatment. The downloadable form examples implement this with a persistent SQLite receipt.


## Related topics

- [Verdict endpoint](/api/verdict-endpoint.md)
- [Challenge widget](/integration/challenge-widget.md)
- [Quickstart](/quickstart.md)
- [SvelteKit](/integration/sveltekit.md)
- [Plain HTML](/integration/plain-html.md)
