<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: DSP</title>
    <description>The latest articles on DEV Community by DSP (@darksun159).</description>
    <link>https://dev.to/darksun159</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4207629%2F220a1db9-7307-495e-bf6f-9ca69fc66337.png</url>
      <title>DEV Community: DSP</title>
      <link>https://dev.to/darksun159</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/darksun159"/>
    <language>en</language>
    <item>
      <title>DigitProof: one API for every check digit, and it fixes the wrong ones</title>
      <dc:creator>DSP</dc:creator>
      <pubDate>Mon, 26 Oct 2026 05:57:40 +0000</pubDate>
      <link>https://dev.to/darksun159/digitproof-one-api-for-every-check-digit-and-it-fixes-the-wrong-ones-3ebm</link>
      <guid>https://dev.to/darksun159/digitproof-one-api-for-every-check-digit-and-it-fixes-the-wrong-ones-3ebm</guid>
      <description>&lt;p&gt;Every codebase I have ever worked in has a graveyard of half-remembered validation rules. The IBAN check is mod-97, but only after you rearrange the string. The ISBN-10 check digit can be the letter X. A VIN has a check digit at position 9, unless it was built for a market that does not use one. Each identifier has its own standard, its own math, and its own way of quietly going wrong.&lt;/p&gt;

&lt;p&gt;DigitProof is my attempt to put all of that in one place. It is a single API that validates nine families of structured identifiers, parses them, and, this is the part I actually wanted, tells you the correct check digit when the one you have is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it validates
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;IBAN: length and structure for 86 countries, mod-97 check digits, BBAN parsing (bank code, branch code, account number) for 22 countries, SEPA flag, grouped formatting&lt;/li&gt;
&lt;li&gt;LEI: the ISO 17442 format and its mod-97 check digits, with the LOU prefix broken out&lt;/li&gt;
&lt;li&gt;ISIN: country and NSIN structure with the Luhn check digit&lt;/li&gt;
&lt;li&gt;ISBN-10 and ISBN-13, including conversion between them&lt;/li&gt;
&lt;li&gt;GTIN-8, UPC-A, EAN-13, and GTIN-14, detected from length&lt;/li&gt;
&lt;li&gt;VIN: 17 characters, the North American position-9 check digit, make hints and model year decoding&lt;/li&gt;
&lt;li&gt;Payment cards: Luhn, brand detection, and the networks' published test numbers flagged as &lt;code&gt;test_only&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;UUID: version and variant parsing&lt;/li&gt;
&lt;li&gt;EU and GB VAT numbers: per-country formats, with check digits verified where a country defines them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is format and checksum validation only. There are no bank directories or registry lookups behind it, and the listing says so plainly. A valid result means the identifier is well-formed and its math checks out.&lt;/p&gt;

&lt;h2&gt;
  
  
  A taste
&lt;/h2&gt;

&lt;p&gt;Send a known-good IBAN:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://digitproof.p.rapidapi.com/v1/validate"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-RapidAPI-Key: YOUR_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"value": "GB29 NWBK 6016 1331 9268 19"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get back the parsed structure, not just a yes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"iban"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"valid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"details"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"country_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sepa"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"bank_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NWBK"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"branch_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"601613"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"account_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"31926819"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"formatted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GB29 NWBK 6016 1331 9268 19"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the interesting part. Change the last digit of that account number and the checksum fails, and instead of a bare &lt;code&gt;false&lt;/code&gt; you get the expected check digits and the corrected identifier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"valid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"errors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bad_checksum"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"expected_check_digits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"56"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"actual_check_digits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"29"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"correction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GB56NWBK60161331926818"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also a dedicated check-digit endpoint that computes the digit even when the check position holds a placeholder, auto-detection if you do not know what kind of identifier you are holding (a 13-digit 978 number comes back as both ISBN-13 and GTIN-13), and batch validation for up to 100 identifiers per call.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it is built
&lt;/h2&gt;

&lt;p&gt;FastAPI on the free tier of Render, with the validation logic kept pure and separate from the web layer. Every validator runs on published standards and official test vectors, and the test suite caught me three real math bugs before launch, including one in the French VAT key formula. The API is stateless and stores nothing: values are processed in transit and forgotten.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;p&gt;Every endpoint is available on every plan, including the free tier. The validate, batch, check-digit, and types endpoints are all there from the start, with no Pro-only identifier types and no correction feature held back to force an upgrade. Upgrading simply gets you more requests per month: start free with 150 requests a month (and remember, one request can carry a batch of 100), move up to Pro at $9/month for 5,000, Ultra at $19/month for 20,000, or Mega at $49/month for 100,000. You pay for volume, not for access.&lt;/p&gt;

