<?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: Ing. Pablo Cueto</title>
    <description>The latest articles on DEV Community by Ing. Pablo Cueto (@pabscueto).</description>
    <link>https://dev.to/pabscueto</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%2F3931788%2F9bdc8f9b-b007-48c9-8b01-2d69407106ca.jpeg</url>
      <title>DEV Community: Ing. Pablo Cueto</title>
      <link>https://dev.to/pabscueto</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pabscueto"/>
    <language>en</language>
    <item>
      <title>Building a RAG assistant that says “I don’t know”: evaluation, security and a $0 deployment</title>
      <dc:creator>Ing. Pablo Cueto</dc:creator>
      <pubDate>Fri, 16 Oct 2026 23:16:29 +0000</pubDate>
      <link>https://dev.to/pabscueto/building-a-rag-assistant-that-says-i-dont-know-evaluation-security-and-a-0-deployment-28lm</link>
      <guid>https://dev.to/pabscueto/building-a-rag-assistant-that-says-i-dont-know-evaluation-security-and-a-0-deployment-28lm</guid>
      <description>&lt;p&gt;Most RAG tutorials end when the chatbot answers a question about a PDF. I wanted to find out what comes after that: how do you know it works, who is allowed to see what, what happens when a document tries to give it orders, and how do you ship it without paying for anything?&lt;/p&gt;

&lt;p&gt;This post walks through enterprise-ai-agent, a learning and portfolio project. It answers questions about public AWS documentation, cites its sources, refuses what the documents do not cover, and includes a small agent that can use tools. A note on how it was built: I worked step by step with an AI coding assistant (Claude). I ran every command, read every error and decided the scope, and writing this post was my way of making sure I can explain each choice. The project is not production software, and I say where it falls short at the end.&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/PabsCueto/enterprise-ai-agent" rel="noopener noreferrer"&gt;https://github.com/PabsCueto/enterprise-ai-agent&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What it does&lt;br&gt;
• POST /ask retrieves the chunks the user is allowed to see, asks Gemini to answer using only that context, and returns the answer with source URLs and token usage.&lt;br&gt;
• POST /agent is a LangGraph agent with three tools: search the documentation, query a simulated internal status API, and create a ticket draft (a local JSON file that is sent nowhere).&lt;br&gt;
• Around them: an evaluation harness, per-user access control, structured logs, and a Docker image that is built and smoke-tested in CI.&lt;br&gt;
The corpus is seven introductory pages of AWS documentation, about a hundred chunks. It is small on purpose: I can check every result by hand.&lt;br&gt;
request -&amp;gt; FastAPI (request id, JSON log, API key, rate limit)&lt;br&gt;
             |-- /ask   -&amp;gt; retrieve (per-user filter + score threshold) -&amp;gt; Gemini&lt;br&gt;
             |-- /agent -&amp;gt; LangGraph: agent &amp;lt;-&amp;gt; tools -&amp;gt; answer&lt;br&gt;
Two layers against made-up answers&lt;br&gt;
The first layer is a similarity threshold. Chunks that are not close enough to the question are dropped, and if nothing is left the model is never called. The second layer is the prompt: answer only from the context, or say you cannot find it.&lt;/p&gt;

&lt;p&gt;I measured both. Questions unrelated to AWS (the capital of France, a taco recipe) scored at most 0.575, while questions the corpus covers scored at least 0.682, so a threshold of 0.6 separates them cleanly.&lt;/p&gt;

&lt;p&gt;The interesting case is a question that is about AWS but is not covered: VPCs, EC2 pricing, RDS, Amplify. Those scored between 0.60 and 0.664, above the threshold. Embeddings measure topic, not whether the answer is in the text, and a question about RDS looks a lot like DynamoDB documentation. The score cannot catch them; the prompt does, and the model refused all four. Neither layer would be enough on its own.&lt;/p&gt;

&lt;p&gt;An agent with guardrails&lt;br&gt;
The agent is a small LangGraph graph: a model node that decides, a tools node that executes, and a loop between them until the model answers. What matters is what the agent cannot do:&lt;br&gt;
• It has no tools to modify or delete anything, and tickets are drafts.&lt;br&gt;
• Tickets are created only on an explicit request.&lt;br&gt;
• The graph stops after 10 steps, thread ids are validated, and tool output is treated as data rather than instructions.&lt;br&gt;
One lesson came from the prompt. My first rule told the agent to ask when information was missing. When I wrote “create a ticket for the problem you find”, it asked me for a title and description it could have written itself. The fix was one sentence: if the user delegates the content, draft it from the data you gathered, and ask only when there is not enough to write it.&lt;/p&gt;

