> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://mpp.dev/api/mcp` to find what you need.

# Email OTP \[Verify an email address]

Use the email OTP extension when callers need a verified address and you don't have your own identity provider. An issuer is the service that emails codes and signs attestations: bearer tokens confirming inbox access for a specific API origin. The server checks this token before issuing a payment Challenge or settling a Credential.

The examples use Tempo's hosted issuer; you can also self-host it.

## Overview

```mermaid
sequenceDiagram
  participant Client
  participant Issuer
  participant API
  Client->>API: Request without attestation
  API-->>Client: 401 + Identity-Attestation-Required
  Client->>Issuer: Request code for email and API origin
  Issuer-->>Client: Email code
  Client->>Issuer: Exchange code
  Issuer-->>Client: Origin-bound attestation
  Client->>API: Request + Identity-Attestation
  API-->>Client: 402 + Challenge
  Client->>API: Credential + Identity-Attestation
  API-->>Client: 200 + Receipt

```

## Set it up

```ts twoslash [server.ts]
import * as EmailOtp from 'mppx/extensions/identity/email-otp'
import { Mppx, tempo } from 'mppx/server'

// [!code hl:start]
const email = EmailOtp.Server.verifier({
  allow: (address) => address.endsWith('@example.com'),
  audience: 'https://api.example.com',
  issuer: 'https://identity.mpp.tempo.xyz',
})
// [!code hl:end]
const mppx = Mppx.create({ methods: [tempo.charge()] })

export async function handler(request: Request) {
  const result = await mppx.charge({ amount: '0.05', identity: [email] })(request)
  if (result.status === 402) return result.challenge
  return result.withReceipt(Response.json({ email: result.identity?.get(email)?.email }))
}
```

Configure the matching client prover. Use your terminal or form to return the emailed code from `code`:

```ts twoslash [client.ts]
import { Mppx, tempo } from 'mppx/client'
import * as EmailOtp from 'mppx/extensions/identity/email-otp'
import type { Account } from 'viem'

declare const account: Account
declare function promptForCode(message: string): Promise<string | undefined>

const client = Mppx.create({
  // [!code hl:start]
  identity: [EmailOtp.Client.prover({
    code: ({ audience, email, issuer }) =>
      promptForCode(`Code from ${issuer} for ${email} at ${audience}:`),
    email: 'you@example.com',
    issuers: ['https://identity.mpp.tempo.xyz'],
    scope: ['https://api.example.com'],
  })],
  // [!code hl:end]
  methods: [tempo({ account })],
  polyfill: false,
})

const response = await client.fetch('https://api.example.com/report')
console.log(response.status)
// @log: 200
```

### Details

* The prover only contacts allowlisted `issuers` and only sends attestations to in-scope origins matching their audience. Never trust an issuer supplied only by a server—it could relay your code to another issuer.
* The server returns `401` for missing or invalid attestations, `403` for a verified address rejected by `allow`, and `503` when issuer keys are unavailable. Only identity success reaches payment.
* Cached attestations are reused in memory and refreshed near expiry before payment. The prover prompts at most three times for rejected codes; paid attempts aren't resent by identity recovery.
* For your own send/verify form, use [`sendCode`](/sdk/typescript/client/EmailOtp.sendCode), [`verifyCode`](/sdk/typescript/client/EmailOtp.verifyCode), and [pre-obtained attestations](/sdk/typescript/client/EmailOtp.prover#use-a-custom-email-form).

:::warning[Keep attestations at their audience]
An attestation is a reusable bearer token, not a per-request signature. Use HTTPS and configure `audience` as your fixed public API origin. Derive it from the request only when the platform guarantees the host; a caller-controlled `Host` header can bypass isolation.

Don't forward `Identity-Attestation` to upstream services. `mppx` strips it from cross-origin payment preparation, and its proxy strips both this request header and upstream `Identity-Attestation-Required` responses. Authenticate each hop independently.
:::

See [`EmailOtp.Server.verifier`](/sdk/typescript/server/EmailOtp.verifier) for key resolution and rejection rules, and [`EmailOtp.Client.prover`](/sdk/typescript/client/EmailOtp.prover) for callbacks, expiry handling, and errors. The [Cloudflare Worker example](https://github.com/wevm/mppx/tree/main/examples/email-otp) includes a terminal prompt, a domain allowlist, and a self-hosted issuer option.

## Other attestation types

An attestation is a short-lived token an issuer signs after checking one fact about the caller, bound to your API's origin. Email OTP is one attestation type. Use the attestation module directly for another type, such as a phone number, or for your own issuer.

```ts twoslash [server.ts]
import * as Attestation from 'mppx/extensions/identity/attestation'
import { Mppx, tempo } from 'mppx/server'

// [!code hl:start]
const phone = Attestation.Server.verifier({
  audience: 'https://api.example.com',
  issuer: 'https://phone.example.com',
  type: 'phone',
  validate: (claims) =>
    claims.sub.startsWith('+1') ? undefined : 'Only US numbers are allowed.',
})
// [!code hl:end]
const mppx = Mppx.create({ methods: [tempo.charge()] })
export const handler = mppx.charge({ amount: '0.05', identity: [phone] })
```

A request without a matching attestation gets `401` with `Identity-Attestation-Required: phone;issuer="https://phone.example.com"`. A `validate` rejection gets `403`, and unreachable issuer keys get `503`.

On the client, the prover sends cached attestations to in-scope origins and calls `acquire` when a server asks for one from a trusted issuer:

```ts twoslash [client.ts]
import { Mppx, tempo } from 'mppx/client'
import * as Attestation from 'mppx/extensions/identity/attestation'
import type { Account } from 'viem'

declare const account: Account
declare function obtainPhoneAttestation(parameters: {
  audience: string
  issuer: string
}): Promise<string | undefined>

const client = Mppx.create({
  // [!code hl:start]
  identity: [Attestation.Client.prover({
    acquire: ({ audience, issuer }) => obtainPhoneAttestation({ audience, issuer }),
    issuers: ['https://phone.example.com'],
    scope: ['https://api.example.com'],
    types: ['phone'],
  })],
  // [!code hl:end]
  methods: [tempo({ account })],
  polyfill: false,
})
```

* The audience is always the request's own origin, never one a server names, so a server can't obtain an attestation it could replay elsewhere.
* `acquire` only runs for `issuers` you list. Never trust an issuer named only by a server.
* To run an issuer, sign claims with [`Attestation.sign`](/sdk/typescript/core/Attestation.sign) and publish your Ed25519 keys at `/.well-known/jwks.json`. Servers read them with [`Attestation.Jwks.resolver`](/sdk/typescript/core/Attestation.Jwks.resolver).

See [`Attestation.Server.verifier`](/sdk/typescript/server/Attestation.verifier) and [`Attestation.Client.prover`](/sdk/typescript/client/Attestation.prover) for every option.