&lt;p&gt;The listing is here: &lt;a href="https://rapidapi.com/darksundarker/api/digitproof" rel="noopener noreferrer"&gt;https://rapidapi.com/darksundarker/api/digitproof&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feedback very welcome, especially the "I wish it also validated X" kind. That is how the next identifier on the list gets chosen.&lt;/p&gt;

</description>
      <category>api</category>
      <category>python</category>
      <category>fastapi</category>
      <category>validation</category>
    </item>
    <item>
      <title>SynthForge: fake test data with deterministic seeds, custom schemas, and SQL output</title>
      <dc:creator>DSP</dc:creator>
      <pubDate>Mon, 26 Oct 2026 04:11:47 +0000</pubDate>
      <link>https://dev.to/darksun159/synthforge-fake-test-data-with-deterministic-seeds-custom-schemas-and-sql-output-5ebe</link>
      <guid>https://dev.to/darksun159/synthforge-fake-test-data-with-deterministic-seeds-custom-schemas-and-sql-output-5ebe</guid>
      <description>&lt;p&gt;Seeding a database with &lt;code&gt;user1&lt;/code&gt;, &lt;code&gt;user2&lt;/code&gt;, &lt;code&gt;test@test.com&lt;/code&gt; works right up until your UI has to render a real-looking name, your demo needs 500 believable products, or your test suite needs the &lt;em&gt;same&lt;/em&gt; dataset on every run, not a fresh snowflake each time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SynthForge&lt;/strong&gt; is a synthetic data API we built at DSP to fix that. It's live on RapidAPI with a free tier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Predefined generators
&lt;/h2&gt;

&lt;p&gt;Seven generators, up to &lt;strong&gt;1,000 records per call&lt;/strong&gt;: &lt;code&gt;person&lt;/code&gt;, &lt;code&gt;company&lt;/code&gt;, &lt;code&gt;address&lt;/code&gt;, &lt;code&gt;product&lt;/code&gt;, &lt;code&gt;user&lt;/code&gt;, &lt;code&gt;payment-card&lt;/code&gt;, and &lt;code&gt;internet&lt;/code&gt;, covering names, safe emails, phones, job titles, SKUs, prices, usernames, IPs, UUIDs, and the rest of the usual test-data furniture.&lt;/p&gt;

&lt;p&gt;Payment cards deserve a specific mention: numbers come only from the standard public test ranges (Visa &lt;code&gt;4242…&lt;/code&gt;, Mastercard &lt;code&gt;5555…&lt;/code&gt;, Amex, Discover), are Luhn-valid so your validators accept them, and every record is flagged &lt;code&gt;"test_only": true&lt;/code&gt;. They will never work for a real purchase, by design.&lt;/p&gt;

&lt;h2&gt;
  
  
  The headline: bring your own schema
&lt;/h2&gt;

&lt;p&gt;Describe the shape you want using any of &lt;strong&gt;38 field generators&lt;/strong&gt;, and get back exactly that shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://synthforge.p.rapidapi.com/v1/generate/schema &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-RapidAPI-Key: YOUR_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"schema": {"id": "uuid", "name": "product_name",
                  "price": "price", "city": "city", "zip": "zip_code"},
       "count": 2, "seed": 7}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6513270e-269e-4d37-b2a7-4de452e6b438"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Premium Lamp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"395.03"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"North Loriville"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"zip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"48432"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"81e74ef5-e8e2-4d94-8ed9-04759531985d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Modular Blender"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"59.99"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"North Christopher"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"zip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"55311"&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output formats: &lt;strong&gt;JSON, CSV, or SQL &lt;code&gt;INSERT&lt;/code&gt; statements&lt;/strong&gt;, so generated data can go straight into a seed file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seeds make it deterministic
&lt;/h2&gt;

&lt;p&gt;Every endpoint accepts a &lt;code&gt;seed&lt;/code&gt;. Same seed, same data, byte for byte, seed 42 on the person generator gives you Margaret Johnson today, tomorrow, and in CI. That's the difference between a flaky fixture and a reliable one.&lt;/p&gt;