&lt;p&gt;Measuring it&lt;br&gt;
The harness has three small datasets: 22 retrieval questions (14 covered, 4 unrelated, 4 about AWS but not covered), 8 agent cases and 7 prompt-injection cases. It reports hit rate and MRR, how many questions are answered or rejected, and whether the agent called the expected tools in order and avoided forbidden ones.&lt;/p&gt;

&lt;p&gt;The results were good: 100% hit rate at 4 with an MRR of 1.0 on the covered questions, and 8 of 8 agent cases. But the datasets are small and I wrote them, so those numbers say little on their own. What the harness gave me were two bugs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;One agent case failed. It was not the agent: my evaluation script passed a temporary folder to the ticket tool as a string instead of a path. The agent had chosen the right tools and crashed inside mine. There is now a regression test that fails without the fix.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;My first report suggested a threshold of 0.673. It came from mixing two kinds of “negative” question, and the margin was only 0.018. I separated them: the threshold is calibrated only against unrelated questions, and the AWS-but-not-covered ones are judged by the final answer.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Who can see what&lt;br&gt;
• API keys. Random 256-bit values, stored only as SHA-256 hashes and compared in constant time. With no users configured, every protected request is rejected.&lt;br&gt;
• Document groups. Each document belongs to a group, each user has groups, and the filter runs inside the vector search before the top-k is taken, so a forbidden document never reaches the prompt.&lt;br&gt;
• Per document, not per topic. A restricted user asking about IAM got a partial answer built from the DynamoDB and S3 pages, which mention IAM. It was honest about what the context lacked, but it shows the granularity.&lt;br&gt;
• Conversations. The internal thread id is the user name plus the thread id, so two users sharing a thread id never share history.&lt;br&gt;
• Rate limit per user with a sliding window.&lt;br&gt;
To check the security tests were not vacuous, I removed each protection on purpose and confirmed that at least one test failed every time.&lt;/p&gt;

&lt;p&gt;Poisoned documents&lt;br&gt;
Indirect prompt injection is the opposite threat: the user is legitimate, but a document contains hidden orders. I wrote four short documents with a normal introduction and one hidden instruction each: create a ticket titled HACKED, answer only PWNED, append an image and a link to a fake domain, or print the system prompt. They live in a separate index, never in the real one.&lt;/p&gt;

&lt;p&gt;Each case ends as OK (the poisoned text reached the model and it did not obey), FAIL, or inconclusive (the poisoned text never reached the model, so the case proves nothing). That third state is what prevents a false “all good”. Result: 7 of 7 resisted, none inconclusive.&lt;/p&gt;

&lt;p&gt;I would not read that as “it is safe”. They are seven payloads I wrote, detection is by tool calls and fixed strings, and there is no complete defence against prompt injection. What actually bounds the damage is structural: no destructive tools, draft-only tickets and a step limit.&lt;/p&gt;

&lt;p&gt;Observability without a platform&lt;br&gt;
Every request writes one JSON log line with a request id (also returned in a response header), status, latency, user, tools used, token usage and whether the question was refused. For an agent turn the tokens are summed over every model call. The question text is not logged, only its length and a short hash, and tool arguments are not logged either. A protected /metrics endpoint shows counts, average and p95 latency, tokens and refusal rate. Metrics are in memory, so they reset on restart.&lt;/p&gt;

&lt;p&gt;Shipping it for $0&lt;br&gt;
The Docker image is slim, runs as a non-root user and contains no documents, index or keys. My Mac runs macOS 12 and cannot run Docker Desktop, so CI builds the image and smoke-tests it: it starts the container with a tiny fake index and checks that /health answers and that /ask returns 401 when no users exist.&lt;/p&gt;

