1import http from 'node:http';
2import { Actor } from 'apify';
3import { cleanPhoneNumber, cleanPhoneNumbers } from './cleaner.js';
4
5const PPE_EVENT = 'phone-number-clean';
6
7
8
9
10
11
12function normalizeItems(items) {
13 return items.map((item) => {
14 if (typeof item === 'string') {
15 return item;
16 }
17 if (item && typeof item === 'object' && 'input' in item) {
18 return String(item.input ?? '');
19 }
20 return String(item ?? '');
21 });
22}
23
24await Actor.init();
25
26if (Actor.config.get('metaOrigin') === 'STANDBY') {
27 startStandbyServer();
28} else {
29 await runBatchMode();
30 await Actor.exit();
31}
32
33
34
35
36
37function startStandbyServer() {
38 const port = Actor.config.get('containerPort');
39
40 const server = http.createServer(async (req, res) => {
41
42 if (req.headers['x-apify-container-server-readiness-probe']) {
43 res.writeHead(200, { 'Content-Type': 'application/json' });
44 res.end(JSON.stringify({ status: 'ready' }));
45 return;
46 }
47
48
49 if (req.method !== 'GET') {
50 res.writeHead(405, { 'Content-Type': 'application/json' });
51 res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
52 return;
53 }
54
55
56 const reqUrl = new URL(req.url, `http://${req.headers.host}`);
57 if (!reqUrl.searchParams.get('input')) {
58 res.writeHead(200, { 'Content-Type': 'text/html' });
59 res.end(getLandingPageHtml());
60 return;
61 }
62
63 try {
64 const result = handleStandbyRequest(reqUrl);
65 await Actor.pushData(result);
66
67
68 try {
69 await Actor.charge({ eventName: PPE_EVENT, count: 1 });
70 } catch (chargeErr) {
71 console.warn(`PPE charge failed: ${chargeErr.message}`);
72 }
73
74 res.writeHead(200, { 'Content-Type': 'application/json' });
75 res.end(JSON.stringify(result));
76 } catch (err) {
77 const statusCode = err.statusCode || 500;
78 res.writeHead(statusCode, { 'Content-Type': 'application/json' });
79 res.end(JSON.stringify({ error: err.message }));
80 }
81 });
82
83 server.listen(port, () => {
84 console.log(`Superclean Phone Numbers Standby server listening on port ${port}`);
85 });
86}
87
88function handleStandbyRequest(reqUrl) {
89 const input = reqUrl.searchParams.get('input');
90
91 if (!input) {
92 const err = new Error("Provide an 'input' query parameter with the phone number to clean.");
93 err.statusCode = 400;
94 throw err;
95 }
96
97 const defaultCountry = (reqUrl.searchParams.get('defaultCountry') || 'US').toUpperCase();
98 const outputFormat = reqUrl.searchParams.get('outputFormat') || 'e164';
99
100
101 if (defaultCountry.length !== 2) {
102 const err = new Error('defaultCountry must be a 2-letter ISO country code (e.g., US, GB, DE)');
103 err.statusCode = 400;
104 throw err;
105 }
106
107
108 const validFormats = ['e164', 'international', 'national'];
109 if (!validFormats.includes(outputFormat)) {
110 const err = new Error(`Invalid outputFormat "${outputFormat}". Must be one of: ${validFormats.join(', ')}`);
111 err.statusCode = 400;
112 throw err;
113 }
114
115 const cleaned = cleanPhoneNumber(input, { defaultCountry, outputFormat });
116
117 return {
118 id: 1,
119 input: cleaned.input,
120 output: cleaned.output,
121 e164: cleaned.e164,
122 isValid: cleaned.isValid,
123 type: cleaned.type,
124 countryCode: cleaned.countryCode,
125 extension: cleaned.extension,
126 };
127}
128
129
130
131
132
133async function runBatchMode() {
134 const input = await Actor.getInput();
135
136 let {
137 items: rawItems = [],
138 item,
139 defaultCountry = 'US',
140 outputFormat = 'e164'
141 } = input || {};
142
143
144 if (typeof item === 'string' && item.trim()) {
145 if (!Array.isArray(rawItems)) rawItems = [];
146 rawItems = [item.trim(), ...rawItems];
147 }
148
149
150 if (typeof rawItems === 'string') rawItems = [rawItems];
151
152
153 if (!Array.isArray(rawItems) || rawItems.length === 0) {
154 await Actor.fail('Input required: provide "items" (array) or "item" (single string). Example: {"items": ["+1 (555) 123-4567"]}');
155 return;
156 }
157
158
159 const items = normalizeItems(rawItems);
160
161
162 if (typeof defaultCountry !== 'string' || defaultCountry.length !== 2) {
163 throw new Error('defaultCountry must be a 2-letter ISO country code (e.g., US, GB, DE)');
164 }
165
166
167 const validFormats = ['e164', 'international', 'national'];
168 if (!validFormats.includes(outputFormat)) {
169 throw new Error(`Invalid outputFormat "${outputFormat}". Must be one of: ${validFormats.join(', ')}`);
170 }
171
172 console.log(`Processing ${items.length} phone number(s)`);
173 console.log(`Options: defaultCountry=${defaultCountry}, outputFormat=${outputFormat}`);
174
175
176 const results = cleanPhoneNumbers(items, {
177 defaultCountry: defaultCountry.toUpperCase(),
178 outputFormat
179 });
180
181
182 let chargedCount = 0;
183 let chargeLimitReached = false;
184 for (const result of results) {
185 if (!chargeLimitReached) {
186 try {
187 const chargeResult = await Actor.charge({ eventName: PPE_EVENT, count: 1 });
188 chargedCount++;
189 if (chargeResult?.eventChargeLimitReached) {
190 chargeLimitReached = true;
191 console.log(`User spending limit reached after charging ${chargedCount}/${results.length} items`);
192 }
193 } catch (chargeErr) {
194 console.warn(`PPE charge failed: ${chargeErr.message}`);
195 }
196 }
197 }
198
199 await Actor.pushData(results);
200
201
202 const validCount = results.filter(r => r.isValid).length;
203 const typeBreakdown = results.reduce((acc, r) => {
204 if (r.type) {
205 acc[r.type] = (acc[r.type] || 0) + 1;
206 }
207 return acc;
208 }, {});
209
210 console.log(`Processed ${results.length} phone numbers`);
211 console.log(`Valid: ${validCount}, Invalid: ${results.length - validCount}`);
212 if (Object.keys(typeBreakdown).length > 0) {
213 console.log(`Types: ${JSON.stringify(typeBreakdown)}`);
214 }
215}
216
217
218
219
220
221function getLandingPageHtml() {
222 return `<!DOCTYPE html>
223<html lang="en">
224<head>
225<meta charset="utf-8">
226<meta name="viewport" content="width=device-width, initial-scale=1">
227<title>Superclean Phone Numbers — Superlative</title>
228<link rel="preconnect" href="https://fonts.googleapis.com">
229<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
230<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
231<style>
232:root{--color-bg:#0a0e14;--color-surface:#12171f;--color-border:#1e2632;--color-text:#e8eaed;--color-text-muted:#8b95a5;--color-accent:#4ade80;--color-accent-dim:rgba(74,222,128,0.15);--font-display:'Space Mono',monospace;--font-body:'DM Sans',sans-serif}
233*{margin:0;padding:0;box-sizing:border-box}
234body{font-family:var(--font-body);background:var(--color-bg);color:var(--color-text);line-height:1.6;min-height:100vh;overflow-x:hidden}
235a{color:var(--color-accent);text-decoration:none;transition:color .2s ease}
236a:hover{color:#6ee7a0}
237.grid-bg{position:fixed;top:0;left:0;width:100%;height:100%;background-image:linear-gradient(var(--color-border) 1px,transparent 1px),linear-gradient(90deg,var(--color-border) 1px,transparent 1px);background-size:60px 60px;opacity:.3;pointer-events:none;z-index:0}
238.accent-line{position:fixed;top:0;left:0;width:100%;height:2px;background:linear-gradient(90deg,transparent 0%,var(--color-accent) 50%,transparent 100%);animation:shimmer 3s ease-in-out infinite;z-index:10}
239@keyframes shimmer{0%,100%{transform:translateX(-100%)}50%{transform:translateX(100%)}}
240@keyframes fadeUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
241.container{position:relative;z-index:1;max-width:900px;margin:0 auto;padding:0 24px}
242header{padding:32px 0}
243.logo{font-family:var(--font-display);font-size:14px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--color-text);display:flex;align-items:center;gap:10px;text-decoration:none}
244.logo:hover{color:var(--color-text)}
245.logo-mark{width:28px;height:28px;background:var(--color-accent);display:flex;align-items:center;justify-content:center}
246.logo-mark svg{width:16px;height:16px}
247.hero{padding:80px 0 60px;animation:fadeUp .8s ease-out}
248.hero-label{font-family:var(--font-display);font-size:12px;font-weight:400;letter-spacing:.15em;text-transform:uppercase;color:var(--color-accent);margin-bottom:24px;display:flex;align-items:center;gap:12px}
249.hero-label::before{content:'';width:24px;height:1px;background:var(--color-accent)}
250h1{font-family:var(--font-display);font-size:clamp(28px,5vw,44px);font-weight:700;line-height:1.15;margin-bottom:20px;letter-spacing:-.02em}
251h1 .highlight{color:var(--color-accent)}
252.hero-desc{font-size:18px;color:var(--color-text-muted);max-width:540px;margin-bottom:28px;line-height:1.7}
253.hero-link{font-family:var(--font-display);font-size:13px;letter-spacing:.05em;color:var(--color-accent);display:inline-flex;align-items:center;gap:6px}
254.section{padding:60px 0;border-top:1px solid var(--color-border);animation:fadeUp .8s ease-out .2s backwards}
255.section-label{font-family:var(--font-display);font-size:11px;font-weight:400;letter-spacing:.2em;text-transform:uppercase;color:var(--color-text-muted);margin-bottom:24px}
256h2{font-family:var(--font-display);font-size:20px;font-weight:700;margin-bottom:16px;letter-spacing:-.01em}
257p,li{font-size:15px;color:var(--color-text-muted);line-height:1.7}
258li{margin-bottom:6px}
259ul{margin:.5rem 0 1rem 1.25rem}
260strong{color:var(--color-text);font-weight:600}
261pre{background:var(--color-surface);color:var(--color-text);padding:16px 20px;overflow-x:auto;margin:12px 0 16px;font-family:var(--font-display);font-size:13px;line-height:1.6;border:1px solid var(--color-border)}
262code{font-family:var(--font-display);font-size:13px}
263p code,li code,td code{background:var(--color-accent-dim);color:var(--color-accent);padding:2px 6px}
264table{width:100%;border-collapse:collapse;margin:12px 0 16px;font-size:14px}
265th,td{border:1px solid var(--color-border);padding:10px 14px;text-align:left}
266th{background:var(--color-surface);color:var(--color-text-muted);font-family:var(--font-display);font-size:12px;font-weight:400;letter-spacing:.05em;text-transform:uppercase}
267td{color:var(--color-text-muted)}
268.badge{display:inline-block;background:var(--color-accent-dim);color:var(--color-accent);padding:3px 8px;font-family:var(--font-display);font-size:11px;font-weight:400;letter-spacing:.05em}
269.note{font-size:13px;color:var(--color-text-muted);margin-top:8px}
270.product-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:16px;margin-top:24px}
271.product-card{background:var(--color-surface);border:1px solid var(--color-border);padding:24px;transition:all .3s ease;text-decoration:none;color:inherit;display:block}
272.product-card:hover{border-color:var(--color-accent);transform:translateY(-2px)}
273.product-name{font-family:var(--font-display);font-size:14px;font-weight:700;margin-bottom:8px;color:var(--color-text);display:flex;align-items:center;gap:8px}
274.product-name .status{font-size:9px;font-weight:400;padding:3px 6px;background:var(--color-accent-dim);color:var(--color-accent);letter-spacing:.1em}
275.product-desc{font-size:14px;color:var(--color-text-muted);line-height:1.5}
276footer{padding:48px 0;border-top:1px solid var(--color-border)}
277.footer-content{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:24px}
278.footer-text{font-size:13px;color:var(--color-text-muted)}
279.footer-links{display:flex;gap:24px}
280.footer-links a{font-family:var(--font-display);font-size:12px;color:var(--color-text-muted);letter-spacing:.05em}
281.footer-links a:hover{color:var(--color-accent)}
282@media(max-width:600px){.hero{padding:60px 0 40px}.section{padding:40px 0}.footer-content{flex-direction:column;align-items:flex-start}pre{font-size:12px;padding:12px 14px}}
283</style>
284</head>
285<body>
286<div class="grid-bg"></div>
287<div class="accent-line"></div>
288
289<div class="container">
290 <header>
291 <a href="https://apify.com/superlativetech?fpr=8e9l1" class="logo">
292 <div class="logo-mark">
293 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
294 <polyline points="4 17 10 11 4 5"></polyline>
295 <line x1="12" y1="19" x2="20" y2="19"></line>
296 </svg>
297 </div>
298 Superlative
299 </a>
300 </header>
301
302 <section class="hero">
303 <div class="hero-label">Superclean</div>
304 <h1>Superclean Phone <span class="highlight">Numbers</span></h1>
305 <p class="hero-desc">Instant phone number cleaning and validation. Format to E.164, detect type, handle vanity numbers. Sub-second responses via Standby API.</p>
306 <a href="https://apify.com/superlativetech/superclean-phone-numbers?fpr=8e9l1" class="hero-link">View on Apify Store →</a>
307 </section>
308
309 <section class="section">
310 <div class="section-label">Features</div>
311 <h2>What this does</h2>
312 <ul>
313 <li><strong>Formats numbers</strong> — E.164, international, or national output formats</li>
314 <li><strong>Validates numbers</strong> — Check if phone numbers are valid with type detection</li>
315 <li><strong>Handles vanity</strong> — Convert 1-800-FLOWERS to digits automatically</li>
316 <li><strong>Detects type</strong> — Mobile, landline, toll-free, VOIP classification</li>
317 <li><strong>Batch mode</strong> — Process hundreds of numbers via the standard Actor run</li>
318 </ul>
319 </section>
320
321 <section class="section">
322 <div class="section-label">Getting Started</div>
323 <h2>Quick start</h2>
324 <p>Replace <code>YOUR_TOKEN</code> with your Apify API token.</p>
325
326 <p style="margin-top:16px"><strong>Clean a phone number (E.164):</strong></p>
327 <pre>curl "https://superlativetech--superclean-phone-numbers.apify.actor?token=YOUR_TOKEN&input=(555)+123-4567"</pre>
328
329 <p><strong>International format:</strong></p>
330 <pre>curl "https://superlativetech--superclean-phone-numbers.apify.actor?token=YOUR_TOKEN&input=(555)+123-4567&outputFormat=international"</pre>
331
332 <p><strong>Non-US number:</strong></p>
333 <pre>curl "https://superlativetech--superclean-phone-numbers.apify.actor?token=YOUR_TOKEN&input=020+7946+0958&defaultCountry=GB"</pre>
334 </section>
335
336 <section class="section">
337 <div class="section-label">Reference</div>
338 <h2>Query parameters</h2>
339 <table>
340 <tr><th>Parameter</th><th>Required</th><th>Description</th></tr>
341 <tr><td><code>input</code></td><td>Yes</td><td>Phone number to clean</td></tr>
342 <tr><td><code>defaultCountry</code></td><td>No</td><td>2-letter ISO country code (default: <code>US</code>)</td></tr>
343 <tr><td><code>outputFormat</code></td><td>No</td><td>Format: <code>e164</code> (default), <code>international</code>, or <code>national</code></td></tr>
344 <tr><td><code>token</code></td><td>Yes</td><td>Your Apify API token</td></tr>
345 </table>
346 </section>
347
348 <section class="section">
349 <div class="section-label">Response</div>
350 <h2>Response format</h2>
351 <pre>{
352 "id": 1,
353 "input": "(555) 123-4567",
354 "output": "+15551234567",
355 "e164": "+15551234567",
356 "isValid": true,
357 "type": "fixed_line_or_mobile",
358 "countryCode": "US",
359 "extension": null
360}</pre>
361 </section>
362
363 <section class="section">
364 <div class="section-label">Cost</div>
365 <h2>Pricing</h2>
366 <p>Pay-per-event pricing on the Apify platform:</p>
367 <table>
368 <tr><th>Tier</th><th>Numbers</th><th>Per 1,000</th></tr>
369 <tr><td>Base</td><td>Any</td><td>$0.50</td></tr>
370 <tr><td><span class="badge">Bronze</span></td><td>100+</td><td>$0.45</td></tr>
371 <tr><td><span class="badge">Silver</span></td><td>1,000+</td><td>$0.40</td></tr>
372 <tr><td><span class="badge">Gold</span></td><td>10,000+</td><td>$0.35</td></tr>
373 </table>
374 </section>
375
376 <section class="section">
377 <div class="section-label">Auth</div>
378 <h2>Authentication</h2>
379 <p>Authenticate using either method:</p>
380 <ul>
381 <li>Query parameter: <code>?token=YOUR_APIFY_TOKEN</code></li>
382 <li>Header: <code>Authorization: Bearer YOUR_APIFY_TOKEN</code></li>
383 </ul>
384 <p style="margin-top:8px">Get your token from <a href="https://console.apify.com/settings/integrations">Apify Console → Settings → Integrations</a>.</p>
385 </section>
386
387 <section class="section">
388 <div class="section-label">Superlative</div>
389 <h2>More from Superlative</h2>
390 <div class="product-grid">
391 <a href="https://apify.com/superlativetech/superclean-company-names?fpr=8e9l1" class="product-card">
392 <div class="product-name">Company Names <span class="status">Live</span></div>
393 <p class="product-desc">Normalize company names for CRM and cold email outreach.</p>
394 </a>
395 <a href="https://apify.com/superlativetech/superclean-job-titles?fpr=8e9l1" class="product-card">
396 <div class="product-name">Job Titles <span class="status">Live</span></div>
397 <p class="product-desc">Standardize job titles for outreach sequences.</p>
398 </a>
399 <a href="https://apify.com/superlativetech/superclean-person-names?fpr=8e9l1" class="product-card">
400 <div class="product-name">Person Names <span class="status">Live</span></div>
401 <p class="product-desc">Clean and format person names for personalization.</p>
402 </a>
403 <a href="https://apify.com/superlativetech/superclean-product-names?fpr=8e9l1" class="product-card">
404 <div class="product-name">Product Names <span class="status">Live</span></div>
405 <p class="product-desc">Normalize product and brand names from exports.</p>
406 </a>
407 <a href="https://apify.com/superlativetech/superclean-places?fpr=8e9l1" class="product-card">
408 <div class="product-name">Places <span class="status">Live</span></div>
409 <p class="product-desc">Parse and standardize location strings.</p>
410 </a>
411 <a href="https://apify.com/superlativetech/superclean-urls?fpr=8e9l1" class="product-card">
412 <div class="product-name">URLs <span class="status">Live</span></div>
413 <p class="product-desc">Clean and normalize URLs from lead data.</p>
414 </a>
415 <a href="https://apify.com/superlativetech/dns-lookup?fpr=8e9l1" class="product-card">
416 <div class="product-name">Supernet DNS Lookup <span class="status">Live</span></div>
417 <p class="product-desc">Look up DNS records for any domain.</p>
418 </a>
419 <a href="https://apify.com/superlativetech/http-api?fpr=8e9l1" class="product-card">
420 <div class="product-name">HTTP API <span class="status">Live</span></div>
421 <p class="product-desc">General-purpose HTTP request utility.</p>
422 </a>
423 </div>
424 </section>
425
426 <footer>
427 <div class="footer-content">
428 <p class="footer-text">© 2026 Superlative</p>
429 <div class="footer-links">
430 <a href="https://apify.com/superlativetech?fpr=8e9l1">Apify Store</a>
431 </div>
432 </div>
433 </footer>
434</div>
435
436</body>
437</html>`;
438}