&lt;p&gt;Locales are supported where they make sense (&lt;code&gt;en_US&lt;/code&gt;, &lt;code&gt;en_GB&lt;/code&gt;, &lt;code&gt;de_DE&lt;/code&gt;, &lt;code&gt;fr_FR&lt;/code&gt;, &lt;code&gt;es_ES&lt;/code&gt;, &lt;code&gt;ja_JP&lt;/code&gt;), and emails use safe, non-deliverable addresses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;p&gt;Every endpoint is available on every plan, including the free tier. All seven predefined generators and the custom schema endpoint are included from the start, with no Pro-only generators and no output format held back to force an upgrade. Upgrading simply gets you more requests per month: start free with 150 requests a month, move up to Pro at $4/month for 5,000, Ultra at $9/month for 20,000, or Mega at $24/month for 500,000. And since a single request can generate up to 1,000 records, even the free tier goes a long way. You pay for volume, not for access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href="https://rapidapi.com/darksundarker/api/synthforge" rel="noopener noreferrer"&gt;https://rapidapi.com/darksundarker/api/synthforge&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What generator is missing for your use case? That's the v1.1 backlog, and it's being written by whoever asks.&lt;/p&gt;

</description>
      <category>api</category>
      <category>python</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Messy data in, clean records out: RecordScrub, a data-cleaning API</title>
      <dc:creator>DSP</dc:creator>
      <pubDate>Mon, 26 Oct 2026 04:03:46 +0000</pubDate>
      <link>https://dev.to/darksun159/messy-data-in-clean-records-out-recordscrub-a-data-cleaning-api-3ce7</link>
      <guid>https://dev.to/darksun159/messy-data-in-clean-records-out-recordscrub-a-data-cleaning-api-3ce7</guid>
      <description>&lt;p&gt;Every project eventually inherits the same spreadsheet: emails in three different cases, phone numbers formatted six ways, names like "DOE,  john  michael", and the same customer entered twice. Cleaning it by hand, or with a pile of regexes you don't quite trust, is nobody's favorite afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RecordScrub&lt;/strong&gt; is a small API we built at DSP to do that job in one call. It's live on RapidAPI now, with a free tier.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Email validation &amp;amp; normalization&lt;/strong&gt;, single or batch, with valid/invalid stats and a plain-language reason for every rejection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phone normalization to E.164&lt;/strong&gt;, powered by Google's libphonenumber, so &lt;code&gt;(312) 555-0142&lt;/code&gt; becomes &lt;code&gt;+13125550142&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name normalization&lt;/strong&gt;, &lt;code&gt;"DOE,  john  michael"&lt;/code&gt; → &lt;code&gt;John Michael Doe&lt;/code&gt;, with first/last split out&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;US address normalization&lt;/strong&gt;, state codes, ZIP+4, standardized street suffixes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch record cleaning&lt;/strong&gt;, send CSV-style records, get snake_case keys, per-field cleaning, and exact dedupe on the fields &lt;em&gt;you&lt;/em&gt; choose, with a duplicate report showing what was dropped and why&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every dropped or altered value comes back with a &lt;code&gt;_warnings&lt;/code&gt; explanation. No silent data loss.&lt;/p&gt;

&lt;h2&gt;
  
  
  A taste
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://record-scrub.p.rapidapi.com/v1/normalize/phone &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-RapidAPI-Key: YOUR_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"phone": "(312) 555-0142"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"original"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"(312) 555-0142"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"normalized"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"+13125550142"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"valid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"country_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"national_format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"(312) 555-0142"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the batch cleaner, deduplicating on email + phone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"records"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"John Doe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"john@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"phone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"+13125550142"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"stats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"received"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"returned"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"duplicates_removed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"duplicates"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"duplicate_of"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"record"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}}]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How it's built
&lt;/h2&gt;

&lt;p&gt;FastAPI, completely stateless. Requests are processed in transit and &lt;strong&gt;nothing is stored&lt;/strong&gt;, so there's no database to leak from. It runs on a free-tier host, so the first call after a quiet spell can take a few extra seconds while it wakes up; after that it's fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;p&gt;Every endpoint is available on every plan, including the free tier. No feature gates, no Pro-only cleaners, no endpoint held back to force an upgrade. Upgrading simply gets you more requests per month: start free with 100 requests a month, move up to Pro at $10/month for 5,000, and scale from there if your batch volume grows. You pay for volume, not for access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href="https://rapidapi.com/darksundarker/api/record-scrub" rel="noopener noreferrer"&gt;https://rapidapi.com/darksundarker/api/record-scrub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feedback very welcome, especially the "I wish it also cleaned X" kind. That's how the v2 list gets written.&lt;/p&gt;

</description>
      <category>api</category>
      <category>python</category>
      <category>fastapi</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