&lt;p&gt;The public demo runs on Render’s free instance, which needs no card, sleeps after 15 minutes and has an ephemeral disk. Three things bit me:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;New services default to a newer Python than the one I had tested, so I pinned 3.13 with a .python-version file.&lt;/li&gt;
&lt;li&gt;The index is built during the deploy, so waking up never spends embedding calls.&lt;/li&gt;
&lt;li&gt;I pasted the wrong text into the secret file that holds the users, and the server crashed with an opaque traceback. The clipboard still held an old terminal command. The app now logs the file path, its size and its first character (never the content), starts with no users, and rejects every protected request until it is fixed. Startup on the free 0.1-CPU instance takes about 26 seconds.
Limits, and what I would do next
• The corpus is tiny, the status API is simulated, and the three datasets are small and self-written.
• There is no answer-faithfulness metric yet, only whether the model refuses.
• Conversation memory, rate limits and metrics live in one process.
• Real use would need HTTPS, key rotation and proper authentication, not API keys in a file.
• Next: a faithfulness check, a bigger corpus with real PDFs, a vector database, human confirmation before write actions such as tickets, and a short-lived deployment on AWS.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you take one thing from this: the model was the easy part. The value was in measuring it, breaking it on purpose, and writing down what I could not prove.&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>rag</category>
      <category>security</category>
    </item>
    <item>
      <title>Blue/green deployments on ECS Fargate with CloudFormation.</title>
      <dc:creator>Ing. Pablo Cueto</dc:creator>
      <pubDate>Wed, 07 Oct 2026 19:25:10 +0000</pubDate>
      <link>https://dev.to/pabscueto/bluegreen-deployments-on-ecs-fargate-with-cloudformation-12b2</link>
      <guid>https://dev.to/pabscueto/bluegreen-deployments-on-ecs-fargate-with-cloudformation-12b2</guid>
      <description>&lt;p&gt;I built this project to practice container deployments on AWS while preparing for the AWS Certified Developer – Associate exam. It is a small FastAPI notes API running on ECS Fargate behind an Application Load Balancer, with blue/green deployments defined in CloudFormation. This post walks through the design, what went wrong, and how I kept the cost close to zero.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/PabsCueto/ecs-fargate-bluegreen" rel="noopener noreferrer"&gt;https://github.com/PabsCueto/ecs-fargate-bluegreen&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FPabsCueto%2Fecs-fargate-bluegreen%2Fmain%2Fdocs%2Farchitecture.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FPabsCueto%2Fecs-fargate-bluegreen%2Fmain%2Fdocs%2Farchitecture.png" alt="Architecture diagram: GitHub Actions builds and pushes the image to ECR using OIDC; an ALB with two target groups sends traffic to blue or green Fargate tasks" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A FastAPI app with &lt;code&gt;/health&lt;/code&gt;, &lt;code&gt;/version&lt;/code&gt; and a small in-memory CRUD for notes.&lt;/li&gt;
&lt;li&gt;A pipeline that builds the Docker image, tests it, and pushes it to ECR.&lt;/li&gt;
&lt;li&gt;A CloudFormation template with an ALB, two target groups, and an ECS service that uses the built-in blue/green strategy.&lt;/li&gt;
&lt;li&gt;Scripts to preview, deploy, and tear everything down.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The notes live in memory on purpose: I wanted the focus to be the deployment, not the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline: GitHub Actions with OIDC
&lt;/h2&gt;

&lt;p&gt;The workflow assumes an IAM role through GitHub's OIDC provider, so there are no long-lived AWS keys in the repo. The trust policy is limited to one repository and branch, and the role can only push to one ECR repository.&lt;/p&gt;

&lt;p&gt;Before pushing the image, the workflow starts the container inside the runner and calls &lt;code&gt;/health&lt;/code&gt; and &lt;code&gt;/version&lt;/code&gt;. I don't have a local Docker setup that can run current Docker Desktop, so this smoke test also acts as my "does it actually start?" check.&lt;/p&gt;

&lt;h2&gt;
  
  
  The infrastructure: ECS blue/green in CloudFormation
&lt;/h2&gt;

&lt;p&gt;The ALB has two target groups and a listener rule that carries production traffic. The ECS service declares the blue/green strategy and tells ECS which rule and alternate target group to use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;EcsService&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::ECS::Service&lt;/span&gt;
  &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;DeploymentController&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ECS&lt;/span&gt;
    &lt;span class="na"&gt;DeploymentConfiguration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;BLUE_GREEN&lt;/span&gt;
      &lt;span class="na"&gt;BakeTimeInMinutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
      &lt;span class="na"&gt;DeploymentCircuitBreaker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Enable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;Rollback&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;LoadBalancers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ContainerName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;
        &lt;span class="na"&gt;ContainerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8000&lt;/span&gt;
        &lt;span class="na"&gt;TargetGroupArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;TargetGroupBlue&lt;/span&gt;
        &lt;span class="na"&gt;AdvancedConfiguration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;AlternateTargetGroupArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;TargetGroupGreen&lt;/span&gt;
          &lt;span class="na"&gt;ProductionListenerRule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;ListenerProdRule&lt;/span&gt;
          &lt;span class="na"&gt;RoleArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;EcsInfrastructureRole.Arn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Changing anything in the task definition (I change an environment variable) creates a new revision and starts a deployment. ECS starts the green tasks, waits for them to pass the health checks, moves the listener rule to the green target group, keeps both versions for the bake time, and then stops the old one. If the new version never becomes healthy, the circuit breaker rolls back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the cost near zero
