diff --git a/README.md b/README.md index 8ceadd8..c3697e6 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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): diff --git a/context7.json b/context7.json new file mode 100644 index 0000000..82444ef --- /dev/null +++ b/context7.json @@ -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": [] +} diff --git a/package-lock.json b/package-lock.json index b60c2f5..5213c75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "checkthatphone", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "checkthatphone", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "devDependencies": { "tsup": "^8.0.0", diff --git a/package.json b/package.json index f12b1f5..82299cb 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/index.ts b/src/index.ts index d825bad..f937b90 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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; } @@ -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; } @@ -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', @@ -157,6 +174,6 @@ export class CheckThatPhone { } } -export const VERSION = '0.1.0'; +export const VERSION = '0.2.0'; export default CheckThatPhone; diff --git a/test/client.test.ts b/test/client.test.ts index 01a4a66..54911f3 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -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); @@ -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).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');