From 3b56f0ae1873d5f74da613125a2ce3ee75edb92c Mon Sep 17 00:00:00 2001 From: Corey Haines <34802794+coreyhaines31@users.noreply.github.com> Date: Sat, 21 Feb 2026 03:03:51 -0800 Subject: [PATCH 1/2] fix: align SDK with real Truelist API - Change verify endpoint from POST /api/v1/form_verify (JSON body) to POST /api/v1/verify_inline?email=... (query param, no body) - Change account endpoint from GET /api/v1/account to GET /me - Unwrap response from emails[] array wrapper - Map API field names: address->email, email_state->state, email_sub_state->subState, did_you_mean->suggestion - Add new fields: domain, canonical, mxRecord, firstName, lastName, verifiedAt - Remove fields that dont exist in real API: freeEmail, role, disposable - Update state values: valid->ok, invalid->email_invalid, new accept_all - Update sub-state values: ok->email_ok, disposable_address->is_disposable, role_address->is_role, unknown->unknown_error, new failed_smtp_check - Remove form_verify/verify endpoint option (single endpoint now) - Update Zod and React Hook Form integrations - Update README with correct API details Co-Authored-By: Claude Opus 4.6 --- README.md | 67 +++++++++++++++++----------------- src/client.ts | 71 +++++++++++++++++++++++++++++-------- src/email-input.tsx | 4 +-- src/index.ts | 4 ++- src/provider.tsx | 4 +-- src/react-hook-form.ts | 19 +++------- src/types.ts | 58 ++++++++++++++++++------------ src/use-email-validation.ts | 2 +- src/zod.ts | 23 ++++-------- 9 files changed, 145 insertions(+), 107 deletions(-) diff --git a/README.md b/README.md index 0a7d83f..4d9e773 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ import { TruelistProvider, useEmailValidation } from "@truelist/react"; function App() { return ( - + ); @@ -30,7 +30,7 @@ function SignupForm() { validate(e.target.value)} - aria-invalid={result?.state === "invalid"} + aria-invalid={result?.state === "email_invalid"} /> ); } @@ -58,8 +58,8 @@ function EmailField() { placeholder="you@example.com" /> {isValidating && Checking...} - {result?.state === "invalid" && This email is not valid.} - {result?.state === "risky" && This email may not receive mail.} + {result?.state === "email_invalid" && This email is not valid.} + {result?.state === "accept_all" && This email may not be verifiable.} {result?.suggestion && Did you mean {result.suggestion}?} {error && {error}} @@ -106,15 +106,15 @@ import { EmailInput } from "@truelist/react"; The input exposes a `data-validation-state` attribute for CSS styling: ```css -input[data-validation-state="valid"] { +input[data-validation-state="ok"] { border-color: green; } -input[data-validation-state="invalid"] { +input[data-validation-state="email_invalid"] { border-color: red; } -input[data-validation-state="risky"] { +input[data-validation-state="accept_all"] { border-color: orange; } @@ -129,7 +129,7 @@ All standard `` props are supported, plus: | Prop | Type | Default | Description | |---|---|---|---| -| `validateOn` | `"blur" \| "change" \| "submit"` | `"blur"` | When to trigger validation | +| `validateOn` | `"blur" \| "change"` | `"blur"` | When to trigger validation | | `debounceMs` | `number` | `500` | Debounce delay for "change" mode | | `onValidation` | `(result: ValidationResult) => void` | -- | Callback when validation completes | | `renderSuggestion` | `(suggestion: string) => ReactNode` | -- | Custom suggestion renderer | @@ -144,7 +144,7 @@ import { z } from "zod"; import { truelistEmail } from "@truelist/react/zod"; const schema = z.object({ - email: truelistEmail({ apiKey: "your-form-api-key" }), + email: truelistEmail({ apiKey: "your-api-key" }), }); // Async validation (required for API calls) @@ -155,17 +155,17 @@ const result = await schema.parseAsync({ email: "user@example.com" }); | Option | Type | Default | Description | |---|---|---|---| -| `apiKey` | `string` | *required* | Your Truelist form API key | +| `apiKey` | `string` | *required* | Your Truelist API key | | `baseUrl` | `string` | `https://api.truelist.io` | Custom API base URL | -| `rejectStates` | `ValidationState[]` | `["invalid"]` | States that fail validation | +| `rejectStates` | `ValidationState[]` | `["email_invalid"]` | States that fail validation | | `message` | `string` | `"This email address is not valid."` | Error message | -Reject risky emails too: +Reject unknown emails too: ```ts truelistEmail({ - apiKey: "your-form-api-key", - rejectStates: ["invalid", "risky"], + apiKey: "your-api-key", + rejectStates: ["email_invalid", "unknown"], }); ``` @@ -179,7 +179,7 @@ import { truelistFieldValidator } from "@truelist/react/react-hook-form"; function SignupForm() { const { register, handleSubmit, formState: { errors } } = useForm(); - const validate = truelistFieldValidator({ apiKey: "your-form-api-key" }); + const validate = truelistFieldValidator({ apiKey: "your-api-key" }); return (
@@ -195,9 +195,9 @@ function SignupForm() { | Option | Type | Default | Description | |---|---|---|---| -| `apiKey` | `string` | *required* | Your Truelist form API key | +| `apiKey` | `string` | *required* | Your Truelist API key | | `baseUrl` | `string` | `https://api.truelist.io` | Custom API base URL | -| `rejectStates` | `ValidationState[]` | `["invalid"]` | States that fail validation | +| `rejectStates` | `ValidationState[]` | `["email_invalid"]` | States that fail validation | | `message` | `string` | `"This email address is not valid."` | Error message | ## Types @@ -214,23 +214,23 @@ import type { ### `ValidationState` ```ts -type ValidationState = "valid" | "invalid" | "risky" | "unknown"; +type ValidationState = "ok" | "email_invalid" | "accept_all" | "unknown"; ``` ### `ValidationSubState` ```ts type ValidationSubState = - | "ok" - | "accept_all" - | "disposable_address" - | "role_address" + | "email_ok" + | "is_disposable" + | "is_role" + | "failed_smtp_check" | "failed_mx_check" | "failed_spam_trap" | "failed_no_mailbox" | "failed_greylisted" | "failed_syntax_check" - | "unknown"; + | "unknown_error"; ``` ### `ValidationResult` @@ -240,9 +240,13 @@ type ValidationResult = { state: ValidationState; subState: ValidationSubState; email: string; - suggestion?: string; - freeEmail?: boolean; - role?: boolean; + domain: string; + canonical: string; + mxRecord: string | null; + firstName: string | null; + lastName: string | null; + verifiedAt: string; + suggestion: string | null; }; ``` @@ -262,22 +266,21 @@ type TruelistConfig = { Wrap your app with `TruelistProvider` to make the API key available to all hooks and components: ```tsx - + ``` | Prop | Type | Default | Description | |---|---|---|---| -| `apiKey` | `string` | *required* | Your Truelist form API key | +| `apiKey` | `string` | *required* | Your Truelist API key | | `baseUrl` | `string` | `https://api.truelist.io` | Custom API base URL | ### API Details -- **Endpoint**: `POST https://api.truelist.io/api/v1/form_verify` -- **Auth**: Bearer token (your form API key) -- **Rate limit**: 60 requests per minute for form keys -- **Billing**: Credits are only charged for definitive results (`valid`/`invalid`), not for `unknown` +- **Endpoint**: `POST https://api.truelist.io/api/v1/verify_inline?email=user@example.com` +- **Auth**: Bearer token (your API key) +- **Response**: `{ "emails": [{ "address": "...", "email_state": "ok", ... }] }` Get your API key at [truelist.io](https://truelist.io). diff --git a/src/client.ts b/src/client.ts index a0bad36..aa44e22 100644 --- a/src/client.ts +++ b/src/client.ts @@ -20,40 +20,36 @@ export class TruelistApiError extends Error { * @param email - The email address to validate. * @param config - API key and optional base URL. * @param signal - Optional AbortSignal to cancel the request. - * @param endpoint - Which API endpoint to use. `"form_verify"` for client-side, `"verify"` for server-side. Default: `"form_verify"`. * @returns The validation result. * @throws {TruelistApiError} When the API returns a non-OK response. */ export async function verifyEmail( email: string, config: TruelistConfig, - signal?: AbortSignal, - endpoint: "form_verify" | "verify" = "form_verify" + signal?: AbortSignal ): Promise { const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL; - const url = `${baseUrl}/api/v1/${endpoint}`; + const url = `${baseUrl}/api/v1/verify_inline?email=${encodeURIComponent(email)}`; const response = await fetch(url, { method: "POST", headers: { - "Content-Type": "application/json", Authorization: `Bearer ${config.apiKey}`, }, - body: JSON.stringify({ email }), signal, }); if (!response.ok) { if (response.status === 429) { throw new TruelistApiError( - "Rate limit exceeded. The form API allows 60 requests per minute.", + "Rate limit exceeded. Please try again later.", 429 ); } if (response.status === 401) { throw new TruelistApiError( - "Invalid API key. Check your Truelist form API key.", + "Invalid API key. Check your Truelist API key.", 401 ); } @@ -65,14 +61,59 @@ export async function verifyEmail( } const data: ApiResponse = await response.json(); + const record = data.emails[0]; + + if (!record) { + throw new TruelistApiError("No email record returned from API."); + } return { - state: data.state, - subState: data.sub_state, - email: data.email, - suggestion: data.suggestion, - freeEmail: data.free_email, - role: data.role, - disposable: data.disposable, + state: record.email_state, + subState: record.email_sub_state, + email: record.address, + domain: record.domain, + canonical: record.canonical, + mxRecord: record.mx_record, + firstName: record.first_name, + lastName: record.last_name, + verifiedAt: record.verified_at, + suggestion: record.did_you_mean, }; } + +/** + * Fetches account information for the authenticated API key. + * + * @param config - API key and optional base URL. + * @returns The account data. + * @throws {TruelistApiError} When the API returns a non-OK response. + */ +export async function getAccount( + config: TruelistConfig +): Promise> { + const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL; + const url = `${baseUrl}/me`; + + const response = await fetch(url, { + method: "GET", + headers: { + Authorization: `Bearer ${config.apiKey}`, + }, + }); + + if (!response.ok) { + if (response.status === 401) { + throw new TruelistApiError( + "Invalid API key. Check your Truelist API key.", + 401 + ); + } + + throw new TruelistApiError( + `Truelist API error: ${response.status} ${response.statusText}`, + response.status + ); + } + + return response.json() as Promise>; +} diff --git a/src/email-input.tsx b/src/email-input.tsx index 6df890a..dc57eef 100644 --- a/src/email-input.tsx +++ b/src/email-input.tsx @@ -18,7 +18,7 @@ export type EmailInputProps = Omit< onValidation?: (result: ValidationResult) => void; /** * Render function for the suggestion message. - * Receives the suggested domain (e.g. "gmail.com"). + * Receives the suggested correction (e.g. "user@gmail.com"). * If not provided, a default message is rendered as a ``. */ renderSuggestion?: (suggestion: string) => React.ReactNode; @@ -118,7 +118,7 @@ export const EmailInput = forwardRef( onChange={handleChange} onBlur={handleBlur} data-validation-state={dataState} - aria-invalid={result?.state === "invalid" || undefined} + aria-invalid={result?.state === "email_invalid" || undefined} /> {result?.suggestion && (renderSuggestion ? ( diff --git a/src/index.ts b/src/index.ts index 1546c32..b51a43c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,7 @@ export { EmailInput } from "./email-input"; export type { EmailInputProps, ValidateOn } from "./email-input"; // Client -export { verifyEmail, TruelistApiError } from "./client"; +export { verifyEmail, getAccount, TruelistApiError } from "./client"; // Types export type { @@ -22,4 +22,6 @@ export type { ValidationSubState, ValidationResult, TruelistConfig, + ApiResponse, + ApiEmailRecord, } from "./types"; diff --git a/src/provider.tsx b/src/provider.tsx index fc2e1e3..22b7ea1 100644 --- a/src/provider.tsx +++ b/src/provider.tsx @@ -5,7 +5,7 @@ import type { TruelistConfig } from "./types"; const TruelistContext = createContext(null); export type TruelistProviderProps = { - /** Your Truelist form API key. */ + /** Your Truelist API key. */ apiKey: string; /** Base URL for the Truelist API. Defaults to `https://api.truelist.io`. */ baseUrl?: string; @@ -20,7 +20,7 @@ export type TruelistProviderProps = { * * @example * ```tsx - * + * * * * ``` diff --git a/src/react-hook-form.ts b/src/react-hook-form.ts index bc55d35..f71b4f6 100644 --- a/src/react-hook-form.ts +++ b/src/react-hook-form.ts @@ -2,14 +2,9 @@ import { verifyEmail } from "./client"; import type { TruelistConfig, ValidationState } from "./types"; export type TruelistFieldValidatorOptions = TruelistConfig & { - /** - * Which API endpoint to use. Default: `"verify"` (server-side). - * Use `"form_verify"` for client-side usage. - */ - endpoint?: "form_verify" | "verify"; /** * Which validation states to treat as invalid. - * Default: `["invalid"]` + * Default: `["email_invalid"]` */ rejectStates?: ValidationState[]; /** Error message returned when validation fails. */ @@ -29,7 +24,7 @@ export type TruelistFieldValidatorOptions = TruelistConfig & { * function MyForm() { * const { register, handleSubmit } = useForm(); * const validate = truelistFieldValidator({ - * apiKey: "your-form-api-key", + * apiKey: "your-api-key", * }); * * return ( @@ -46,8 +41,7 @@ export function truelistFieldValidator( const { apiKey, baseUrl, - endpoint, - rejectStates = ["invalid"], + rejectStates = ["email_invalid"], message = "This email address is not valid.", } = options; @@ -57,12 +51,7 @@ export function truelistFieldValidator( } try { - const result = await verifyEmail( - value, - { apiKey, baseUrl }, - undefined, - endpoint ?? "verify" - ); + const result = await verifyEmail(value, { apiKey, baseUrl }); if (rejectStates.includes(result.state)) { return message; diff --git a/src/types.ts b/src/types.ts index c7c28db..fb1e542 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,18 +1,18 @@ /** Primary validation state returned by the Truelist API. */ -export type ValidationState = "valid" | "invalid" | "risky" | "unknown"; +export type ValidationState = "ok" | "email_invalid" | "accept_all" | "unknown"; /** Detailed sub-state providing more context about the validation result. */ export type ValidationSubState = - | "ok" - | "accept_all" - | "disposable_address" - | "role_address" + | "email_ok" + | "is_disposable" + | "is_role" + | "failed_smtp_check" | "failed_mx_check" | "failed_spam_trap" | "failed_no_mailbox" | "failed_greylisted" | "failed_syntax_check" - | "unknown"; + | "unknown_error"; /** The result object returned after validating an email address. */ export type ValidationResult = { @@ -22,31 +22,45 @@ export type ValidationResult = { subState: ValidationSubState; /** The email address that was validated. */ email: string; + /** The domain portion of the email address. */ + domain: string; + /** The canonical (local) portion of the email address. */ + canonical: string; + /** The MX record for the domain, or null. */ + mxRecord: string | null; + /** First name associated with the email, or null. */ + firstName: string | null; + /** Last name associated with the email, or null. */ + lastName: string | null; + /** ISO 8601 timestamp of when the email was verified. */ + verifiedAt: string; /** A suggested correction if a typo was detected (e.g. "Did you mean gmail.com?"). */ - suggestion?: string; - /** Whether the email belongs to a free email provider. */ - freeEmail: boolean; - /** Whether the email is a role-based address (e.g. info@, support@). */ - role: boolean; - /** Whether the email uses a disposable/temporary email provider. */ - disposable: boolean; + suggestion: string | null; }; /** Configuration for the Truelist provider and API client. */ export type TruelistConfig = { - /** Your Truelist form API key. */ + /** Your Truelist API key. */ apiKey: string; /** Base URL for the Truelist API. Defaults to `https://api.truelist.io`. */ baseUrl?: string; }; -/** Raw API response shape from the form_verify endpoint. */ +/** Raw API response shape from the verify_inline endpoint. */ export type ApiResponse = { - state: ValidationState; - sub_state: ValidationSubState; - email: string; - suggestion?: string; - free_email: boolean; - role: boolean; - disposable: boolean; + emails: ApiEmailRecord[]; +}; + +/** A single email record in the API response. */ +export type ApiEmailRecord = { + address: string; + domain: string; + canonical: string; + mx_record: string | null; + first_name: string | null; + last_name: string | null; + email_state: ValidationState; + email_sub_state: ValidationSubState; + verified_at: string; + did_you_mean: string | null; }; diff --git a/src/use-email-validation.ts b/src/use-email-validation.ts index edb9401..1058ba4 100644 --- a/src/use-email-validation.ts +++ b/src/use-email-validation.ts @@ -42,7 +42,7 @@ export type UseEmailValidationReturn = { * validate(e.target.value)} - * aria-invalid={result?.state === "invalid"} + * aria-invalid={result?.state === "email_invalid"} * /> * ); * } diff --git a/src/zod.ts b/src/zod.ts index d896424..e2bbc34 100644 --- a/src/zod.ts +++ b/src/zod.ts @@ -4,16 +4,11 @@ import { verifyEmail, TruelistApiError } from "./client"; import type { TruelistConfig, ValidationState } from "./types"; export type TruelistEmailOptions = TruelistConfig & { - /** - * Which API endpoint to use. Default: `"verify"` (server-side). - * Use `"form_verify"` for client-side usage. - */ - endpoint?: "form_verify" | "verify"; /** * Which validation states to treat as invalid. - * Default: `["invalid"]` + * Default: `["email_invalid"]` * - * Example: reject risky emails too with `["invalid", "risky"]`. + * Example: reject unknown emails too with `["email_invalid", "unknown"]`. */ rejectStates?: ValidationState[]; /** Custom error message. Default: "This email address is not valid." */ @@ -32,7 +27,7 @@ export type TruelistEmailOptions = TruelistConfig & { * import { truelistEmail } from "@truelist/react/zod"; * * const schema = z.object({ - * email: truelistEmail({ apiKey: "your-form-api-key" }), + * email: truelistEmail({ apiKey: "your-api-key" }), * }); * * // Async validation @@ -43,8 +38,7 @@ export function truelistEmail(options: TruelistEmailOptions): ZodType { const { apiKey, baseUrl, - endpoint, - rejectStates = ["invalid"], + rejectStates = ["email_invalid"], message = "This email address is not valid.", } = options; @@ -54,15 +48,10 @@ export function truelistEmail(options: TruelistEmailOptions): ZodType { .refine( async (email) => { try { - const result = await verifyEmail( - email, - { apiKey, baseUrl }, - undefined, - endpoint ?? "verify" - ); + const result = await verifyEmail(email, { apiKey, baseUrl }); return !rejectStates.includes(result.state); } catch (err) { - // Auth errors must always surface — never silently swallow a 401 + // Auth errors must always surface -- never silently swallow a 401 if (err instanceof TruelistApiError && err.status === 401) { throw err; } From bd7de616b52f7ac065375b48f0ec36acafd55a0e Mon Sep 17 00:00:00 2001 From: Corey Haines <34802794+coreyhaines31@users.noreply.github.com> Date: Sat, 21 Feb 2026 03:21:52 -0800 Subject: [PATCH 2/2] Add missing 'risky' to ValidationState type union Co-Authored-By: Claude Opus 4.6 --- src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index fb1e542..e7422e6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,5 @@ /** Primary validation state returned by the Truelist API. */ -export type ValidationState = "ok" | "email_invalid" | "accept_all" | "unknown"; +export type ValidationState = "ok" | "email_invalid" | "risky" | "accept_all" | "unknown"; /** Detailed sub-state providing more context about the validation result. */ export type ValidationSubState =