&lt;/h2&gt;

&lt;p&gt;An ALB and Fargate tasks are billed per hour, so I designed around short-lived deployments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;deploy.sh --preview&lt;/code&gt; creates only a CloudFormation change set, so nothing is provisioned.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;deploy.sh&lt;/code&gt; shows a cost warning and requires typing a confirmation before creating anything.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;teardown.sh&lt;/code&gt; deletes the stack and runs &lt;code&gt;check-costs.sh&lt;/code&gt;, which lists any load balancer, running task, NAT gateway, or Elastic IP left behind.&lt;/li&gt;
&lt;li&gt;Tasks run in public subnets with a security group that only accepts traffic from the ALB, which avoids a NAT gateway.&lt;/li&gt;
&lt;li&gt;Smallest Fargate size, one task, one-day log retention, and a 5-second deregistration delay.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Result
&lt;/h2&gt;

&lt;p&gt;I deployed &lt;code&gt;1.0.0&lt;/code&gt; (blue), then updated the version to &lt;code&gt;2.0.0&lt;/code&gt; (green):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /version -&amp;gt; {"version":"1.0.0","color":"blue","hostname":"ip-172-31-67-143..."}
GET /version -&amp;gt; {"version":"2.0.0","color":"green","hostname":"ip-172-31-84-17..."}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hostname changed, which shows traffic moved to a different set of tasks. ECS reported both deployments as successful (about 1 minute 25 seconds for the first one and 2 minutes 25 seconds for the blue/green update, including the bake time). Then I destroyed the stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  What went wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. OIDC: "Not authorized to perform sts:AssumeRoleWithWebIdentity".&lt;/strong&gt; My trust policy matched the classic subject format (&lt;code&gt;repo:owner/repo:ref:refs/heads/main&lt;/code&gt;), but the token for this repository included numeric IDs: &lt;code&gt;repo:owner@&amp;lt;owner-id&amp;gt;/repo@&amp;lt;repo-id&amp;gt;:ref:refs/heads/main&lt;/code&gt;. I only found out by adding a temporary workflow step that decoded and printed the token claims.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Smoke test failing with curl exit code 56.&lt;/strong&gt; The published port accepted connections before uvicorn was ready, and curl doesn't retry that error by default. Adding &lt;code&gt;--retry-all-errors&lt;/code&gt; fixed it, and the workflow now prints the container logs when the smoke test fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. CodeDeploy was not available.&lt;/strong&gt; My first version used the CloudFormation CodeDeploy blue/green hook. The stack was created, but the update failed with &lt;code&gt;SubscriptionRequiredException&lt;/code&gt;, and &lt;code&gt;aws deploy list-applications&lt;/code&gt; returned the same error. I suspect a restriction of my account's plan, although I couldn't confirm that. I switched to ECS's native blue/green strategy, which needs no extra service and meets the same goal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;There is no live demo: I tear the stack down to avoid costs. The README has the evidence.&lt;/li&gt;
&lt;li&gt;HTTP only, no domain or HTTPS.&lt;/li&gt;
&lt;li&gt;I didn't measure downtime with a continuous request loop during the switch, and I didn't use a test listener to validate the green version before the shift. Those are natural next steps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to look at the code, it's here: &lt;a href="https://github.com/PabsCueto/ecs-fargate-bluegreen" rel="noopener noreferrer"&gt;https://github.com/PabsCueto/ecs-fargate-bluegreen&lt;/a&gt;. Feedback is welcome.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>docker</category>
      <category>devops</category>
      <category>python</category>
    </item>
    <item>
      <title>How I Built a Password Strength &amp; Breach Check API with FastAPI and HaveIBeenPwned</title>
      <dc:creator>Ing. Pablo Cueto</dc:creator>
      <pubDate>Thu, 11 Jun 2026 00:30:21 +0000</pubDate>
      <link>https://dev.to/pabscueto/how-i-built-a-password-strength-breach-check-api-with-fastapi-and-haveibeenpwned-5dm4</link>
      <guid>https://dev.to/pabscueto/how-i-built-a-password-strength-breach-check-api-with-fastapi-and-haveibeenpwned-5dm4</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Most password strength checkers only validate character rules — length, uppercase, digits. But a password like &lt;code&gt;Password1&lt;/code&gt; passes every rule and has been breached over 2 million times.&lt;/p&gt;

