Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![npm version](https://img.shields.io/npm/v/checkthatphone)](https://www.npmjs.com/package/checkthatphone) [![CI](https://github.com/CheckThatPhone/checkthatphone-node/actions/workflows/ci.yml/badge.svg)](https://github.com/CheckThatPhone/checkthatphone-node/actions/workflows/ci.yml) [![node >= 18](https://img.shields.io/node/v/checkthatphone)](https://www.npmjs.com/package/checkthatphone)

Official Node.js client for the [CheckThatPhone](https://checkthatphone.com) phone validation API. Validate US and Canadian phone numbers in real time: carrier and line type from live carrier data, portability and deliverability, GeoIP and timezone, plus optional TCPA litigator screening and a free state do-not-call scrub — one call, one credit.
Official Node.js client for the [CheckThatPhone](https://checkthatphone.com) phone validation API. Validate US and Canadian phone numbers in real time: carrier and line type from live carrier data, portability and deliverability, GeoIP and timezone, plus optional TCPA litigator screening and free state do-not-call and complainer scrubs — one call, one credit.

Zero dependencies. Node 18+. TypeScript types included.

Expand Down Expand Up @@ -41,17 +41,22 @@ if (result.data.litigator === 'true') {
}
```

## State DNC scrub (free)
## State DNC and complainer scrub (free)

Screen state do-not-call registries (40 states) and a national complainer list at no extra credit:
Screen state do-not-call registries (38 states plus DC) and a national complainer list at no extra credit. Each check is its own flag:

```js
const result = await client.lookup('8182925409', { dncOther: true });
result.data.dncStateResult; // "STATE DNC" or ""
result.data.dncComplainerResult; // "DNC COMPLAINER" or ""
result.data.dncStateCovered; // "false" = state not in the data; don't read "" as clear
const result = await client.lookup('8182925409', { dncState: true, dncComplainer: true });
result.data.dncStateChecked; // "true" when the state check ran; absent for the 12 states with no registry data
result.data.dncStateResult; // "STATE DNC" on a match, "" otherwise (only when dncStateChecked is "true")
result.data.dncComplainerChecked; // "true" when the complainer check ran
result.data.dncComplainerResult; // "DNC COMPLAINER" on a match, "" otherwise
```

Read `dncStateChecked` before `dncStateResult`: a number in an uncovered state comes back with neither field, which means not checked, not clear. `"error"` means the check could not complete; treat it as unavailable.

`dncOther: true` still works as a shorthand for both checks, but it is deprecated.

## Landline SMS reachability

Some landlines can receive texts. Detect them instead of dropping them (+1 credit, charged only when the number is a landline):
Expand Down
27 changes: 27 additions & 0 deletions context7.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"$schema": "https://context7.com/schema/context7.json",
"projectTitle": "CheckThatPhone Node.js SDK",
"description": "Official Node.js client for the CheckThatPhone phone validation API: US and Canada carrier lookup, line type, portability, deliverability, GeoIP and timezone, TCPA litigator screening and a free state do-not-call scrub in one call.",
"folders": [],
"excludeFolders": [
"src",
"test",
".github"
],
"excludeFiles": [
"LICENSE",
"package-lock.json"
],
"rules": [
"Phone numbers must be US or Canadian: 10 digits, or 11 digits starting with 1. Strip formatting before calling client.lookup(); anything else is rejected with HTTP 400 and costs nothing.",
"Keep the API key server-side, e.g. new CheckThatPhone(process.env.CHECKTHATPHONE_API_KEY). Never call the API from browser code.",
"Every value in result.data is a string: compare with 'true' / 'false' (result.data.deliverable === 'true'), never with booleans.",
"Use result.data.action and result.data.deliverable as the recommendation for a number. Do not re-derive deliverability from raw carrier fields.",
"Pass options.ip (the contact's IP address) whenever you have it: it upgrades location to city level and sets an IANA timezone (result.data.timezone) at no extra credit. Without it, location falls back to the area code.",
"Add-ons are opt-in flags on the options object: litigatorFilter costs +1 credit per lookup, landlineSmsLookup costs +1 credit only when the number is a landline, dncState (state do-not-call registries) and dncComplainer (national complainer list) are free. Enable only the add-ons you read. dncOther is a deprecated alias for both DNC checks.",
"With dncState, read result.data.dncStateChecked before dncStateResult: both are absent for the 12 states with no registry data (not checked, never clear), and 'error' means the check could not complete.",
"Catch CheckThatPhoneError and check err.retryable before retrying; a 429 means the per-minute rate limit was hit, so back off. Failed and invalid requests cost 0 credits.",
"One lookup costs one credit plus any add-ons (result.creditsUsed). The free tier is 500 lookups per month and hard-capped, so cache results if you re-check the same numbers often."
],
"previousVersions": []
}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "checkthatphone",
"version": "0.1.0",
"version": "0.2.0",
"description": "Official Node.js client for the CheckThatPhone phone validation API — US & Canada carrier lookup, line type, TCPA litigator and state DNC screening",
"keywords": ["phone-validation", "carrier-lookup", "tcpa", "litigator-scrub", "dnc", "phone-number", "sms", "nanp", "line-type"],
"homepage": "https://checkthatphone.com/docs",
Expand Down
27 changes: 22 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,15 @@ export interface LookupOptions {
litigatorFilter?: boolean;
/** Landline SMS reachability (+1 credit, landlines only). Adds dipMessaging* fields. */
landlineSmsLookup?: boolean;
/** State DNC & complainers scrub (free). Adds dncOtherChecked, dncStateResult,
* dncComplainerResult, dncStateCovered. */
/** State do-not-call registry check (free; 38 states plus DC). Adds
* dncStateChecked and dncStateResult; both are absent for the 12 states
* with no registry data, so "absent" means not checked, never clear. */
dncState?: boolean;
/** National complainer-list check (free). Adds dncComplainerChecked and
* dncComplainerResult. */
dncComplainer?: boolean;
/** @deprecated Use dncState and dncComplainer. Turns both on; the client
* sends the two new flags, never the legacy one. */
dncOther?: boolean;
}

Expand Down Expand Up @@ -57,9 +64,18 @@ export interface LookupData {
dipMessagingProvider?: string;
dipMessagingRefId?: string;
dipMessagingCountryCode?: string;
dncOtherChecked?: string;
/** "true" when the state check ran; "error" when it could not complete
* (treat as unavailable, not clear). Absent for uncovered states. */
dncStateChecked?: string;
/** "STATE DNC" on a registry match, "" otherwise. Only when dncStateChecked is "true". */
dncStateResult?: string;
/** "true" when the complainer check ran; "error" when it could not complete. */
dncComplainerChecked?: string;
/** "DNC COMPLAINER" on a match, "" otherwise. Only when dncComplainerChecked is "true". */
dncComplainerResult?: string;
/** Legacy: "true" when either DNC check ran. */
dncOtherChecked?: string;
/** @deprecated Same answer as dncStateChecked. */
dncStateCovered?: string;
[key: string]: unknown;
}
Expand Down Expand Up @@ -126,7 +142,8 @@ export class CheckThatPhone {
if (options.ip) body.ip = options.ip;
if (options.litigatorFilter) body.litigatorFilter = true;
if (options.landlineSmsLookup) body.landlineSmsLookup = true;
if (options.dncOther) body.dncOther = true;
if (options.dncState || options.dncOther) body.dncState = true;
if (options.dncComplainer || options.dncOther) body.dncComplainer = true;

const res = await fetch(`${this.baseUrl}/v1/lookup`, {
method: 'POST',
Expand Down Expand Up @@ -157,6 +174,6 @@ export class CheckThatPhone {
}
}

export const VERSION = '0.1.0';
export const VERSION = '0.2.0';

export default CheckThatPhone;
18 changes: 16 additions & 2 deletions test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('CheckThatPhone.lookup', () => {
data: { subscriber: '8182925409', nanpType: 'mobile', litigator: 'false' },
});
const client = new CheckThatPhone('ctp_live_test');
const res = await client.lookup('(818) 292-5409', { litigatorFilter: true, dncOther: true });
const res = await client.lookup('(818) 292-5409', { litigatorFilter: true, dncState: true, dncComplainer: true });

expect(res.success).toBe(true);
expect(res.creditsUsed).toBe(2);
Expand All @@ -26,10 +26,24 @@ describe('CheckThatPhone.lookup', () => {
const [url, init] = fn.mock.calls[0] as [string, RequestInit];
expect(url).toBe('https://api.checkthatphone.com/v1/lookup');
const sent = JSON.parse(String(init.body));
expect(sent).toEqual({ phone: '(818) 292-5409', litigatorFilter: true, dncOther: true });
expect(sent).toEqual({ phone: '(818) 292-5409', litigatorFilter: true, dncState: true, dncComplainer: true });
expect((init.headers as Record<string, string>).authorization).toBe('Bearer ctp_live_test');
});

it('translates the deprecated dncOther flag into dncState + dncComplainer on the wire', async () => {
const fn = mockFetchOnce(200, { success: true, credits_used: 1, data: {} });
await new CheckThatPhone('k').lookup('8182925409', { dncOther: true });
const sent = JSON.parse(String((fn.mock.calls[0] as [string, RequestInit])[1].body));
expect(sent).toEqual({ phone: '8182925409', dncState: true, dncComplainer: true });
});

it('sends only the DNC check that was asked for', async () => {
const fn = mockFetchOnce(200, { success: true, credits_used: 1, data: {} });
await new CheckThatPhone('k').lookup('8182925409', { dncComplainer: true });
const sent = JSON.parse(String((fn.mock.calls[0] as [string, RequestInit])[1].body));
expect(sent).toEqual({ phone: '8182925409', dncComplainer: true });
});

it('throws a typed error with status and detail on 4xx', async () => {
mockFetchOnce(400, { error: 'Invalid request', detail: 'phone must be 10-11 digits' });
const client = new CheckThatPhone('k');
Expand Down
Loading