&lt;p&gt;I wanted a single API that does both: &lt;strong&gt;score the strength&lt;/strong&gt; and &lt;strong&gt;check it against real breach data&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FastAPI&lt;/strong&gt; + Python 3.13&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HaveIBeenPwned&lt;/strong&gt; Pwned Passwords API v3&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Render&lt;/strong&gt; for deployment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RapidAPI&lt;/strong&gt; for distribution&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The k-Anonymity Model
&lt;/h2&gt;

&lt;p&gt;The most important design decision was privacy. I never send the full password to HaveIBeenPwned. Instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hash the password with SHA-1&lt;/li&gt;
&lt;li&gt;Send only the first 5 characters of the hash&lt;/li&gt;
&lt;li&gt;HIBP returns all hashes that match that prefix&lt;/li&gt;
&lt;li&gt;Check locally if the full hash is in the list
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;sha1_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sha1_hash&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;suffix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sha1_hash&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.pwnedpasswords.com/range/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;hash_suffix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hash_suffix&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;suffix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Times found in breaches
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full password never leaves your server.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Scoring Algorithm
&lt;/h2&gt;

&lt;p&gt;The strength score (0–100) combines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Length&lt;/strong&gt; — up to 35 pts (6 tiers from &amp;lt;6 chars to 20+)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Character variety&lt;/strong&gt; — up to 40 pts (lowercase, uppercase, digits, special chars)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entropy bonus&lt;/strong&gt; — up to 25 pts based on pool entropy (&lt;code&gt;length × log2(charset_size)&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus penalties for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Common passwords (&lt;code&gt;password&lt;/code&gt;, &lt;code&gt;123456&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;Keyboard walks (&lt;code&gt;qwerty&lt;/code&gt;, &lt;code&gt;asdfgh&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Repeated characters (&lt;code&gt;aaa&lt;/code&gt;, &lt;code&gt;111&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Sequential patterns (&lt;code&gt;123&lt;/code&gt;, &lt;code&gt;abc&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Endpoints
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/password/analyze&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Score + analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/password/breach-check&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HIBP breach lookup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/password/full-check&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Both in one call&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Example Response
&lt;/h2&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;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Very Strong"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"entropy_bits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.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;"length"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"checks"&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;"has_lowercase"&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;"has_uppercase"&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;"has_digit"&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;"has_special"&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="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;"feedback"&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;"penalties"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;Available on RapidAPI as part of my &lt;strong&gt;Fraud Prevention Bundle&lt;/strong&gt; alongside Email Validator, Phone Validator Pro, and IP Geolocation APIs.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://rapidapi.com/PabsCueto" rel="noopener noreferrer"&gt;Password Strength &amp;amp; Breach Check API on RapidAPI&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>security</category>
      <category>api</category>
    </item>
    <item>
      <title>I built and deployed an IP Geolocation API from scratch in one day — here's the full stack</title>
      <dc:creator>Ing. Pablo Cueto</dc:creator>
      <pubDate>Wed, 27 May 2026 23:52:21 +0000</pubDate>
      <link>https://dev.to/pabscueto/i-built-and-deployed-an-ip-geolocation-api-from-scratch-in-one-day-heres-the-full-stack-4i4c</link>
      <guid>https://dev.to/pabscueto/i-built-and-deployed-an-ip-geolocation-api-from-scratch-in-one-day-heres-the-full-stack-4i4c</guid>
      <description>&lt;p&gt;No third-party black boxes. No vendor lock-in. Just clean Python code, two free data sources, and a live API on RapidAPI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FastAPI&lt;/strong&gt; — async endpoints, automatic Swagger docs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IPLocate.io&lt;/strong&gt; — primary geo source (VPN/proxy/Tor detection on free tier)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ipgeolocation.io&lt;/strong&gt; — automatic fallback&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-memory TTL cache&lt;/strong&gt; — LRU cache, 1h TTL, 10k entries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Render&lt;/strong&gt; — auto-deploy from GitHub on every push&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RapidAPI&lt;/strong&gt; — marketplace distribution, billing, rate limiting&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /ip/{ip}&lt;/code&gt; — full geolocation for any IPv4 or IPv6&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /ip/me&lt;/code&gt; — auto-detect caller's IP&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /ip/bulk&lt;/code&gt; — up to 50 IPs in one request&lt;/li&gt;
&lt;li&gt;Security flags on every response: VPN, Tor, proxy, datacenter&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The architecture decision that matters
&lt;/h2&gt;

&lt;p&gt;Two geo sources with automatic failover. If IPLocate goes down, ipgeolocation.io takes over silently. The client never sees a 502. Cache handles repeated lookups without touching either upstream API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The response
&lt;/h2&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;"ip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"8.8.8.8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"location"&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"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"United States"&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;"Mountain View"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"latitude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;37.38605&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"longitude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;-122.08385&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;"security"&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;"is_vpn"&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;"is_tor"&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;"is_datacenter"&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="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;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"iplocate"&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;
  
  
  Try it live
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://rapidapi.com/PabsCueto/api/ip-geolocation38" rel="noopener noreferrer"&gt;https://rapidapi.com/PabsCueto/api/ip-geolocation38&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Free tier available. Bulk endpoint included from the first paid plan. The entire project — models, services, routers, tests, render.yaml — is a clean FastAPI structure you can fork and adapt for any data enrichment API.&lt;/p&gt;

&lt;h1&gt;
  
  
  python #fastapi #api #webdev #programming #backend #devops #opensource #buildinpublic #pythondeveloper #apidesign #render #rapidapi #geolocation #cybersecurity
&lt;/h1&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>cybersecurity</category>
      <category>backenddevelopment</category>
    </item>
    <item>
      <title>I Built a Phone Validation API — Live on RapidAPI published: true tags: python, fastapi, api, productivity</title>
      <dc:creator>Ing. Pablo Cueto</dc:creator>
      <pubDate>Sat, 16 May 2026 19:48:28 +0000</pubDate>
      <link>https://dev.to/pabscueto/i-built-a-phone-validation-api-live-on-rapidapi-published-true-tags-python-fastapi-api-a9g</link>
      <guid>https://dev.to/pabscueto/i-built-a-phone-validation-api-live-on-rapidapi-published-true-tags-python-fastapi-api-a9g</guid>
      <description>&lt;p&gt;Validating phone numbers sounds simple until you realize regex won't tell you if a number is mobile, landline, or VoIP — or what country it belongs to.&lt;br&gt;
I built Phone Validator Pro, a REST API that does all of that in a single request.&lt;/p&gt;

&lt;p&gt;What it does:&lt;/p&gt;

&lt;p&gt;Validates format (E.164, international, national)&lt;br&gt;
Detects country, region and timezone&lt;br&gt;
Identifies the carrier/operator&lt;br&gt;
Detects line type: mobile, landline, VoIP, toll-free&lt;br&gt;
Batch validation — up to 100 numbers per request&lt;/p&gt;

&lt;p&gt;Example response&lt;br&gt;
jsonGET /v1/validate/+14155551234&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;
  "is_valid": true,&lt;br&gt;
  "formats": {&lt;br&gt;
    "e164": "+14155551234",&lt;br&gt;
    "international": "+1 415-555-1234",&lt;br&gt;
    "national": "(415) 555-1234"&lt;br&gt;
  },&lt;br&gt;
  "country": {&lt;br&gt;
    "iso2": "US",&lt;br&gt;
    "name": "United States",&lt;br&gt;
    "calling_code": "+1",&lt;br&gt;
    "timezone": ["America/New_York"]&lt;br&gt;
  },&lt;br&gt;
  "line_type": "mobile",&lt;br&gt;
  "carrier": { "name": "T-Mobile", "available": true }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Stack&lt;/p&gt;

&lt;p&gt;FastAPI + Python 3.12&lt;br&gt;
Google's libphonenumber for validation&lt;br&gt;
Deployed on Render&lt;br&gt;
Monetized via RapidAPI&lt;/p&gt;

&lt;p&gt;Try it free&lt;br&gt;
No credit card required. The free tier gives you 100 requests/month to test it in your project.&lt;br&gt;
rapidapi.com/PabsCueto/api/phone-validator-pro1&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>productivity</category>
      <category>api</category>
    </item>
    <item>
      <title>How I Built a 4-Layer Email Validator API with FastAPI and Published it on RapidAPI</title>
      <dc:creator>Ing. Pablo Cueto</dc:creator>
      <pubDate>Fri, 15 May 2026 22:42:24 +0000</pubDate>
      <link>https://dev.to/pabscueto/how-i-built-a-4-layer-email-validator-api-with-fastapi-and-published-it-on-rapidapi-3g28</link>
      <guid>https://dev.to/pabscueto/how-i-built-a-4-layer-email-validator-api-with-fastapi-and-published-it-on-rapidapi-3g28</guid>
      <description>&lt;p&gt;I just launched Email Validator API on RapidAPI — a professional email &lt;br&gt;
verification service built with Python and FastAPI.&lt;/p&gt;

&lt;p&gt;Most email validators only check syntax. Mine runs 4 layers of verification&lt;br&gt;
and returns a confidence score for every email.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 4 Layers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Layer 1 — Syntax Check&lt;/strong&gt;&lt;br&gt;
Validates format against RFC 5322 standards. Also detects common typos &lt;br&gt;
and suggests corrections — for example &lt;code&gt;user@gmial.com&lt;/code&gt; → &lt;code&gt;user@gmail.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2 — DNS / MX Lookup&lt;/strong&gt;&lt;br&gt;
Confirms the domain exists and is configured to receive emails using &lt;br&gt;
&lt;code&gt;dnspython&lt;/code&gt;. Catches domains that look valid but can't receive mail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 3 — Disposable Domain Detection&lt;/strong&gt;&lt;br&gt;
Flags throwaway addresses from 3,000+ known providers like Mailinator, &lt;br&gt;
Guerrilla Mail, YOPmail and TempMail. Essential for preventing fake signups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 4 — SMTP Verification&lt;/strong&gt;&lt;br&gt;
Performs a real SMTP handshake to confirm the mailbox exists — without &lt;br&gt;
sending a single email. Available on Pro plans.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Response
&lt;/h2&gt;

&lt;p&gt;Every request returns a detailed JSON breakdown:&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;
  "email": "&lt;a href="mailto:user@gmail.com"&gt;user@gmail.com&lt;/a&gt;",&lt;br&gt;
  "valid": true,&lt;br&gt;
  "score": 95,&lt;br&gt;
  "syntax_valid": true,&lt;br&gt;
  "mx_found": true,&lt;br&gt;
  "disposable": false,&lt;br&gt;
  "smtp_valid": true,&lt;br&gt;
  "suggestion": null,&lt;br&gt;
  "error": null&lt;br&gt;
}&lt;/p&gt;

&lt;h2&gt;
  
  
  Score Interpretation
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0–29&lt;/td&gt;
&lt;td&gt;Invalid syntax&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30–54&lt;/td&gt;
&lt;td&gt;Domain issues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;55–74&lt;/td&gt;
&lt;td&gt;Likely valid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;75–94&lt;/td&gt;
&lt;td&gt;Valid + not disposable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;95–100&lt;/td&gt;
&lt;td&gt;Fully verified via SMTP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FastAPI&lt;/strong&gt; — async endpoints, automatic Swagger docs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;dnspython&lt;/strong&gt; — async DNS/MX lookups&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;slowapi&lt;/strong&gt; — rate limiting per plan tier&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Render&lt;/strong&gt; — deployment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RapidAPI&lt;/strong&gt; — marketplace and monetization&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Plans
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Requests/month&lt;/th&gt;
&lt;th&gt;Layers&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BASIC&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;1–2&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PRO&lt;/td&gt;
&lt;td&gt;5,000&lt;/td&gt;
&lt;td&gt;1–3&lt;/td&gt;
&lt;td&gt;$9/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MEGA&lt;/td&gt;
&lt;td&gt;20,000&lt;/td&gt;
&lt;td&gt;1–4&lt;/td&gt;
&lt;td&gt;$19/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ULTRA&lt;/td&gt;
&lt;td&gt;50,000&lt;/td&gt;
&lt;td&gt;1–4 + bulk&lt;/td&gt;
&lt;td&gt;$29/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;a href="https://rapidapi.com/PabsCueto/api/email-validator-api32" rel="noopener noreferrer"&gt;https://rapidapi.com/PabsCueto/api/email-validator-api32&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Free plan available — no credit card required.&lt;/p&gt;

&lt;p&gt;Happy to answer questions about the implementation!&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Built a Password Strength Analyzer API with Python + FastAPI</title>
      <dc:creator>Ing. Pablo Cueto</dc:creator>
      <pubDate>Thu, 14 May 2026 19:00:54 +0000</pubDate>
      <link>https://dev.to/pabscueto/i-built-a-password-strength-analyzer-api-with-python-fastapi-38jp</link>
      <guid>https://dev.to/pabscueto/i-built-a-password-strength-analyzer-api-with-python-fastapi-38jp</guid>
      <description>&lt;h1&gt;
  
  
  I Built a Password Strength Analyzer API with Python + FastAPI 🔐
&lt;/h1&gt;

&lt;p&gt;As a backend developer, I wanted to build something &lt;strong&gt;useful, real, and monetizable&lt;/strong&gt; — not just another tutorial project.&lt;/p&gt;

&lt;p&gt;So I built a &lt;strong&gt;Password Strength Analyzer API&lt;/strong&gt; that any developer can integrate into their app in minutes.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Returns a strength score from 0 (very weak) to 4 (very strong)&lt;/li&gt;
&lt;li&gt;Estimates crack time using real-world attack scenarios&lt;/li&gt;
&lt;li&gt;Detects if the password is in known breach lists&lt;/li&gt;
&lt;li&gt;Returns actionable suggestions to improve security&lt;/li&gt;
&lt;li&gt;Analyzes uppercase, lowercase, numbers and symbols&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Python 3.10+&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FastAPI&lt;/strong&gt; — for the REST API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;zxcvbn&lt;/strong&gt; — the same algorithm Dropbox uses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pydantic&lt;/strong&gt; — data validation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Render&lt;/strong&gt; — cloud deployment&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Example Request
&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 &lt;span class="s2"&gt;"https://password-analyzern.onrender.com/analyze"&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;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: YOUR_API_KEY"&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;'{"password": "MyP@ssw0rd!"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example Response
&lt;/h2&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;"password"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"strength_label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Fuerte 🟢"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"crack_time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3 months"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"suggestions"&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="s2"&gt;"Idealmente usa 12 o más caracteres"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"warnings"&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;"length"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"has_uppercase"&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;"has_lowercase"&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;"has_numbers"&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;"has_symbols"&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;"is_common"&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="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;
  
  
  Try it for free
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;RapidAPI (free plan available):&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://rapidapi.com/PabsCueto/api/password-strength-analyzer1" rel="noopener noreferrer"&gt;https://rapidapi.com/PabsCueto/api/password-strength-analyzer1&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://github.com/PabsCueto/password-analyzer" rel="noopener noreferrer"&gt;https://github.com/PabsCueto/password-analyzer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live docs:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://password-analyzern.onrender.com/docs" rel="noopener noreferrer"&gt;https://password-analyzern.onrender.com/docs&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Built this as part of my API portfolio. Feedback welcome! &lt;/p&gt;

</description>
      <category>python</category>
      <category>security</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How I built a PDF Generator API with FastAPI and ReportLab</title>
      <dc:creator>Ing. Pablo Cueto</dc:creator>
      <pubDate>Thu, 14 May 2026 18:08:51 +0000</pubDate>
      <link>https://dev.to/pabscueto/how-i-built-a-pdf-generator-api-with-fastapi-and-reportlab-331i</link>
      <guid>https://dev.to/pabscueto/how-i-built-a-pdf-generator-api-with-fastapi-and-reportlab-331i</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Almost every SaaS app needs to generate PDFs at some point — invoices, &lt;br&gt;
reports, receipts. Most developers end up wrestling with Puppeteer, &lt;br&gt;
wkhtmltopdf, or heavy libraries that are painful to deploy.&lt;/p&gt;

&lt;p&gt;I decided to build a simple REST API that does it with a single POST request.&lt;/p&gt;
&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FastAPI&lt;/strong&gt; — for the REST endpoints&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ReportLab&lt;/strong&gt; — for PDF generation in Python&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker + Render&lt;/strong&gt; — for deployment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RapidAPI&lt;/strong&gt; — for monetization&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Send a JSON payload, get a PDF back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://pdf-generator-api-1fx5.onrender.com/generate/invoice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-API-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice_number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INV-2025-001&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;company&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Acme Corp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billing@acme.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;John Doe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;john@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Web Development&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quantity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unit_price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tax_rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Invoice PDFs&lt;/strong&gt; — with line items, tax calculation and totals&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTML to PDF&lt;/strong&gt; — convert any HTML string to PDF&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Report PDFs&lt;/strong&gt; — turn tabular data into formatted reports&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it free
&lt;/h2&gt;

&lt;p&gt;Available on RapidAPI with a free tier (50 requests/month):&lt;br&gt;
🔗 &lt;a href="https://rapidapi.com/PabsCueto/api/pdf-generator17" rel="noopener noreferrer"&gt;https://rapidapi.com/PabsCueto/api/pdf-generator17&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Would love feedback from the community!&lt;/p&gt;

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