<?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: yobox</title>
    <description>The latest articles on DEV Community by yobox (@yobox).</description>
    <link>https://dev.to/yobox</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%2F3981137%2F2f924e03-ddcd-497c-b085-cb7a2dd8dd03.png</url>
      <title>DEV Community: yobox</title>
      <link>https://dev.to/yobox</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yobox"/>
    <language>en</language>
    <item>
      <title>Why OTP Verification Fails (and How to Fix It)</title>
      <dc:creator>yobox</dc:creator>
      <pubDate>Wed, 12 Aug 2026 16:19:01 +0000</pubDate>
      <link>https://dev.to/yobox/why-otp-verification-fails-and-how-to-fix-it-2d0a</link>
      <guid>https://dev.to/yobox/why-otp-verification-fails-and-how-to-fix-it-2d0a</guid>
      <description>&lt;p&gt;You typed the code. You're sure you typed it right. The site says "Invalid or expired code." You request a new one. Same thing.&lt;/p&gt;

&lt;p&gt;OTP failures are one of the most frustrating UX patterns on the modern web, because they fail silently and the error messages are useless. Here are the seven real reasons OTP verification fails, in order of how often we actually see them in user reports and our own debugging.&lt;/p&gt;

&lt;h1&gt;
  
  
  1. The Code Already Expired
&lt;/h1&gt;

&lt;p&gt;OTPs typically live 5–10 minutes. By the time you opened the email, read the code, switched tabs, and pasted it, you may have burned 9 minutes of the window — especially if the email was delayed in transit.&lt;/p&gt;

&lt;p&gt;Fix: request a new code. Type it immediately. Don't go make coffee.&lt;/p&gt;

&lt;p&gt;Developer note: if you're building an OTP flow, surface the exact expiry time in the email and the form. "This code expires at 14:32 UTC" beats "expires in 10 minutes" by a mile.&lt;/p&gt;

&lt;h1&gt;
  
  
  2. The Code Was Already Used
&lt;/h1&gt;

&lt;p&gt;OTPs are typically single-use. If you mis-clicked Verify twice, or if a browser autofill submitted the form once and then again, the second attempt fails — even though the code is correct.&lt;/p&gt;

&lt;p&gt;Fix: request a new code. Don't double-click. Watch for autofill.&lt;/p&gt;

&lt;h1&gt;
  
  
  3. The Email Was Silently Rejected
&lt;/h1&gt;

&lt;p&gt;If you used a disposable email address, the site may have rejected your address at submission time without telling you. The form said "we sent a code"; nothing was actually sent.&lt;/p&gt;

&lt;p&gt;Fix: if you suspect this, try with an email alias instead of a disposable address. Many sites accept aliases while blocking disposable domains.&lt;/p&gt;

&lt;h1&gt;
  
  
  4. The Email Went to a Spam Folder You Can't See
&lt;/h1&gt;

&lt;p&gt;For temp mail users, this is silent death — temp inboxes don't have spam folders, so a filtered message is just gone. For real inbox users, it's a 30-second hunt.&lt;/p&gt;

&lt;p&gt;Fix: check spam in your real inbox. For temp mail, regenerate the address and try again from a different provider. See "Receive OTP Codes with Temp Mail".&lt;/p&gt;

&lt;h1&gt;
  
  
  5. You're Verifying With the Wrong Address
&lt;/h1&gt;

&lt;p&gt;Some flows let you request OTPs for any address you type, then verify by typing the address plus the code. If you typed the address slightly differently the second time (capitalization, trailing space, alias variant), the code won't match.&lt;/p&gt;

&lt;p&gt;Fix: copy-paste both the address and the code. Never retype.&lt;/p&gt;

&lt;h1&gt;
  
  
  6. The Site's Clock Is Wrong
&lt;/h1&gt;

&lt;p&gt;This is rare on modern infrastructure but still happens — especially on self-hosted apps and small SaaS. If the server's clock is skewed by more than the OTP TTL, valid codes appear expired.&lt;/p&gt;

&lt;p&gt;Fix: nothing you can do from outside. Email the site's support.&lt;/p&gt;

&lt;h1&gt;
  
  
  7. Rate Limiting
&lt;/h1&gt;

&lt;p&gt;If you've requested 5 codes in 10 minutes, most senders silently stop delivering. The code "didn't arrive" because the system refused to send it.&lt;/p&gt;

&lt;p&gt;Fix: wait 10 minutes. Don't spam the "resend code" button.&lt;/p&gt;

&lt;h1&gt;
  
  
  How to Diagnose, in Order
&lt;/h1&gt;

&lt;p&gt;If your OTP is failing, walk this list:&lt;/p&gt;

&lt;p&gt;Did the email actually arrive? Check the timestamp.&lt;br&gt;
Are you within the expiry window?&lt;br&gt;
Have you tried this exact code before?&lt;br&gt;
Is the email address you're verifying with exactly the one the code was sent to?&lt;br&gt;
Did you request more than 3 codes in the last 10 minutes?&lt;br&gt;
Are you using a disposable address that the site silently rejects?&lt;br&gt;
Most failures resolve at step 1 or 2.&lt;/p&gt;
&lt;h1&gt;
  
  
  OTP Best Practices for Developers
&lt;/h1&gt;

&lt;p&gt;If you're building an OTP flow, these are the patterns that hurt users least:&lt;/p&gt;

&lt;p&gt;Use 6 digits, not 4. 4 digits is brute-forceable in seconds.&lt;br&gt;
Set TTL to 10 minutes, not 60 seconds. A minute is hostile.&lt;br&gt;
Allow at least 5 attempts per code. Typos happen.&lt;br&gt;
Rate-limit at 3 codes per 10 minutes. Enough to handle real users, blocks abuse.&lt;br&gt;
Don't invalidate the code on a typo. Invalidate after 5 failed attempts or on success.&lt;br&gt;
Show the expiry time in the form. Not just "expires soon."&lt;br&gt;
Send the code in the subject line. Mobile users see it without opening the email.&lt;br&gt;
Echo the requesting IP / device in the email. Helps users spot account-takeover attempts.&lt;br&gt;
For testing your own OTP flow, the YoBox Temp Mail tool gives you a disposable inbox in under a second; pair it with the Webhook Tester if your verify endpoint also fires backend webhooks (some auth providers send a "user.verified" webhook you'll want to assert on).&lt;/p&gt;
&lt;h1&gt;
  
  
  A Minimal Test Loop
&lt;/h1&gt;

&lt;p&gt;If you're QA'ing OTPs in CI, this is the pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;signup with OTP&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tempMail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createInbox&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/signup&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[name=email]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text=Send code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tempMail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pollForCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b(\d{6})\b&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[name=otp]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text=Verify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full walkthrough in "Email Testing Guide for Developers".&lt;/p&gt;

&lt;h1&gt;
  
  
  OTP vs Magic Links
&lt;/h1&gt;

&lt;p&gt;A close cousin: magic links. Same idea (one-time token in email), different UX (you click instead of type). The failure modes are nearly identical, with one addition: magic links break when opened in a different browser than the one that requested them. If you request the link on desktop and open it on mobile, many implementations refuse.&lt;/p&gt;

&lt;p&gt;For testing, YoBox Temp Mail handles both — you can extract a 6-digit code with a regex or fetch the link and follow it programmatically.&lt;/p&gt;

&lt;h1&gt;
  
  
  FAQ
&lt;/h1&gt;

&lt;p&gt;Why does Discord say my OTP is invalid even when it's right?&lt;br&gt;
Most often: you used a disposable address Discord silently rejected; the email never arrived. See "Temporary Email for Discord".&lt;/p&gt;

&lt;p&gt;Why does the same code work in Chrome but not Safari?&lt;br&gt;
Likely a cookie / session mismatch. The code is tied to a session ID stored in a cookie; switching browsers breaks the link.&lt;/p&gt;

&lt;p&gt;Can I extend the OTP expiry?&lt;br&gt;
Not as a user. As a developer, yes — but anything over 15 minutes hurts security.&lt;/p&gt;

&lt;p&gt;Why does my OTP arrive twice?&lt;br&gt;
Some senders retry on transient failures. Use the first code; the second is usually the same value resent.&lt;/p&gt;

&lt;p&gt;Is SMS OTP more reliable than email OTP?&lt;br&gt;
Generally yes for delivery speed (SMS is often instant), but SMS has its own failure modes (SIM swap, carrier filtering) and is more expensive for the sender.&lt;/p&gt;

&lt;h1&gt;
  
  
  Bottom Line
&lt;/h1&gt;

&lt;p&gt;OTPs fail for boring reasons more often than exciting ones — expiry, double-clicks, blocklisted disposable domains, rate limits. Walk the diagnostic list before assuming the site is broken. If you're building OTP flows, optimize for human typos and slow inboxes, not the happy path.&lt;/p&gt;

&lt;h1&gt;
  
  
  YoBox Team
&lt;/h1&gt;

&lt;p&gt;Builder behind YoBox — a privacy-first toolbox for developers and QA engineers covering disposable email, webhook capture, regex, secure passwords, Docker, and end-to-end testing.&lt;/p&gt;

</description>
      <category>authentication</category>
      <category>security</category>
      <category>javascript</category>
      <category>testing</category>
    </item>
    <item>
      <title>Temporary Email for Developers: The Definitive Guide</title>
      <dc:creator>yobox</dc:creator>
      <pubDate>Sun, 09 Aug 2026 16:53:19 +0000</pubDate>
      <link>https://dev.to/yobox/temporary-email-for-developers-the-definitive-guide-2nop</link>
      <guid>https://dev.to/yobox/temporary-email-for-developers-the-definitive-guide-2nop</guid>
      <description>&lt;p&gt;Most articles about temp mail are written for people who want to dodge marketing emails. This one isn't. This is the developer's guide to disposable email — how it fits into your workflow, the patterns that scale, and the specific tools that pair with the rest of a modern stack.&lt;/p&gt;

&lt;p&gt;By the end you'll know exactly when to reach for a disposable inbox, how to plug it into CI, how to pair it with webhook testing, and how to keep your real inbox sane while you build.&lt;/p&gt;

&lt;h1&gt;
  
  
  What Developers Actually Use Temp Mail For
&lt;/h1&gt;

&lt;p&gt;The use cases that matter for working developers:&lt;/p&gt;

&lt;p&gt;Testing your own signup flows. Don't sign up to your app 400 times with your real Gmail.&lt;br&gt;
CI-driven E2E tests. Cypress, Playwright, or Postman tests that need to receive a real OTP.&lt;br&gt;
OAuth provider testing. New OAuth provider integrations need test accounts on those providers.&lt;br&gt;
Beta product evaluation. You want to try a SaaS product without putting it on your real email forever.&lt;br&gt;
Disposable accounts for sketchy services. That npm package's signup wall, that Stack Overflow clone, that AI tool with a 7-day trial.&lt;br&gt;
Bot / service accounts. A CI service that needs its own identity.&lt;br&gt;
Verifying email-required signup walls. Reading a Reddit thread, downloading a PDF.&lt;br&gt;
For everything in this list, disposable email is the right tool. For things not in this list (your own production accounts, anything with billing, anything you want to recover), use an alias or real email — see "Disposable Email vs Real Email vs Aliases".&lt;/p&gt;

&lt;h1&gt;
  
  
  The Developer Toolkit
&lt;/h1&gt;

&lt;p&gt;The four tools every developer should have set up:&lt;/p&gt;

&lt;p&gt;Tool    Use for&lt;br&gt;
Real email  Your identity, work, billing, recovery&lt;br&gt;
Email alias service (SimpleLogin / Apple Hide My Email) Per-service permanent addresses&lt;br&gt;
YoBox Temp Mail One-off testing, CI, throwaway signups&lt;br&gt;
Local SMTP catcher (Mailhog / MailCatcher)  Local dev where email shouldn't leave the machine&lt;br&gt;
Each has a clear role. Mixing them up is how you end up with 12 password resets for the same forum in your work inbox.&lt;/p&gt;

&lt;h1&gt;
  
  
  Pattern 1: Testing Your Own Signup Flow
&lt;/h1&gt;

&lt;p&gt;Your app sends an OTP at signup. You want to test the flow without using your personal email.&lt;/p&gt;

&lt;p&gt;The pattern:&lt;/p&gt;

&lt;p&gt;Open YoBox Temp Mail in a new tab.&lt;br&gt;
Copy the address.&lt;br&gt;
Submit your signup form with it.&lt;br&gt;
Read the OTP from the disposable inbox.&lt;br&gt;
Paste back.&lt;br&gt;
Inspect the resulting user state.&lt;br&gt;
For a manual debug session, this is the fastest possible loop. For automated tests, see pattern 2.&lt;/p&gt;

&lt;h1&gt;
  
  
  Pattern 2: E2E Tests in CI
&lt;/h1&gt;

&lt;p&gt;For CI, you want the same loop fully scripted. The YoBox Temp Mail tool exposes a JSON API designed for this:&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;ts&lt;br&gt;
// Create inbox&lt;br&gt;
const inbox = await fetch('https://yobox.dev/mail/account', {&lt;br&gt;
method: 'POST',&lt;br&gt;
headers: { 'content-type': 'application/json' },&lt;br&gt;
body: JSON.stringify({&lt;br&gt;
address: test-${Date.now()}@yobox-test.dev&lt;/code&gt;,&lt;br&gt;
password: crypto.randomUUID(),&lt;br&gt;
}),&lt;br&gt;
}).then(r =&amp;gt; r.json());&lt;/p&gt;

&lt;p&gt;// Trigger signup&lt;br&gt;
await fetch('&lt;a href="https://myapp.com/signup" rel="noopener noreferrer"&gt;https://myapp.com/signup&lt;/a&gt;', {&lt;br&gt;
method: 'POST',&lt;br&gt;
headers: { 'content-type': 'application/json' },&lt;br&gt;
body: JSON.stringify({ email: inbox.address }),&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;// Poll for message&lt;br&gt;
const msg = await pollForEmail(inbox.token, 30_000);&lt;br&gt;
const code = msg.text.match(/\b(\d{6})\b/)?.[1];&lt;/p&gt;

&lt;p&gt;// Verify&lt;br&gt;
await fetch('&lt;a href="https://myapp.com/verify" rel="noopener noreferrer"&gt;https://myapp.com/verify&lt;/a&gt;', {&lt;br&gt;
method: 'POST',&lt;br&gt;
headers: { 'content-type': 'application/json' },&lt;br&gt;
body: JSON.stringify({ email: inbox.address, code }),&lt;br&gt;
});&lt;br&gt;
`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Full Cypress and Playwright versions in "How to Test Email Flows Without a Real Inbox".&lt;/p&gt;

&lt;h1&gt;
  
  
  Pattern 3: Pair with Webhook Capture
&lt;/h1&gt;

&lt;p&gt;Many signup flows fire backend webhooks alongside the email — Stripe's customer.created, your own user.verified, Mailgun's delivery events. To test the full async loop, pair Temp Mail with the Webhook Tester:&lt;/p&gt;

&lt;p&gt;Generate a disposable inbox.&lt;br&gt;
Generate a webhook capture URL.&lt;br&gt;
Configure your app to use both.&lt;br&gt;
Trigger the signup.&lt;br&gt;
Assert on the OTP email arriving.&lt;br&gt;
Assert on the downstream webhook firing.&lt;br&gt;
This is how you catch bugs like "the email goes out but the webhook never fires because the after-hooks crashed silently."&lt;/p&gt;

&lt;p&gt;See "Cypress E2E with YoBox Disposable Email and Webhook Tester".&lt;/p&gt;

&lt;h1&gt;
  
  
  Pattern 4: OAuth Provider Testing
&lt;/h1&gt;

&lt;p&gt;You're integrating Sign in with GitHub. Your test needs a fresh GitHub account.&lt;/p&gt;

&lt;p&gt;Honest answer: don't use disposable email for this. GitHub blocks it. Use an alias for a long-term test account. See "Temporary Email for GitHub".&lt;/p&gt;

&lt;p&gt;For providers that do allow disposable email (some smaller OAuth providers, niche identity services), YoBox Temp Mail works fine — same flow as the signup pattern.&lt;/p&gt;

&lt;h1&gt;
  
  
  Pattern 5: Beta / Trial Accounts
&lt;/h1&gt;

&lt;p&gt;You want to try a new product. You don't want them having your real email forever. Three options:&lt;/p&gt;

&lt;p&gt;Alias for products you might actually adopt.&lt;br&gt;
Disposable for products you'll evaluate once.&lt;br&gt;
Real email if the product needs to talk to you long-term (billing, support).&lt;br&gt;
When in doubt, alias. Disposable for true one-offs.&lt;/p&gt;

&lt;h1&gt;
  
  
  Pattern 6: Bot / Service Accounts
&lt;/h1&gt;

&lt;p&gt;You need an account for a CI process to use. Two clean options:&lt;/p&gt;

&lt;p&gt;Custom-domain alias&lt;br&gt;
Set up &lt;a href="mailto:ci-bot@yourcompany.dev"&gt;ci-bot@yourcompany.dev&lt;/a&gt;. Forwards to a real address. Survives forever. This is the right answer for any long-lived bot.&lt;/p&gt;

&lt;p&gt;YoBox + persistence&lt;br&gt;
For short-lived CI accounts (per-test isolation), generate a fresh YoBox inbox per test run. Don't try to make it long-lived.&lt;/p&gt;

&lt;h1&gt;
  
  
  Reducing Inbox Spam
&lt;/h1&gt;

&lt;p&gt;A side benefit of using disposable email aggressively: your real inbox stays clean. The compound effect over 5 years is the difference between an inbox that takes 10 minutes to triage daily and one that takes 60.&lt;/p&gt;

&lt;p&gt;The rule that works: default to disposable for everything that isn't tier 1 (bank, work, government, password manager). Use aliases for anything you'd want to receive mail from. Reserve your real email for the small set of accounts that matter most.&lt;/p&gt;

&lt;h1&gt;
  
  
  What to Build Into Your Dotfiles
&lt;/h1&gt;

&lt;p&gt;If you do a lot of email testing, automate the boring parts:&lt;/p&gt;

&lt;p&gt;A shell function mail-temp that hits the YoBox API and copies a fresh address to your clipboard.&lt;br&gt;
A Cypress / Playwright command (cy.disposableEmail()) that returns an inbox object.&lt;br&gt;
A snippet in your test fixture library that polls for OTPs with a regex.&lt;br&gt;
A Slack / Discord shortcut that opens YoBox Temp Mail and the Webhook Tester side by side.&lt;br&gt;
Once these are muscle memory, email testing stops being friction.&lt;/p&gt;

&lt;h1&gt;
  
  
  Security Reminders
&lt;/h1&gt;

&lt;p&gt;Even for developers, temp mail isn't a security tool:&lt;/p&gt;

&lt;p&gt;Inboxes are addressable. Anyone who knows the address can read it (modulo token-based auth).&lt;br&gt;
Don't reuse addresses for unrelated accounts. Password reset cross-pollution is a real risk.&lt;br&gt;
Don't put production secrets through temp mail. It's for fake test users, not real credentials.&lt;br&gt;
Rotate addresses per test run in CI. Avoid shared state.&lt;/p&gt;

&lt;h1&gt;
  
  
  FAQ
&lt;/h1&gt;

&lt;p&gt;Can I use temp mail for my own production app's emails?&lt;br&gt;
No. Production accounts need persistent inboxes. Use temp mail to test the email flow, not as the actual receiver for real users.&lt;/p&gt;

&lt;p&gt;How do I avoid hitting rate limits?&lt;br&gt;
Generate fresh addresses per test. Don't reuse one address for 50 sequential signups — that's what triggers sender-side abuse heuristics.&lt;/p&gt;

&lt;p&gt;Does YoBox Temp Mail work in CI / GitHub Actions?&lt;br&gt;
Yes — no auth, no captcha, no rate limit on normal use. Designed for CI.&lt;/p&gt;

&lt;p&gt;Can I receive attachments?&lt;br&gt;
The API exposes message bodies; attachments are visible in the browser UI but not currently programmatically downloadable.&lt;/p&gt;

&lt;p&gt;Is it safe to use disposable email for OAuth testing?&lt;br&gt;
Yes for providers that accept it (most small / mid). No for providers that block it (GitHub, Google, Discord). See provider-specific posts.&lt;/p&gt;

&lt;h1&gt;
  
  
  Bottom Line
&lt;/h1&gt;

&lt;p&gt;Temp mail is one of the most underrated developer tools. Used right, it cleans up your real inbox, accelerates your test loop, and pairs with webhook capture for full async coverage of complex flows. The YoBox Temp Mail tool was built specifically for developer workflows — fast, scriptable, no nonsense. Add it to your toolkit and stop signing up to your own app with &lt;a href="mailto:yourname+test001@gmail.com"&gt;yourname+test001@gmail.com&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  YoBox Team
&lt;/h1&gt;

&lt;p&gt;Builder behind YoBox — a privacy-first toolbox for developers and QA engineers covering disposable email, webhook capture, regex, secure passwords, Docker, and end-to-end testing.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>security</category>
      <category>testing</category>
    </item>
    <item>
      <title>Testing API Responses Seamlessly with Postman and YoBox</title>
      <dc:creator>yobox</dc:creator>
      <pubDate>Wed, 05 Aug 2026 17:23:38 +0000</pubDate>
      <link>https://dev.to/yobox/testing-api-responses-seamlessly-with-postman-and-yobox-548l</link>
      <guid>https://dev.to/yobox/testing-api-responses-seamlessly-with-postman-and-yobox-548l</guid>
      <description>&lt;p&gt;Postman is great at one thing: firing a request and asserting on the response body. It's not great at the moment immediately after, when your API quietly sends an email, queues a job, or POSTs to a partner webhook. That side-effect surface is where most production bugs hide, and where most Postman collections silently stop testing.&lt;/p&gt;

&lt;p&gt;YoBox closes the gap. A disposable inbox you can poll over HTTP. A webhook URL that records everything. Both work natively with pm.sendRequest and Newman, no SDK required.&lt;/p&gt;

&lt;p&gt;This guide walks through the patterns we use for serious Postman + YoBox API testing.&lt;/p&gt;

&lt;h1&gt;
  
  
  The mental model
&lt;/h1&gt;

&lt;p&gt;Your API does three kinds of work in response to a request:&lt;/p&gt;

&lt;p&gt;Returns a body — Postman already tests this.&lt;br&gt;
Sends an email — YoBox Temp Mail tests this.&lt;br&gt;
Hits an outbound webhook — YoBox Webhook Tester tests this.&lt;br&gt;
A complete API assertion covers all three.&lt;/p&gt;
&lt;h1&gt;
  
  
  Setting up the environment
&lt;/h1&gt;

&lt;p&gt;Create two collection variables:&lt;/p&gt;

&lt;p&gt;yoboxBase → &lt;a href="https://yobox.dev/api" rel="noopener noreferrer"&gt;https://yobox.dev/api&lt;/a&gt;&lt;br&gt;
apiBase → your service URL&lt;br&gt;
Add a startup folder with two requests:&lt;/p&gt;

&lt;p&gt;POST {{yoboxBase}}/mail/new&lt;br&gt;
POST {{yoboxBase}}/hooks/new&lt;br&gt;
Each saves id and address / url into collection variables. Every subsequent request can reference them.&lt;/p&gt;
&lt;h1&gt;
  
  
  Asserting response shape
&lt;/h1&gt;

&lt;p&gt;Postman's Tests tab handles the basics:&lt;/p&gt;

&lt;p&gt;pm.test("status 201", () =&amp;gt; pm.response.to.have.status(201));&lt;br&gt;
pm.test("returns user id", () =&amp;gt; {&lt;br&gt;
  const body = pm.response.json();&lt;br&gt;
  pm.expect(body.id).to.match(/^[0-9a-f-]{36}$/);&lt;br&gt;
  pm.expect(body.email).to.eql(pm.collectionVariables.get("inboxAddress"));&lt;br&gt;
});&lt;br&gt;
Use the Regex Patterns cheat sheet for common assertions (UUID, JWT, ISO timestamps, currency).&lt;/p&gt;
&lt;h1&gt;
  
  
  Asserting email side effects
&lt;/h1&gt;

&lt;p&gt;The email arrives after the request returns. Wait for it in a pre-request script on the next request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;collectionVariables&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inboxId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;collectionVariables&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yoboxBase&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;/mail/&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;/messages, &lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;err, res&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt; =&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;collectionVariables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;otp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b\d{6}\b&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;collectionVariables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;emailSubject&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Email timeout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then assert the captured values:&lt;/p&gt;

&lt;p&gt;pm.test("welcome email arrived", () =&amp;gt; {&lt;br&gt;
  pm.expect(pm.collectionVariables.get("emailSubject")).to.include("Welcome");&lt;br&gt;
  pm.expect(pm.collectionVariables.get("otp")).to.match(/^\d{6}$/);&lt;br&gt;
});&lt;/p&gt;

&lt;h1&gt;
  
  
  Asserting webhook side effects
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In the Tests tab of the trigger request&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hookId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;collectionVariables&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hookId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;collectionVariables&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yoboxBase&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;/hooks/&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;hookId&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;webhook fired&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;be&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;above&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payload shape&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invoice.paid&lt;/span&gt;&lt;span class="dl"&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="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Newman in CI, prefer a dedicated assertion request after the trigger so the timing is deterministic.&lt;/p&gt;

&lt;h1&gt;
  
  
  Comparison table
&lt;/h1&gt;

&lt;p&gt;Test target Postman alone   Postman + YoBox&lt;br&gt;
Response body shape Yes Yes&lt;br&gt;
Response timing Yes Yes&lt;br&gt;
Email delivery  No  Yes&lt;br&gt;
Email content   No  Yes&lt;br&gt;
Webhook delivery    No  Yes&lt;br&gt;
Webhook payload shape   No  Yes&lt;/p&gt;

&lt;h1&gt;
  
  
  Running in CI
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;run: npx newman run collection.json -e env.json --reporters cli,junit
env:
YOBOX_BASE: &lt;a href="https://yobox.dev/api" rel="noopener noreferrer"&gt;https://yobox.dev/api&lt;/a&gt;
Newman supports pre-request scripts and pm.sendRequest exactly like Postman, so the patterns above run unchanged.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Pairs with
&lt;/h1&gt;

&lt;p&gt;Cypress + YoBox for browser-side flows that depend on API responses.&lt;br&gt;
Playwright + YoBox for cross-browser API + UI tests.&lt;br&gt;
Realistic Mock Data for body fixtures.&lt;br&gt;
Password Generator for credentials.&lt;/p&gt;

&lt;h1&gt;
  
  
  Common pitfalls
&lt;/h1&gt;

&lt;p&gt;Trusting a 200 as proof of side effects. A 200 says the request was accepted, not that the side effect ran.&lt;br&gt;
Polling too fast. 1.5 s is the right interval for email; 500 ms is the right interval for in-process webhooks.&lt;br&gt;
Forgetting environments. Use Postman environments for apiBase so the same collection runs against staging and production.&lt;br&gt;
Asserting on HTML email bodies. Always parse the plain-text part.&lt;/p&gt;

&lt;h1&gt;
  
  
  FAQ
&lt;/h1&gt;

&lt;p&gt;Can I test gRPC or GraphQL?&lt;br&gt;
Yes — Postman supports both. The YoBox plumbing is identical because it's just HTTP polling.&lt;/p&gt;

&lt;p&gt;Does Postman's Flows feature work with YoBox?&lt;br&gt;
Yes — model the wait-for-email step as a delay + HTTP request node.&lt;/p&gt;

&lt;p&gt;How do I share a collection without leaking the YoBox URL?&lt;br&gt;
Use an environment variable, not a hard-coded base.&lt;/p&gt;

&lt;p&gt;What about file attachments?&lt;br&gt;
The messages endpoint returns attachment metadata; download via a follow-up request.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;A Postman collection that only asserts on response bodies tests half your API. Wire YoBox into pre-request scripts and the Tests tab, and the same collection now verifies emails and webhooks too — in Postman, in Newman, in CI. Two fixtures' worth of code, dramatically more coverage.&lt;/p&gt;

&lt;p&gt;Further reading: The Complete Postman Guide, Cypress + YoBox, Regex Patterns Every QA Engineer Should Memorize.&lt;/p&gt;

&lt;h1&gt;
  
  
  Advanced: contract testing with YoBox webhooks
&lt;/h1&gt;

&lt;p&gt;Treat the YoBox-captured payload as the source of truth for your partner's contract. Snapshot the JSON shape on a green build and fail subsequent runs that drift from it.&lt;/p&gt;

&lt;h1&gt;
  
  
  Advanced: response time SLAs
&lt;/h1&gt;

&lt;p&gt;Pair \pm.expect(pm.response.responseTime).to.be.below(500)\ with a YoBox-verified side effect to assert both that the API was fast and that the work actually happened.&lt;/p&gt;

&lt;h1&gt;
  
  
  Migration: from manual to monitored
&lt;/h1&gt;

&lt;p&gt;Postman Monitors run collections on a schedule. Once your collection asserts emails and webhooks via YoBox, you can promote the same collection into a monitor and get continuous production verification for free.&lt;/p&gt;

&lt;h1&gt;
  
  
  Reporting
&lt;/h1&gt;

&lt;p&gt;Newman's JUnit output drops cleanly into any CI dashboard. YoBox-backed assertions look identical to response-body assertions in the report, so there's no new vocabulary for QA to learn.&lt;/p&gt;

&lt;h1&gt;
  
  
  A deeper Postman workflow
&lt;/h1&gt;

&lt;p&gt;Postman is famous for one-off requests, but the real value shows up when you treat collections like code: versioned, reviewed, and runnable in CI. The trick is to combine Postman's environment variables with YoBox's ephemeral primitives so every run is hermetic.&lt;/p&gt;

&lt;p&gt;Environment design&lt;br&gt;
Create three environments — local, staging, and ci. Each carries:&lt;/p&gt;

&lt;p&gt;baseUrl — the API under test&lt;br&gt;
hookId — refreshed per run from Webhook Tester&lt;br&gt;
tempEmail — refreshed per run from Temp Mail&lt;br&gt;
runId — a UUID generated in a pre-request script&lt;br&gt;
// Collection-level pre-request&lt;br&gt;
if (!pm.environment.get("runId")) {&lt;br&gt;
  pm.environment.set("runId", crypto.randomUUID());&lt;br&gt;
}&lt;br&gt;
Chaining requests&lt;br&gt;
Postman's request chaining lets you treat a multi-step flow — signup → verify email → create resource → wait for webhook — as a single test artifact. Each step writes to collection variables that downstream steps consume.&lt;/p&gt;

&lt;p&gt;// After "create resource"&lt;br&gt;
const id = pm.response.json().id;&lt;br&gt;
pm.collectionVariables.set("resourceId", id);&lt;br&gt;
pm.test("resource created", () =&amp;gt; pm.expect(id).to.be.a("string"));&lt;br&gt;
Async assertions&lt;br&gt;
Async webhooks are the bane of API testing. Polling the YoBox Webhook Tester from a Postman test gives you a deterministic wait without sleeping arbitrary durations.&lt;/p&gt;

&lt;p&gt;const url = &lt;code&gt;https://yobox.dev/api/hooks/${pm.environment.get("hookId")}&lt;/code&gt;;&lt;br&gt;
const deadline = Date.now() + 15000;&lt;br&gt;
(function poll() {&lt;br&gt;
  pm.sendRequest(url, (err, res) =&amp;gt; {&lt;br&gt;
    const hits = res &amp;amp;&amp;amp; res.json().requests || [];&lt;br&gt;
    if (hits.length &amp;gt; 0) {&lt;br&gt;
      pm.test("webhook received", () =&amp;gt; pm.expect(hits[0].method).to.eql("POST"));&lt;br&gt;
    } else if (Date.now() &amp;lt; deadline) {&lt;br&gt;
      setTimeout(poll, 500);&lt;br&gt;
    } else {&lt;br&gt;
      pm.test("webhook received", () =&amp;gt; pm.expect.fail("timeout"));&lt;br&gt;
    }&lt;br&gt;
  });&lt;br&gt;
})();&lt;/p&gt;

&lt;h1&gt;
  
  
  Postman + Newman in CI/CD
&lt;/h1&gt;

&lt;p&gt;Newman is Postman's CLI runner and the bridge between Postman the IDE and your pipeline. A typical GitHub Actions job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;name: API contract tests
run: |
npx newman run ./postman/collection.json \
  -e ./postman/env.ci.json \
  --reporters cli,junit \
  --reporter-junit-export junit.xml
Combine that with the Docker builder pattern for reproducible runners that include Newman pre-installed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Comparison: Postman vs. alternatives
&lt;/h1&gt;

&lt;p&gt;Tool    GUI CLI Async webhooks  Best for&lt;br&gt;
Postman Yes Newman  Manual polling  Collaborative API exploration&lt;br&gt;
Insomnia    Yes inso    Plugin needed   Lean, scriptable workflows&lt;br&gt;
Hurl    No  hurl    Limited Plain-text, git-friendly tests&lt;br&gt;
Bruno   Yes bru Manual  Offline-first, file-based specs&lt;br&gt;
k6  No  k6  Yes Load + functional combined runs&lt;br&gt;
The "right" tool depends on team shape. If your QA engineers live in a GUI and your devs live in a terminal, Postman + Newman bridges both.&lt;/p&gt;

&lt;h1&gt;
  
  
  Troubleshooting
&lt;/h1&gt;

&lt;p&gt;My webhook never arrives.&lt;br&gt;
Inspect outbound calls from your service with a packet log or your provider's delivery dashboard. The most common cause is a wrong URL pasted into an environment variable.&lt;/p&gt;

&lt;p&gt;Tests pass locally and fail in CI.&lt;br&gt;
Almost always an environment file mismatch. Use newman run ... --env-var key=value to override per-run instead of editing committed JSON.&lt;/p&gt;

&lt;p&gt;Postman scripts time out.&lt;br&gt;
Default request timeout is 0 (no timeout). For polling loops set a hard deadline in script as shown above.&lt;/p&gt;

&lt;h1&gt;
  
  
  FAQ
&lt;/h1&gt;

&lt;p&gt;Can Postman replace Cypress or Playwright?&lt;br&gt;
No. Postman covers the API surface; Cypress and Playwright cover the browser. They complement each other — API tests are fast and exhaustive, UI tests are slow and selective.&lt;/p&gt;

&lt;p&gt;Does Newman support parallel runs?&lt;br&gt;
Not natively. Use newman-run-parallel or run multiple Newman processes in your CI matrix, each with a distinct collection slice.&lt;/p&gt;

&lt;p&gt;How do I share a Postman collection without leaking secrets?&lt;br&gt;
Export the collection but never the environment. Commit the collection JSON, share a sanitized env.example.json, and let each developer create their own env.local.json ignored by git.&lt;/p&gt;

&lt;p&gt;Is there a free alternative for Postman Cloud?&lt;br&gt;
The Postman desktop app is free for collections and Newman runs. Cloud features like Mock Server and Monitor are paid; for those, YoBox's Webhook Tester covers most ad-hoc needs at zero cost.&lt;/p&gt;

</description>
      <category>postman</category>
      <category>api</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>After months of building, testing, and refining, I'm excited to launch **YoBox** on Product Hunt!</title>
      <dc:creator>yobox</dc:creator>
      <pubDate>Mon, 03 Aug 2026 16:44:00 +0000</pubDate>
      <link>https://dev.to/yobox/after-months-of-building-testing-and-refining-im-excited-to-launch-yobox-on-product-hunt-5dl</link>
      <guid>https://dev.to/yobox/after-months-of-building-testing-and-refining-im-excited-to-launch-yobox-on-product-hunt-5dl</guid>
      <description>&lt;p&gt;🚀 Hey developers!&lt;/p&gt;

&lt;p&gt;After months of building, testing, and refining, I'm excited to launch &lt;strong&gt;YoBox&lt;/strong&gt; on Product Hunt!&lt;/p&gt;

&lt;p&gt;YoBox is an all-in-one developer toolkit designed to save time by bringing the tools you use every day into one clean workspace. No ads overload, no unnecessary complexity—just fast, practical tools that help you get work done.&lt;/p&gt;

&lt;p&gt;✨ Some of the available tools include:&lt;br&gt;
• Temporary Email&lt;br&gt;
• Webhook Tester&lt;br&gt;
• Docker Compose Builder&lt;br&gt;
• JSON Formatter &amp;amp; Validator&lt;br&gt;
• Regex Tools&lt;br&gt;
• Password Generator&lt;br&gt;
• Image &amp;amp; File Utilities&lt;br&gt;
• And many more...&lt;/p&gt;

&lt;p&gt;I'd be honored if you could join the launch as a &lt;strong&gt;Maker&lt;/strong&gt; and support the project.&lt;/p&gt;

&lt;p&gt;👉 Accept the invitation here:&lt;br&gt;
&lt;a href="https://www.producthunt.com/posts/yobox-developer-tools-email/maker-invite?code=QN4gDN" rel="noopener noreferrer"&gt;https://www.producthunt.com/posts/yobox-developer-tools-email/maker-invite?code=QN4gDN&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every maker, upvote, comment, and piece of feedback helps improve YoBox and reach more developers around the world.&lt;/p&gt;

&lt;p&gt;Thank you so much for your support! ❤️&lt;/p&gt;

&lt;h1&gt;
  
  
  ProductHunt #BuildInPublic #IndieHackers #WebDevelopment #DeveloperTools #Programming #Coding #OpenSource #SaaS #Webhooks #Docker #JSON #Regex #Productivity #DevCommunity #LaunchDay #Startups #Tech
&lt;/h1&gt;

</description>
    </item>
    <item>
      <title>Stripe and PayPal Webhook Testing: A Practical Guide</title>
      <dc:creator>yobox</dc:creator>
      <pubDate>Fri, 31 Jul 2026 16:44:35 +0000</pubDate>
      <link>https://dev.to/yobox/stripe-and-paypal-webhook-testing-a-practical-guide-4ph7</link>
      <guid>https://dev.to/yobox/stripe-and-paypal-webhook-testing-a-practical-guide-4ph7</guid>
      <description>&lt;p&gt;Payment webhooks are the highest-stakes webhooks you'll ever wire up. A missed payment_intent.succeeded means a customer paid and didn't get their thing. A double-processed charge.refunded means you accidentally refunded twice. The bugs hide until they don't.&lt;/p&gt;

&lt;p&gt;This is the practical guide to testing Stripe and PayPal webhooks end-to-end — from inspecting the raw payloads to verifying signatures, simulating failures, and asserting on the full async flow.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Three Layers of Webhook Testing
&lt;/h1&gt;

&lt;p&gt;For payment webhooks specifically:&lt;/p&gt;

&lt;p&gt;Layer   What you're testing How&lt;br&gt;
Inspection  What does the provider actually send?   YoBox Webhook Tester, Stripe CLI&lt;br&gt;
Handler logic   Does my code do the right thing on a known payload? Unit tests with fixture payloads&lt;br&gt;
End-to-end  Does the full flow work, including signature, retries, idempotency? Integration tests against test mode&lt;br&gt;
Skip any layer and bugs leak.&lt;/p&gt;
&lt;h1&gt;
  
  
  Layer 1: Inspect the Payloads
&lt;/h1&gt;

&lt;p&gt;Before writing the handler, see what the provider sends. The docs are usually right; "usually" is a problem when money's involved.&lt;/p&gt;

&lt;p&gt;Stripe&lt;br&gt;
Use the YoBox Webhook Tester:&lt;/p&gt;

&lt;p&gt;Generate a capture URL in YoBox.&lt;br&gt;
In the Stripe dashboard → Developers → Webhooks → Add endpoint.&lt;br&gt;
Paste the YoBox URL. Select the events you care about.&lt;br&gt;
Use the Stripe CLI to trigger test events: stripe trigger payment_intent.succeeded.&lt;br&gt;
Watch the capture log fill in.&lt;br&gt;
You'll see the full payload, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stripe-Signature header (the HMAC you'll verify)&lt;/li&gt;
&lt;li&gt;Stripe-Version header&lt;/li&gt;
&lt;li&gt;JSON body with type, data.object, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Save a few payloads as test fixtures. You'll want them for unit tests.&lt;/p&gt;

&lt;p&gt;PayPal&lt;br&gt;
Same flow, but PayPal's UI is more cumbersome:&lt;/p&gt;

&lt;p&gt;Generate a YoBox capture URL.&lt;br&gt;
In the PayPal Developer dashboard → My Apps &amp;amp; Credentials → your app → "Add webhook."&lt;br&gt;
Paste the URL. Select events.&lt;br&gt;
Use the simulator: Webhook Simulator → choose event type → enter your URL.&lt;br&gt;
Inspect in YoBox.&lt;br&gt;
PayPal payloads are noisier than Stripe's. The signature scheme is also more complex (involves cert chain verification, not just HMAC). Capture early, save fixtures.&lt;/p&gt;
&lt;h1&gt;
  
  
  Layer 2: Unit Test the Handler
&lt;/h1&gt;

&lt;p&gt;With fixtures in hand, write unit tests that hit your handler directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;paymentSucceeded&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./fixtures/stripe-payment-intent-succeeded.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;handles payment_intent.succeeded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;handleStripeWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paymentSucceeded&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;processed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;markPaid&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalledWith&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
&lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;paymentSucceeded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;paymentSucceeded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&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="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;is idempotent on duplicate event&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;handleStripeWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paymentSucceeded&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;second&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;handleStripeWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paymentSucceeded&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;second&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;skipped-duplicate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;markPaid&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalledTimes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rejects bad signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="nf"&gt;handleStripeWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paymentSucceeded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bogus&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;rejects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toThrow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/signature/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run on every PR.&lt;/p&gt;

&lt;h1&gt;
  
  
  Layer 3: End-to-End in Test Mode
&lt;/h1&gt;

&lt;p&gt;The most important layer and the one teams most often skip. The flow:&lt;/p&gt;

&lt;p&gt;Spin up your app pointed at Stripe (or PayPal) test mode.&lt;br&gt;
Configure the webhook endpoint to be your app's real handler URL (via ngrok in dev, your staging URL in CI).&lt;br&gt;
Create a real test charge with the Stripe CLI or PayPal sandbox.&lt;br&gt;
Assert on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The webhook arrived&lt;/li&gt;
&lt;li&gt;Your handler returned 2xx within timeout&lt;/li&gt;
&lt;li&gt;The database state is correct&lt;/li&gt;
&lt;li&gt;Any downstream side effects fired
Using the Stripe CLI in CI
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Forward webhooks to localhost while running tests&lt;/span&gt;
stripe listen &lt;span class="nt"&gt;--forward-to&lt;/span&gt; http://localhost:3000/webhooks/stripe &amp;amp;

&lt;span class="c"&gt;#Trigger a known event stripe trigger payment_intent.succeeded&lt;/span&gt;
Now your &lt;span class="nb"&gt;test &lt;/span&gt;asserts on the resulting state &lt;span class="sb"&gt;```&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;% endraw %&lt;span class="o"&gt;}&lt;/span&gt;

The Stripe CLI handles signature verification and gives you a known webhook secret you can use &lt;span class="k"&gt;in &lt;/span&gt;tests.

Pairing with YoBox &lt;span class="k"&gt;for &lt;/span&gt;capture verification
Sometimes you want to assert that your app sent a webhook downstream — &lt;span class="k"&gt;for &lt;/span&gt;example, a notification to a downstream service after the payment processed. Point that downstream URL at the YoBox Webhook Tester &lt;span class="k"&gt;in &lt;/span&gt;&lt;span class="nb"&gt;test &lt;/span&gt;mode:
&lt;span class="o"&gt;{&lt;/span&gt;% raw %&lt;span class="o"&gt;}&lt;/span&gt;


&lt;span class="sb"&gt;```&lt;/span&gt;ts
&lt;span class="nb"&gt;test&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'after payment, notifies fulfillment service'&lt;/span&gt;, async &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
const captureUrl &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'https://yobox.dev/webhook/'&lt;/span&gt; + crypto.randomUUID&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
await setFulfillmentWebhookUrl&lt;span class="o"&gt;(&lt;/span&gt;captureUrl&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

await triggerStripePaymentInTestMode&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

const captured &lt;span class="o"&gt;=&lt;/span&gt; await pollForWebhook&lt;span class="o"&gt;(&lt;/span&gt;captureUrl, 30_000&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
expect&lt;span class="o"&gt;(&lt;/span&gt;captured.body.event&lt;span class="o"&gt;)&lt;/span&gt;.toBe&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'order.paid'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
expect&lt;span class="o"&gt;(&lt;/span&gt;captured.body.amount&lt;span class="o"&gt;)&lt;/span&gt;.toBeGreaterThan&lt;span class="o"&gt;(&lt;/span&gt;0&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;})&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h1&gt;
  
  
  Signature Verification: Stripe
&lt;/h1&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Stripe&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Stripe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE_SECRET_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe-signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// raw body, not parsed&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constructEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Idempotency check&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;wasProcessed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Process&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;processStripeEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;markProcessed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Three things to never skip:&lt;/p&gt;

&lt;p&gt;Use the raw body. Body parsing destroys the signature.&lt;br&gt;
Catch and 400 on invalid signature. Don't 500 (the provider will retry).&lt;br&gt;
Idempotency by event ID. Stripe explicitly recommends this.&lt;/p&gt;
&lt;h1&gt;
  
  
  Signature Verification: PayPal
&lt;/h1&gt;

&lt;p&gt;PayPal is more involved. You need to fetch their cert and verify the signature chain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;verifyPayPalWebhook&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./paypal-verify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paypal-transmission-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paypal-transmission-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paypal-transmission-time&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paypal-transmission-time&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paypal-transmission-sig&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paypal-transmission-sig&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paypal-cert-url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paypal-cert-url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paypal-auth-algo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paypal-auth-algo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;verifyPayPalWebhook&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="na"&gt;webhookId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PAYPAL_WEBHOOK_ID&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// ... process&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use PayPal's official SDK if at all possible — implementing the verification yourself is error-prone.&lt;/p&gt;

&lt;h1&gt;
  
  
  Common Stripe Webhook Bugs to Test For
&lt;/h1&gt;

&lt;p&gt;Missing idempotency. Same payment_intent.succeeded arrives twice, you mark the order paid twice and ship twice.&lt;br&gt;
Out-of-order events. charge.refunded arrives before charge.succeeded. Your handler crashes on missing parent.&lt;br&gt;
Test events processed in production. Always check event.livemode.&lt;br&gt;
Body parsed before signature verified. Signature fails 100% of the time.&lt;br&gt;
Slow 2xx. Stripe retries; you process duplicates.&lt;br&gt;
Treating payment_intent.processing as success. It's not. Wait for succeeded.&lt;br&gt;
Refund webhooks not handled. Customer disputes a charge, your DB still shows it paid.&lt;/p&gt;

&lt;h1&gt;
  
  
  Common PayPal Webhook Bugs
&lt;/h1&gt;

&lt;p&gt;Signature verification skipped because "it's hard." Don't. Use the SDK.&lt;br&gt;
Event types not mapped to product events. PayPal sends PAYMENT.CAPTURE.COMPLETED for what Stripe calls payment_intent.succeeded. Different vocabulary, same concept.&lt;br&gt;
Sandbox vs live confusion. Both modes use similar URLs; check the credentials, not the URL.&lt;br&gt;
Multiple webhook subscriptions firing same event twice. Audit your subscription list.&lt;/p&gt;

&lt;h1&gt;
  
  
  What to Test Manually
&lt;/h1&gt;

&lt;p&gt;Some tests are easier to run manually in test mode than to automate:&lt;/p&gt;

&lt;p&gt;Card decline flows. Trigger with Stripe's 4000000000000002 test card.&lt;br&gt;
3DS / SCA flows. Trigger with 4000002500003155.&lt;br&gt;
Dispute creation. Stripe lets you simulate disputes in the dashboard.&lt;br&gt;
PayPal sandbox checkout. Use sandbox buyer accounts.&lt;br&gt;
For each, capture the full webhook sequence in YoBox and turn into a test fixture.&lt;/p&gt;

&lt;h1&gt;
  
  
  End-to-End with Email
&lt;/h1&gt;

&lt;p&gt;Most payment flows also send an email to the customer (receipt, confirmation). To test the full flow:&lt;/p&gt;

&lt;p&gt;Sign up a test user with a YoBox Temp Mail address.&lt;br&gt;
Trigger payment in Stripe test mode.&lt;br&gt;
Wait for the receipt email in the temp inbox.&lt;br&gt;
Wait for the webhook to fire (capture in YoBox Webhook Tester or your real handler).&lt;br&gt;
Assert on database state, email body, and webhook payload.&lt;br&gt;
See "Email Testing Guide for Developers" and "Webhook Testing Complete Guide" for the patterns.&lt;/p&gt;

&lt;h1&gt;
  
  
  FAQ
&lt;/h1&gt;

&lt;p&gt;Can I test Stripe webhooks without ngrok?&lt;br&gt;
Yes — use the Stripe CLI's stripe listen for local, or deploy to a staging URL. For inspection, YoBox Webhook Tester.&lt;/p&gt;

&lt;p&gt;Why is my Stripe signature always failing?&lt;br&gt;
99% of the time: body is being parsed before verification. Use raw body.&lt;/p&gt;

&lt;p&gt;Does PayPal sign webhooks?&lt;br&gt;
Yes — it's just more complex than Stripe's HMAC. Use the official SDK.&lt;/p&gt;

&lt;p&gt;Should I unit test or integration test payment webhooks?&lt;br&gt;
Both. Unit tests with fixtures for handler logic; integration tests in Stripe test mode for the full flow.&lt;/p&gt;

&lt;p&gt;Can I replay a Stripe webhook?&lt;br&gt;
Yes — from the Stripe dashboard webhook log, click "Resend." Useful for replaying after fixing handler bugs.&lt;/p&gt;

&lt;h1&gt;
  
  
  Bottom Line
&lt;/h1&gt;

&lt;p&gt;Payment webhooks deserve more test coverage than they usually get. Inspect the real payloads with YoBox Webhook Tester, save fixtures, unit test the handler, end-to-end test in provider test mode, and never skip signature verification or idempotency. The cost of getting this right once is way lower than the cost of refunding 1,000 customers because of a duplicate-event bug.&lt;/p&gt;

&lt;h1&gt;
  
  
  YoBox Team
&lt;/h1&gt;

&lt;p&gt;Builder behind YoBox — a privacy-first toolbox for developers and QA engineers covering disposable email, webhook capture, regex, secure passwords, Docker, and end-to-end testing.&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>paypal</category>
      <category>webhooks</category>
      <category>api</category>
    </item>
    <item>
      <title>The Only docker-compose.yml Pattern You Need (2026 Edition)</title>
      <dc:creator>yobox</dc:creator>
      <pubDate>Wed, 29 Jul 2026 16:33:08 +0000</pubDate>
      <link>https://dev.to/yobox/the-only-docker-composeyml-pattern-you-need-2026-edition-1abo</link>
      <guid>https://dev.to/yobox/the-only-docker-composeyml-pattern-you-need-2026-edition-1abo</guid>
      <description>&lt;p&gt;There are two kinds of docker-compose.yml files in the world: the one a developer writes in twenty minutes that works for exactly one machine, and the one a team refactors fourteen times before settling on a pattern that survives both local dev and CI. This article gives you the second one, fully formed, with the reasoning behind each section.&lt;/p&gt;

&lt;p&gt;The YoBox Docker Builder scaffolds it; this guide explains why the lines exist.&lt;/p&gt;

&lt;h1&gt;
  
  
  The five jobs a good compose file does
&lt;/h1&gt;

&lt;p&gt;Bring the app up locally with a single command.&lt;br&gt;
Reproduce CI exactly, no surprises.&lt;br&gt;
Compose nicely with sidecars (databases, caches, e2e runners).&lt;br&gt;
Survive partial failures with health checks and explicit dependencies.&lt;br&gt;
Stay readable when a new engineer opens it on day one.&lt;/p&gt;

&lt;h1&gt;
  
  
  The pattern
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;

&lt;span class="na"&gt;x-defaults&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;defaults&lt;/span&gt;
&lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;span class="na"&gt;init&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;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*defaults&lt;/span&gt;
&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
&lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
&lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
&lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;db-data:/var/lib/postgresql/data"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;app&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;app"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
&lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
&lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;

&lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*defaults&lt;/span&gt;
&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7-alpine&lt;/span&gt;
&lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis-cli"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ping"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
&lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
&lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;

&lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*defaults&lt;/span&gt;
&lt;span class="na"&gt;build&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;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://app:app@db:5432/app&lt;/span&gt;
&lt;span class="na"&gt;REDIS_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis://redis:6379&lt;/span&gt;
&lt;span class="na"&gt;YOBOX&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://yobox.dev/api&lt;/span&gt;
&lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;service_healthy&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;service_healthy&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:3000"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wget"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-qO-"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:3000/health"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
&lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
&lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;

&lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*defaults&lt;/span&gt;
&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./web&lt;/span&gt;
&lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;API_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://api:3000&lt;/span&gt;
&lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;service_healthy&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:80"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;e2e&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;profiles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./tests&lt;/span&gt;
&lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;BASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://web:80&lt;/span&gt;
&lt;span class="na"&gt;YOBOX&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://yobox.dev/api&lt;/span&gt;
&lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;service_started&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;service_healthy&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;db-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Eighty lines that handle local dev, CI e2e, sidecar databases, health-checked dependencies, and a profile gate so docker compose up doesn't pull in the test runner unless you ask.&lt;/p&gt;

&lt;h1&gt;
  
  
  Why each section matters
&lt;/h1&gt;

&lt;p&gt;x-defaults anchor&lt;br&gt;
YAML anchors are the single most underused feature of compose. They eliminate the restart / init boilerplate that otherwise drifts between services.&lt;/p&gt;

&lt;p&gt;Health checks on everything&lt;br&gt;
depends_on: [db] without condition: service_healthy only guarantees the container started — not that Postgres is accepting connections. The retries-with-interval pattern above prevents the race condition that makes 5% of your CI runs go red.&lt;/p&gt;

&lt;p&gt;Profiles for test-only services&lt;br&gt;
profiles: ["test"] keeps e2e out of normal up invocations. Run with docker compose --profile test up.&lt;/p&gt;

&lt;p&gt;YoBox over the public network&lt;br&gt;
The e2e service hits &lt;a href="https://yobox.dev/api" rel="noopener noreferrer"&gt;https://yobox.dev/api&lt;/a&gt; directly. That keeps the compose file simple — no mail server, no webhook tunnel, no extra sidecar. The same compose file runs identically on a laptop, in GitHub Actions, and on a self-hosted runner.&lt;/p&gt;

&lt;h1&gt;
  
  
  Local dev workflow
&lt;/h1&gt;

&lt;p&gt;docker compose up -d&lt;br&gt;
docker compose logs -f api&lt;br&gt;
When you change application code in a bind-mounted volume, restart just one service:&lt;/p&gt;

&lt;h1&gt;
  
  
  CI workflow
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;run: docker compose --profile test up --build --abort-on-container-exit --exit-code-from e2e
--abort-on-container-exit stops everything as soon as e2e finishes. --exit-code-from e2e propagates the test runner's exit code to the workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Comparison: common patterns
&lt;/h1&gt;

&lt;p&gt;Pattern Pros    Cons&lt;br&gt;
One compose, one machine    Trivial Doesn't survive CI&lt;br&gt;
Multiple compose files (-f) Maximum flexibility Easy to drift, hard to onboard&lt;br&gt;
Profiles + healthchecks (this)  One file, two modes, deterministic deps Slightly more upfront thought&lt;/p&gt;

&lt;h1&gt;
  
  
  Pairs with
&lt;/h1&gt;

&lt;p&gt;Docker Builder for Cypress &amp;amp; Playwright CI for the test-runner Dockerfile.&lt;br&gt;
Cypress + YoBox and Playwright + YoBox for the test patterns running inside e2e.&lt;/p&gt;

&lt;h1&gt;
  
  
  Common pitfalls
&lt;/h1&gt;

&lt;p&gt;No healthchecks. Race conditions on slow CI runners are the most common e2e flake.&lt;br&gt;
Bind-mounting node_modules. Native modules differ between host and container — keep node_modules inside the image.&lt;br&gt;
Forgetting init: true. Without it, Node child processes don't get reaped, leading to zombie PIDs in long runs.&lt;br&gt;
Putting secrets in the compose file. Use env_file: and a .gitignored .env.&lt;/p&gt;

&lt;h1&gt;
  
  
  FAQ
&lt;/h1&gt;

&lt;p&gt;Should I use docker-compose (v1) or docker compose (v2)?&lt;br&gt;
v2. It's the supported version and the syntax above targets it.&lt;/p&gt;

&lt;p&gt;Do I need a version: key at the top?&lt;br&gt;
No — it's been ignored since v2. Leaving it out is cleaner.&lt;/p&gt;

&lt;p&gt;Can I run this on ARM?&lt;br&gt;
All the images above publish multi-arch tags. Yes.&lt;/p&gt;

&lt;p&gt;Where do volumes go in production?&lt;br&gt;
This pattern is for dev and CI. Production uses managed Postgres, managed Redis, and Kubernetes or Fly — not compose.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;One compose file. Two modes (up and --profile test up). Healthchecks on everything, anchors for shared defaults, and YoBox over the public internet so your e2e suite has real inboxes and real webhook capture without any sidecar gymnastics. Generate the starting skeleton from the Docker Builder and customize from there.&lt;/p&gt;

&lt;p&gt;See also: Docker Builder for Cypress &amp;amp; Playwright CI, Cypress + YoBox, Playwright + YoBox.&lt;/p&gt;

&lt;h1&gt;
  
  
  Advanced: \develop.watch\ for hot reload
&lt;/h1&gt;

&lt;p&gt;Compose v2.22+ ships \develop.watch\ — file-system events that sync source into a container and trigger a restart. Replaces \docker compose up\ plus a \nodemon\ sidecar with a single declarative block.&lt;/p&gt;

&lt;h1&gt;
  
  
  Resource limits in CI
&lt;/h1&gt;

&lt;p&gt;Add \deploy.resources.limits\ to keep noisy services from starving the e2e runner. Especially important on GHA runners where 7 GB is the hard ceiling.&lt;/p&gt;

&lt;h1&gt;
  
  
  Secrets
&lt;/h1&gt;

&lt;p&gt;Use Docker secrets for anything sensitive. They mount as files inside \/run/secrets\ rather than appearing in \docker inspect.&lt;/p&gt;

&lt;h1&gt;
  
  
  Migration: from a sprawling Makefile
&lt;/h1&gt;

&lt;p&gt;Most teams arrive at a clean compose file by replacing a 200-line Makefile. The transition is mechanical: each Make target becomes either a service, a profile, or a \run --rm\ invocation.&lt;/p&gt;

&lt;h1&gt;
  
  
  CI invocation reference
&lt;/h1&gt;

&lt;p&gt;\docker compose --profile test up --build --abort-on-container-exit --exit-code-from e2e\ is the one-liner. Add --quiet-pull\ in CI to suppress noise.&lt;/p&gt;

&lt;h1&gt;
  
  
  The pattern, restated
&lt;/h1&gt;

&lt;p&gt;One docker-compose.yml per repo. Services named after what they do, not where they came from. Volumes for state, networks only when you need isolation, and a .env file for everything that differs between developers. That's it. Anything more is premature.&lt;/p&gt;

&lt;p&gt;services:&lt;br&gt;
  app:&lt;br&gt;
    build: .&lt;br&gt;
    env_file: .env&lt;br&gt;
    ports: ["3000:3000"]&lt;br&gt;
    depends_on: [db, redis]&lt;br&gt;
  db:&lt;br&gt;
    image: postgres:16-alpine&lt;br&gt;
    environment:&lt;br&gt;
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}&lt;br&gt;
    volumes: ["pgdata:/var/lib/postgresql/data"]&lt;br&gt;
  redis:&lt;br&gt;
    image: redis:7-alpine&lt;br&gt;
volumes:&lt;br&gt;
  pgdata:&lt;br&gt;
Five services, three lines of volumes, zero networks. If a new contributor can't run docker compose up and get a working app, the file is wrong, not them.&lt;/p&gt;

&lt;h1&gt;
  
  
  Profiles for optional services
&lt;/h1&gt;

&lt;p&gt;Compose profiles let you keep heavy or optional services in the same file without forcing every developer to run them.&lt;/p&gt;

&lt;p&gt;services:&lt;br&gt;
  mailhog:&lt;br&gt;
    image: mailhog/mailhog&lt;br&gt;
    profiles: ["email"]&lt;br&gt;
  jaeger:&lt;br&gt;
    image: jaegertracing/all-in-one&lt;br&gt;
    profiles: ["tracing"]&lt;br&gt;
docker compose --profile email up starts MailHog only when needed. For real inbox testing without MailHog, point your app at YoBox Temp Mail and skip the local SMTP stack entirely.&lt;/p&gt;

&lt;h1&gt;
  
  
  Healthchecks that actually matter
&lt;/h1&gt;

&lt;p&gt;depends_on only waits for container start, not readiness. Add a healthcheck so app waits for Postgres to accept connections:&lt;/p&gt;

&lt;p&gt;db:&lt;br&gt;
  healthcheck:&lt;br&gt;
    test: ["CMD-SHELL", "pg_isready -U postgres"]&lt;br&gt;
    interval: 2s&lt;br&gt;
    timeout: 2s&lt;br&gt;
    retries: 20&lt;br&gt;
app:&lt;br&gt;
  depends_on:&lt;br&gt;
    db: { condition: service_healthy }&lt;/p&gt;

&lt;h1&gt;
  
  
  Bind mounts vs. named volumes
&lt;/h1&gt;

&lt;p&gt;Use case    Mount type  Why&lt;br&gt;
Source code (live reload)   Bind    Edit on host, run in container&lt;br&gt;
Database files  Named volume    Docker manages permissions and lifecycle&lt;br&gt;
Shared between dev + CI Named volume    Same image, no host-path surprises&lt;br&gt;
One-off seed data   Bind (:ro)  Reproducible, version-controlled&lt;br&gt;
The biggest source of "works on my machine" is bind-mounting a directory that contains files created by the container as root.&lt;/p&gt;

&lt;h1&gt;
  
  
  Production parity
&lt;/h1&gt;

&lt;p&gt;The same compose file should not be your production deployment. The pattern is:&lt;/p&gt;

&lt;p&gt;docker-compose.yml — local dev, with bind mounts and dev images.&lt;br&gt;
docker-compose.ci.yml — overrides for CI: no bind mounts, deterministic seeds.&lt;br&gt;
Production runs on Kubernetes, ECS, Fly, or Render — translated from compose by hand or with tools like kompose.&lt;br&gt;
Trying to make compose your prod runtime is the road to a custom orchestrator built out of bash.&lt;/p&gt;

&lt;h1&gt;
  
  
  CI usage
&lt;/h1&gt;

&lt;p&gt;In GitHub Actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;run: docker compose -f docker-compose.yml -f docker-compose.ci.yml up -d&lt;/li&gt;
&lt;li&gt;run: docker compose exec -T app npm test&lt;/li&gt;
&lt;li&gt;run: docker compose down -v
Pair with the Docker builder for Cypress and Playwright CI when you need browsers inside the same compose graph.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Troubleshooting
&lt;/h1&gt;

&lt;p&gt;Port already in use.&lt;br&gt;
Either another compose stack is running (docker compose ls) or a host process owns the port. Change the host side: "3001:3000".&lt;/p&gt;

&lt;p&gt;Database loses data between runs.&lt;br&gt;
You're using a bind mount on a path that gets cleaned, or you're running docker compose down -v. Use named volumes and avoid -v unless you mean it.&lt;/p&gt;

&lt;p&gt;Image rebuilds take forever.&lt;br&gt;
Add a .dockerignore to exclude node_modules, .git, and build artifacts. Order Dockerfile instructions so dependencies are cached above source.&lt;/p&gt;

&lt;h1&gt;
  
  
  FAQ
&lt;/h1&gt;

&lt;p&gt;Should I use Docker Compose v1 or v2?&lt;br&gt;
v2 — it's the official Docker CLI plugin. docker compose (space, not dash) is the current command.&lt;/p&gt;

&lt;p&gt;Can I run multiple compose files together?&lt;br&gt;
Yes, with -f base.yml -f override.yml. Later files override earlier ones key-by-key.&lt;/p&gt;

&lt;p&gt;How do secrets work in compose?&lt;br&gt;
Use env_file for dev, secret managers for prod. Don't commit .env. The secrets: block exists but is mostly relevant for Swarm.&lt;/p&gt;

&lt;p&gt;Is compose dead?&lt;br&gt;
No. It remains the best tool for local dev parity. Kubernetes is for production; compose is for the developer loop.&lt;/p&gt;

&lt;p&gt;How does this fit with the YoBox Docker Builder?&lt;br&gt;
The Docker Builder generates the kind of opinionated Dockerfile that plugs into the compose pattern above — node/python/go templates, multistage builds, and a sane default user.&lt;/p&gt;

&lt;h1&gt;
  
  
  YoBox Team
&lt;/h1&gt;

&lt;p&gt;Builder behind YoBox — a privacy-first toolbox for developers and QA engineers covering disposable email, webhook capture, regex, secure passwords, Docker, and end-to-end testing.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Built a Temp Mail &amp; Webhook Tool — Reddit Instantly Thought I Was a Spammer</title>
      <dc:creator>yobox</dc:creator>
      <pubDate>Mon, 27 Jul 2026 16:21:32 +0000</pubDate>
      <link>https://dev.to/yobox/i-built-a-temp-mail-webhook-tool-reddit-instantly-thought-i-was-a-spammer-17aa</link>
      <guid>https://dev.to/yobox/i-built-a-temp-mail-webhook-tool-reddit-instantly-thought-i-was-a-spammer-17aa</guid>
      <description>&lt;p&gt;I spent the better part of two weeks living on caffeine, error logs, and the kind of optimism only a solo developer at 3 AM can produce. The goal was simple, or at least it sounded simple when I wrote it on a sticky note: build a clean, fast, free developer toolbox. No signups. No dark patterns. No "upgrade to Pro to copy your own output."&lt;/p&gt;

&lt;p&gt;I called it YoBox — a single home for the small tools developers and QA engineers reach for every day: disposable email, webhook capture, Docker Compose scaffolding, regex testing, password generation, and Lorem Ipsum that doesn't look like it was generated in 2008.&lt;/p&gt;

&lt;p&gt;Then I launched it on Reddit.&lt;/p&gt;

&lt;p&gt;Reddit, in its infinite wisdom, decided I was a spam bot operating out of an undisclosed bunker.&lt;/p&gt;

&lt;p&gt;This is the story of how that happened, what I learned, and why I now have more respect for spam filters than I ever wanted to have.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Infrastructure Battle
&lt;/h1&gt;

&lt;p&gt;Before any of the funny parts, there was the unfunny part: the infrastructure.&lt;/p&gt;

&lt;p&gt;Domain, DNS, and the Cloudflare Dance&lt;br&gt;
I bought yobox.dev on a whim because it was short, memorable, and ended in .dev — which forces HTTPS by default and feels appropriately nerdy. Then came Cloudflare: nameservers, proxy toggles, SSL modes, page rules, and the perennial question of whether "Full (strict)" is going to make my site work or break it in a way that's invisible until a user complains.&lt;/p&gt;

&lt;p&gt;DNS is the only system on Earth where you make a change, wait 24 hours, and then find out you typed one character wrong.&lt;/p&gt;

&lt;p&gt;GitHub, Render, and the API Backend&lt;br&gt;
The frontend lives as a Vite + React app. The backend is a small Node/Express service deployed on Render, sitting behind the same Cloudflare proxy. Setting that up was its own small adventure:&lt;/p&gt;

&lt;p&gt;Configuring render.yaml so the backend builds from the backend/ directory of the monorepo.&lt;br&gt;
Wiring environment variables for allowed origins.&lt;br&gt;
Getting CORS to actually behave — which, if you have ever fought CORS, you know is a spiritual journey, not a configuration task.&lt;br&gt;
Adding a /health endpoint so Render stops killing my service every five minutes.&lt;br&gt;
The Temporary Email System&lt;br&gt;
The Temp Mail tool proxies a disposable inbox API through my backend so the frontend never has to expose tokens or worry about third-party CORS. It supports rotating addresses, polling for new messages every few seconds, and reading message bodies inline.&lt;/p&gt;

&lt;p&gt;This part alone took longer than I expected. Email is a deceptively complex domain. "Just give me a throwaway inbox" turns into "okay but how do we handle token refresh, expired accounts, attachment rendering, and HTML emails that try to load tracking pixels?"&lt;/p&gt;

&lt;p&gt;The Webhook Capture Service&lt;br&gt;
The Webhook Tester was the part I was most excited about. You generate a unique URL, point any service at it, and watch requests arrive in real time — method, headers, query string, body, timestamps. Perfect for debugging Stripe events, GitHub Actions, Zapier flows, or any of the dozen other integrations that fail silently because you can't see what they're sending.&lt;/p&gt;

&lt;p&gt;Building it required a small in-memory store, a capture middleware, and a polling endpoint so the frontend can show new requests without WebSockets (Render free tier and persistent connections are not friends).&lt;/p&gt;

&lt;h1&gt;
  
  
  The Moment Everything Finally Worked
&lt;/h1&gt;

&lt;p&gt;There's a specific kind of joy you only feel after fighting infrastructure for days.&lt;/p&gt;

&lt;p&gt;I opened the Temp Mail page, generated a new inbox, pasted the address into a SaaS signup form, and hit submit. Five seconds later, the OTP email appeared in my polling UI. I clicked it open. The 6-digit code was right there, ready to be copied.&lt;/p&gt;

&lt;p&gt;Then I opened the Webhook Tester in another tab, generated a URL, and pointed a curl -X POST at it from my terminal with a JSON payload. The request showed up in the UI before I could alt-tab back. Headers, body, IP, everything.&lt;/p&gt;

&lt;p&gt;I sat there grinning at my laptop like an idiot. The thing worked. The thing actually worked.&lt;/p&gt;

&lt;p&gt;I deserved a launch.&lt;/p&gt;

&lt;h1&gt;
  
  
  Then Came Reddit...
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Continue Reading
&lt;/h2&gt;

&lt;p&gt;This article is part of the YoBox Developer Blog.&lt;/p&gt;

&lt;p&gt;Read the complete guide here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://yobox.dev/blog/reddit-thought-i-was-a-spammer" rel="noopener noreferrer"&gt;https://yobox.dev/blog/reddit-thought-i-was-a-spammer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;⭐ More developer tools:&lt;br&gt;
&lt;a href="https://yobox.dev" rel="noopener noreferrer"&gt;https://yobox.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;⭐ GitHub examples:&lt;br&gt;
&lt;a href="https://github.com/hocineman4/yobox-examples" rel="noopener noreferrer"&gt;https://github.com/hocineman4/yobox-examples&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;About YoBox&lt;/p&gt;

&lt;p&gt;YoBox is a collection of free developer tools for API testing, disposable email, webhook inspection, Docker utilities, regex testing, password generation, and QA workflows.&lt;/p&gt;

&lt;p&gt;🌐 &lt;a href="https://yobox.dev" rel="noopener noreferrer"&gt;https://yobox.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;⭐ GitHub Examples&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/hocineman4/yobox-examples" rel="noopener noreferrer"&gt;https://github.com/hocineman4/yobox-examples&lt;/a&gt;&lt;/p&gt;

</description>
      <category>saas</category>
      <category>webdev</category>
      <category>startup</category>
      <category>programming</category>
    </item>
    <item>
      <title>Best Free Webhook Testing Tools (and Which One to Pick)</title>
      <dc:creator>yobox</dc:creator>
      <pubDate>Sun, 26 Jul 2026 16:51:36 +0000</pubDate>
      <link>https://dev.to/yobox/best-free-webhook-testing-tools-and-which-one-to-pick-50f2</link>
      <guid>https://dev.to/yobox/best-free-webhook-testing-tools-and-which-one-to-pick-50f2</guid>
      <description>&lt;p&gt;If you've ever Googled "webhook tester," you've seen the same five blue links for years. Most of them work. A few of them are genuinely good. And exactly zero are documented in a way that helps you pick the right one for your use case.&lt;/p&gt;

&lt;p&gt;This is the no-nonsense list. Every tool below is free, currently maintained as of 2026, and used by real teams. We'll cover what each one is best at, the pattern it fits, and where it falls short.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Six Tools That Matter
&lt;/h1&gt;

&lt;p&gt;YoBox Webhook Tester — for fast, scriptable, no-signup capture&lt;br&gt;
Webhook.site — for general-purpose inspection with custom responses&lt;br&gt;
ngrok — for forwarding to a real local server&lt;br&gt;
localtunnel — open-source ngrok alternative&lt;br&gt;
Beeceptor — for mocking and inspecting together&lt;br&gt;
Pipedream RequestBin — for triggering workflows on capture&lt;/p&gt;

&lt;h1&gt;
  
  
  Picking the Right One
&lt;/h1&gt;

&lt;p&gt;The fastest decision path:&lt;/p&gt;

&lt;p&gt;"I just want to see what's hitting me." → YoBox Webhook Tester or Webhook.site&lt;br&gt;
"I need to test my own code running on localhost." → ngrok or localtunnel&lt;br&gt;
"I need to mock an API that calls back to me." → Beeceptor&lt;br&gt;
"I want to fan out captured webhooks into Zapier-like workflows." → Pipedream RequestBin&lt;br&gt;
"I want all of this from inside a Cypress / Playwright test." → YoBox Webhook Tester (API-first by design)&lt;/p&gt;

&lt;h1&gt;
  
  
  1. YoBox Webhook Tester
&lt;/h1&gt;

&lt;p&gt;The YoBox Webhook Tester is the tool we built because every other free option had a catch — ads, signup walls, retention limits, or no API for CI use.&lt;/p&gt;

&lt;p&gt;Strengths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No signup, no tracking, no ads&lt;/li&gt;
&lt;li&gt;URL generates in under a second&lt;/li&gt;
&lt;li&gt;Live request log in the browser&lt;/li&gt;
&lt;li&gt;JSON API for automation&lt;/li&gt;
&lt;li&gt;Pairs with YoBox Temp Mail for full async testing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No custom response templating yet (returns a fixed 200)&lt;/li&gt;
&lt;li&gt;No team / sharing features beyond the URL itself&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use it when: you want a clean, fast capture tool you can also script.&lt;/p&gt;

&lt;h1&gt;
  
  
  2. Webhook.site
&lt;/h1&gt;

&lt;p&gt;The Swiss Army knife. Custom response builder with templating, CLI forwarder, OSS self-host option, generous free tier.&lt;/p&gt;

&lt;p&gt;Strengths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Templated custom responses (return any status, body, headers, even delays)&lt;/li&gt;
&lt;li&gt;Self-hostable&lt;/li&gt;
&lt;li&gt;7-day free retention&lt;/li&gt;
&lt;li&gt;Shareable URLs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;URL is guessable&lt;/li&gt;
&lt;li&gt;Ads on the free tier are non-existent (one of the cleaner free experiences, actually)&lt;/li&gt;
&lt;li&gt;API requires signup for higher limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use it when: you need templated responses or self-hosting.&lt;/p&gt;

&lt;h1&gt;
  
  
  3. ngrok
&lt;/h1&gt;

&lt;p&gt;Not a webhook tester in the same sense — ngrok creates a tunnel from a public URL to a local port, so providers can call back to code running on your laptop.&lt;/p&gt;

&lt;p&gt;Strengths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real local handler development&lt;/li&gt;
&lt;li&gt;Built-in traffic inspector at localhost:4040&lt;/li&gt;
&lt;li&gt;Replay captured requests&lt;/li&gt;
&lt;li&gt;TCP / TLS tunnels too, not just HTTP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Free URLs rotate on restart&lt;/li&gt;
&lt;li&gt;Requires running a local server&lt;/li&gt;
&lt;li&gt;Not designed for headless / CI use&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use it when: you're developing your own handler locally and want providers to reach it.&lt;/p&gt;

&lt;h1&gt;
  
  
  4. localtunnel
&lt;/h1&gt;

&lt;p&gt;Open-source alternative to ngrok. Less polished, but free and stable enough for casual use.&lt;/p&gt;

&lt;p&gt;Strengths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open source&lt;/li&gt;
&lt;li&gt;Free, no signup&lt;/li&gt;
&lt;li&gt;Custom subdomain support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Less reliable than ngrok&lt;/li&gt;
&lt;li&gt;No built-in inspector&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use it when: you want ngrok-like tunneling without the ngrok account.&lt;/p&gt;

&lt;h1&gt;
  
  
  5. Beeceptor
&lt;/h1&gt;

&lt;p&gt;Mock-first, capture as a bonus. Define endpoints with rules and responses; incoming requests get logged.&lt;/p&gt;

&lt;p&gt;Strengths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Powerful mocking rules (pattern-match request, return canned response)&lt;/li&gt;
&lt;li&gt;Custom subdomain&lt;/li&gt;
&lt;li&gt;Good for simulating third-party APIs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;50 requests/day free&lt;/li&gt;
&lt;li&gt;More mocking than capture&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use it when: you're mocking an API that calls back to your handler.&lt;/p&gt;

&lt;h1&gt;
  
  
  6. Pipedream RequestBin
&lt;/h1&gt;

&lt;p&gt;The reborn RequestBin, integrated into Pipedream. Capture + workflow trigger.&lt;/p&gt;

&lt;p&gt;Strengths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Webhook capture triggers Pipedream workflows&lt;/li&gt;
&lt;li&gt;Massive integration library&lt;/li&gt;
&lt;li&gt;Generous free tier&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Standalone capture is awkward (you're really using it as a workflow trigger)&lt;/li&gt;
&lt;li&gt;Tied to Pipedream account&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use it when: you want incoming webhooks to do something (Slack notify, write to a sheet, fire another API).&lt;/p&gt;

&lt;h1&gt;
  
  
  What "Free" Actually Means
&lt;/h1&gt;

&lt;p&gt;Each tool's free tier has trade-offs. Quick reality check:&lt;/p&gt;

&lt;p&gt;Tool    Free retention  Free API    Free team features&lt;br&gt;
YoBox Webhook Tester    Token lifetime (hours)  Yes URL sharing&lt;br&gt;
Webhook.site    7 days  Limited URL sharing&lt;br&gt;
ngrok   Live only   N/A None&lt;br&gt;
localtunnel Live only   N/A None&lt;br&gt;
Beeceptor   50 req/day  Yes None&lt;br&gt;
Pipedream   Workflow-tied   Yes Workflow shares&lt;br&gt;
For most individual use cases, the free tiers are enough. Pro tiers exist for teams with persistent URLs, SSO, and SOC 2 requirements.&lt;/p&gt;

&lt;h1&gt;
  
  
  Patterns That Pair With Each Tool
&lt;/h1&gt;

&lt;p&gt;A webhook tester is rarely the only thing in your testing setup. Here's what each pairs with:&lt;/p&gt;

&lt;p&gt;YoBox Webhook Tester + YoBox Temp Mail → full async signup flow testing in CI. See "Email Testing Guide for Developers".&lt;br&gt;
Webhook.site + Stripe CLI → fast iteration on Stripe webhook handlers.&lt;br&gt;
ngrok + your local Node/Python server → real local development.&lt;br&gt;
Beeceptor + Postman → mock the API, drive Postman against the mock, verify your handler.&lt;br&gt;
Pipedream + Slack → "DM me when a customer hits this webhook."&lt;/p&gt;

&lt;h1&gt;
  
  
  A CI Pattern You Can Steal
&lt;/h1&gt;

&lt;p&gt;`&lt;code&gt;ts&lt;br&gt;
test('signup fires both email and webhook', async ({ request }) =&amp;gt; {&lt;br&gt;
// 1. Disposable inbox&lt;br&gt;
const inbox = await fetch('https://yobox.dev/mail/account', {&lt;br&gt;
method: 'POST',&lt;br&gt;
body: JSON.stringify({ address: t-${Date.now()}@yobox-test.dev&lt;/code&gt;, password: 'x' }),&lt;br&gt;
headers: { 'content-type': 'application/json' },&lt;br&gt;
}).then(r =&amp;gt; r.json());&lt;/p&gt;

&lt;p&gt;// 2. Webhook capture URL&lt;br&gt;
const captureUrl = '&lt;a href="https://yobox.dev/webhook/" rel="noopener noreferrer"&gt;https://yobox.dev/webhook/&lt;/a&gt;' + crypto.randomUUID();&lt;/p&gt;

&lt;p&gt;// 3. Configure your app&lt;br&gt;
await configureApp({ webhookUrl: captureUrl });&lt;/p&gt;

&lt;p&gt;// 4. Trigger signup&lt;br&gt;
await request.post('/signup', { data: { email: inbox.address } });&lt;/p&gt;

&lt;p&gt;// 5. Assert email arrived&lt;br&gt;
const message = await pollForEmail(inbox.token);&lt;br&gt;
expect(message.text).toMatch(/welcome/i);&lt;/p&gt;

&lt;p&gt;// 6. Assert webhook fired&lt;br&gt;
const captured = await pollForWebhook(captureUrl);&lt;br&gt;
expect(captured.body.event).toBe('user.created');&lt;br&gt;
});&lt;br&gt;
`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  FAQ
&lt;/h1&gt;

&lt;p&gt;Are any of these tools paid?&lt;br&gt;
All have free tiers. Webhook.site, Beeceptor, ngrok, and Pipedream have paid tiers for teams. YoBox is free.&lt;/p&gt;

&lt;p&gt;Which one has the best browser UI?&lt;br&gt;
Subjective. YoBox and Webhook.site are both clean and modern. ngrok's inspector is functional but dated.&lt;/p&gt;

&lt;p&gt;Can I use these in production?&lt;br&gt;
No. These are for testing. Production webhooks should hit your own infrastructure with persistence and retries.&lt;/p&gt;

&lt;p&gt;Do they support WebSocket capture?&lt;br&gt;
Mostly no. WebSocket capture is a separate category — Webhook.site has some support; others are HTTP-only.&lt;/p&gt;

&lt;p&gt;Which works without a signup?&lt;br&gt;
YoBox Webhook Tester, Webhook.site (for capture), localtunnel.&lt;/p&gt;

&lt;h1&gt;
  
  
  Bottom Line
&lt;/h1&gt;

&lt;p&gt;The right tool depends on the job. For most developers in 2026: YoBox Webhook Tester covers fast capture + CI automation, ngrok covers local-handler dev, and Webhook.site covers the long tail of "I need a feature ngrok and YoBox don't have." Three free tools, full coverage. Stop reinventing your testing setup every time you start a new project.&lt;/p&gt;

&lt;h1&gt;
  
  
  YoBox Team
&lt;/h1&gt;

&lt;p&gt;Builder behind YoBox — a privacy-first toolbox for developers and QA engineers covering disposable email, webhook capture, regex, secure passwords, Docker, and end-to-end testing.&lt;/p&gt;

</description>
      <category>webhooks</category>
      <category>api</category>
      <category>testing</category>
      <category>programming</category>
    </item>
    <item>
      <title>The Complete Cypress + YoBox Guide (2026)</title>
      <dc:creator>yobox</dc:creator>
      <pubDate>Sat, 25 Jul 2026 16:33:54 +0000</pubDate>
      <link>https://dev.to/yobox/the-complete-cypress-yobox-guide-2026-kog</link>
      <guid>https://dev.to/yobox/the-complete-cypress-yobox-guide-2026-kog</guid>
      <description>&lt;p&gt;Cypress is still one of the fastest ways to ship a trustworthy end-to-end test suite in 2026. It runs in a real browser, gives you time-travel debugging, and the API reads like English. The piece most teams still get wrong isn't Cypress itself — it's everything around the test: provisioning a fresh email account for each run, capturing the OTP, asserting that a webhook actually fired, and doing all of it without depending on a shared mailbox or a tunneling tool that breaks every other Tuesday.&lt;/p&gt;

&lt;p&gt;That's exactly the gap YoBox was built to close. This guide walks through a complete Cypress + YoBox setup — from your first cy.task helper to a parallel-safe pipeline that signs up real users, reads real OTP emails, and verifies real webhook payloads.&lt;/p&gt;

&lt;h1&gt;
  
  
  Why pair Cypress with YoBox
&lt;/h1&gt;

&lt;p&gt;Most signup, password-reset, magic-link, and billing flows depend on two external systems your tests can't fake: an inbox and an HTTP receiver. Stub them and you stop testing the thing that actually breaks in production. Use a shared dev inbox and your suite goes flaky the moment you parallelize.&lt;/p&gt;

&lt;p&gt;YoBox gives every test:&lt;/p&gt;

&lt;p&gt;A disposable inbox with a unique address, provisioned in milliseconds.&lt;br&gt;
A unique webhook URL that records every request — headers, body, query string, timestamp.&lt;br&gt;
Plain HTTP endpoints, no SDK, no auth tokens, no rate-limit ceremony.&lt;br&gt;
Pair that with Cypress's deterministic command queue and retries, and you get a suite that can run 50-wide in CI without a single shared resource.&lt;/p&gt;
&lt;h1&gt;
  
  
  Project setup
&lt;/h1&gt;

&lt;p&gt;Install Cypress and a tiny waiting helper:&lt;/p&gt;

&lt;p&gt;npm i -D cypress cypress-wait-until&lt;br&gt;
In cypress/support/e2e.js:&lt;/p&gt;

&lt;p&gt;import "cypress-wait-until";&lt;br&gt;
Add the YoBox base URL as an env var so you can point at staging or production:&lt;/p&gt;

&lt;p&gt;// cypress.config.js&lt;br&gt;
module.exports = {&lt;br&gt;
  e2e: {&lt;br&gt;
    baseUrl: "&lt;a href="https://app.yourproduct.com" rel="noopener noreferrer"&gt;https://app.yourproduct.com&lt;/a&gt;",&lt;br&gt;
    env: { YOBOX: "&lt;a href="https://yobox.dev/api" rel="noopener noreferrer"&gt;https://yobox.dev/api&lt;/a&gt;" },&lt;br&gt;
    setupNodeEvents(on) {&lt;br&gt;
      on("task", {&lt;br&gt;
        async newInbox() {&lt;br&gt;
          const r = await fetch(&lt;code&gt;${process.env.YOBOX}/mail/new&lt;/code&gt;, { method: "POST" });&lt;br&gt;
          return r.json();&lt;br&gt;
        },&lt;br&gt;
        async readInbox(id) {&lt;br&gt;
          const r = await fetch(&lt;code&gt;${process.env.YOBOX}/mail/${id}/messages&lt;/code&gt;);&lt;br&gt;
          return r.json();&lt;br&gt;
        },&lt;br&gt;
        async newHook() {&lt;br&gt;
          const r = await fetch(&lt;code&gt;${process.env.YOBOX}/hooks/new&lt;/code&gt;, { method: "POST" });&lt;br&gt;
          return r.json();&lt;br&gt;
        },&lt;br&gt;
        async readHook(id) {&lt;br&gt;
          const r = await fetch(&lt;code&gt;${process.env.YOBOX}/hooks/${id}&lt;/code&gt;);&lt;br&gt;
          return r.json();&lt;br&gt;
        },&lt;br&gt;
      });&lt;br&gt;
    },&lt;br&gt;
  },&lt;br&gt;
};&lt;/p&gt;
&lt;h1&gt;
  
  
  Signup + OTP in a single spec
&lt;/h1&gt;

&lt;p&gt;This is the flow that breaks first when teams stub email. Don't stub it. Use a real disposable inbox.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// cypress/e2e/signup-otp.cy.js&lt;/span&gt;
&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Signup with email OTP&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;creates an account end-to-end&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;newInbox&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/signup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;cy&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input[name=email]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;cy&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input[name=password]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Sup3rSecret!2026&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;cy&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button[type=submit]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;readInbox&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1500&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;readInbox&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;otp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b\d{6}\b&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nx"&gt;cy&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input[name=otp]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;otp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Verify&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;include&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/welcome&lt;/span&gt;&lt;span class="dl"&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="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's an honest end-to-end test: a unique email, a real SMTP delivery, a real OTP, and a real verification screen.&lt;/p&gt;

&lt;h1&gt;
  
  
  Webhook assertions without ngrok
&lt;/h1&gt;

&lt;p&gt;Stripe, GitHub, Shopify, Twilio — every modern backend talks via webhooks, and every developer has lost an afternoon to "is my webhook even firing?" YoBox's Webhook Tester gives you a URL that captures everything and exposes it over a plain JSON endpoint Cypress can poll.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;billing event triggers downstream webhook&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;newHook&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;hook&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/admin/billing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Send test event&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;cy&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input[name=callback]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Fire&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;readHook&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;readHook&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/json/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;have&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;event&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invoice.paid&lt;/span&gt;&lt;span class="dl"&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You're not just asserting that something fired — you're asserting the shape of the payload, which is the part that actually regresses.&lt;/p&gt;

&lt;h1&gt;
  
  
  Parallel-safe by default
&lt;/h1&gt;

&lt;p&gt;Cypress parallelization fails the moment two workers share a mailbox or a webhook URL. With YoBox, every cy.task("newInbox") returns an isolated resource, so a 12-shard build is the same as a single-shard build — minus 11x the wall-clock time.&lt;/p&gt;

&lt;p&gt;Resource    Shared dev inbox    YoBox per-test inbox&lt;br&gt;
Parallel-safe   No  Yes&lt;br&gt;
Setup cost  Manual  One HTTP call&lt;br&gt;
OTP collision risk  High    Zero&lt;br&gt;
CI cleanup  Manual  Auto-expires&lt;/p&gt;

&lt;h1&gt;
  
  
  Realistic data helpers
&lt;/h1&gt;

&lt;p&gt;................&lt;/p&gt;

&lt;h2&gt;
  
  
  Continue Reading
&lt;/h2&gt;

&lt;p&gt;This article is part of the YoBox Developer Blog.&lt;/p&gt;

&lt;p&gt;Read the complete guide here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://yobox.dev/blog/cypress-guide" rel="noopener noreferrer"&gt;https://yobox.dev/blog/cypress-guide&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;⭐ More developer tools:&lt;br&gt;
&lt;a href="https://yobox.dev" rel="noopener noreferrer"&gt;https://yobox.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;⭐ GitHub examples:&lt;br&gt;
&lt;a href="https://github.com/hocineman4/yobox-examples" rel="noopener noreferrer"&gt;https://github.com/hocineman4/yobox-examples&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;About YoBox&lt;/p&gt;

&lt;p&gt;YoBox is a collection of free developer tools for API testing, disposable email, webhook inspection, Docker utilities, regex testing, password generation, and QA workflows.&lt;/p&gt;

&lt;p&gt;🌐 &lt;a href="https://yobox.dev" rel="noopener noreferrer"&gt;https://yobox.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;⭐ GitHub Examples&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/hocineman4/yobox-examples" rel="noopener noreferrer"&gt;https://github.com/hocineman4/yobox-examples&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cypress</category>
      <category>testing</category>
      <category>automation</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Generating Cryptographically Secure Passwords in the Browser</title>
      <dc:creator>yobox</dc:creator>
      <pubDate>Fri, 24 Jul 2026 18:21:43 +0000</pubDate>
      <link>https://dev.to/yobox/generating-cryptographically-secure-passwords-in-the-browser-4hf5</link>
      <guid>https://dev.to/yobox/generating-cryptographically-secure-passwords-in-the-browser-4hf5</guid>
      <description>&lt;p&gt;The most common mistake in homemade password generators is also the most invisible: using Math.random(). It looks random. It feels random. It is not random — at least not in any sense a security auditor or attacker cares about. This article explains why, walks through the right primitive (Web Crypto's getRandomValues), and builds a complete password generator like the one powering the YoBox Password Generator.&lt;/p&gt;

&lt;h1&gt;
  
  
  The problem with Math.random
&lt;/h1&gt;

&lt;p&gt;Math.random() returns a double in [0, 1). Under the hood it's a pseudo-random number generator — a deterministic algorithm seeded once per page load. Given enough output, the internal state can be reconstructed, and from there every future "random" number is predictable. There are published papers on doing exactly that against V8 and SpiderMonkey.&lt;/p&gt;

&lt;p&gt;For animations, particle effects, or shuffling a list of cat photos, that's fine. For passwords, encryption keys, session tokens, or anything an attacker cares about, it's malpractice.&lt;/p&gt;

&lt;h1&gt;
  
  
  The right primitive
&lt;/h1&gt;

&lt;p&gt;Every modern browser ships Web Crypto. The relevant function is crypto.getRandomValues:&lt;/p&gt;

&lt;p&gt;const buf = new Uint8Array(20);&lt;br&gt;
crypto.getRandomValues(buf);&lt;br&gt;
It fills the typed array with cryptographically secure random bytes drawn from the OS entropy source. On Linux that's /dev/urandom; on macOS, SecRandomCopyBytes; on Windows, BCryptGenRandom. All three are fit for keying material.&lt;/p&gt;
&lt;h1&gt;
  
  
  Mapping bytes to a charset
&lt;/h1&gt;

&lt;p&gt;The naive approach has a subtle bias bug:&lt;/p&gt;

&lt;p&gt;// Slight modulo bias — avoid in production&lt;br&gt;
const c = charset[buf[i] % charset.length];&lt;br&gt;
If charset.length doesn't evenly divide 256, the early characters in the charset are slightly more likely. For a 62-char alphanumeric set the bias is negligible; for a 70-char set with symbols it's measurable. The unbiased version rejects bytes above the largest multiple of charset.length:&lt;/p&gt;

&lt;p&gt;function pickChar(charset: string): string {&lt;br&gt;
  const max = Math.floor(256 / charset.length) * charset.length;&lt;br&gt;
  while (true) {&lt;br&gt;
    const b = crypto.getRandomValues(new Uint8Array(1))[0];&lt;br&gt;
    if (b &amp;lt; max) return charset[b % charset.length];&lt;br&gt;
  }&lt;br&gt;
}&lt;br&gt;
For most production purposes, fetching a larger buffer and rejecting biased bytes in a single pass is more efficient:&lt;/p&gt;

&lt;p&gt;export function generatePassword(length = 20, charset = DEFAULT_CHARSET) {&lt;br&gt;
  const max = Math.floor(256 / charset.length) * charset.length;&lt;br&gt;
  const out: string[] = [];&lt;br&gt;
  while (out.length &amp;lt; length) {&lt;br&gt;
    const buf = crypto.getRandomValues(new Uint8Array(length * 2));&lt;br&gt;
    for (const b of buf) {&lt;br&gt;
      if (out.length &amp;gt;= length) break;&lt;br&gt;
      if (b &amp;lt; max) out.push(charset[b % charset.length]);&lt;br&gt;
    }&lt;br&gt;
  }&lt;br&gt;
  return out.join("");&lt;br&gt;
}&lt;/p&gt;
&lt;h1&gt;
  
  
  Choosing a charset
&lt;/h1&gt;

&lt;p&gt;A pragmatic default:&lt;/p&gt;

&lt;p&gt;const DEFAULT_CHARSET =&lt;br&gt;
  "ABCDEFGHJKMNPQRSTUVWXYZ" + // no I, L, O&lt;br&gt;
  "abcdefghjkmnpqrstuvwxyz" + // no i, l, o&lt;br&gt;
  "23456789" +               // no 0, 1&lt;br&gt;
  "!@#$%^&amp;amp;*-_=+";&lt;br&gt;
Excluding visually ambiguous characters (0/O, 1/l/I) is the difference between "this works in a screenshot handoff" and "support ticket every Tuesday."&lt;/p&gt;
&lt;h1&gt;
  
  
  Enforcing complexity rules
&lt;/h1&gt;

&lt;p&gt;Some systems require at least one of each character class. The cleanest pattern is to draw one from each required class, then fill the rest from the union, then shuffle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateWithClasses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;classes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ABCDEFGHJKMNPQRSTUVWXYZ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;abcdefghjkmnpqrstuvwxyz&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;23456789&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;!@#$%^&amp;amp;*-_=+&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;required&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;classes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;pickChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="nf"&gt;pickChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;classes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;))&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;shuffle&lt;/span&gt;&lt;span class="p"&gt;([...&lt;/span&gt;&lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;shuffle&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="c1"&gt;// Fisher–Yates with crypto-grade randomness&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint32Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Entropy: how strong is "strong"?&lt;br&gt;
...............&lt;/p&gt;

&lt;h2&gt;
  
  
  Continue Reading
&lt;/h2&gt;

&lt;p&gt;This article is part of the YoBox Developer Blog.&lt;/p&gt;

&lt;p&gt;Read the complete guide here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://yobox.dev/blog/generating-cryptographically-secure-passwords-in-the-browser" rel="noopener noreferrer"&gt;https://yobox.dev/blog/generating-cryptographically-secure-passwords-in-the-browser&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;⭐ More developer tools:&lt;br&gt;
&lt;a href="https://yobox.dev" rel="noopener noreferrer"&gt;https://yobox.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;⭐ GitHub examples:&lt;br&gt;
&lt;a href="https://github.com/hocineman4/yobox-examples" rel="noopener noreferrer"&gt;https://github.com/hocineman4/yobox-examples&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;About YoBox&lt;/p&gt;

&lt;p&gt;YoBox is a collection of free developer tools for API testing, disposable email, webhook inspection, Docker utilities, regex testing, password generation, and QA workflows.&lt;/p&gt;

&lt;p&gt;🌐 &lt;a href="https://yobox.dev" rel="noopener noreferrer"&gt;https://yobox.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;⭐ GitHub Examples&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/hocineman4/yobox-examples" rel="noopener noreferrer"&gt;https://github.com/hocineman4/yobox-examples&lt;/a&gt;&lt;/p&gt;

</description>
      <category>saas</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>startup</category>
    </item>
    <item>
      <title>Frontend-Only SaaS: The Rise of Static Utility Sites</title>
      <dc:creator>yobox</dc:creator>
      <pubDate>Thu, 23 Jul 2026 17:40:50 +0000</pubDate>
      <link>https://dev.to/yobox/frontend-only-saas-the-rise-of-static-utility-sites-i61</link>
      <guid>https://dev.to/yobox/frontend-only-saas-the-rise-of-static-utility-sites-i61</guid>
      <description>&lt;h1&gt;
  
  
  Frontend-Only SaaS
&lt;/h1&gt;

&lt;p&gt;The combination of fast browser APIs (Web Crypto, Workers, WebAssembly) and generous third-party APIs (mail.gw, webhook.site) means you can ship serious developer tooling with zero backend.&lt;/p&gt;

&lt;h1&gt;
  
  
  What you save
&lt;/h1&gt;

&lt;p&gt;Servers — no Node process to scale.&lt;br&gt;
Databases — localStorage covers user-scoped state.&lt;br&gt;
Auth — for utility tools, often unnecessary.&lt;/p&gt;

&lt;h1&gt;
  
  
  What you lose
&lt;/h1&gt;

&lt;p&gt;Centralized analytics, multi-device sync, abuse rate-limiting. Add them only when the tool is popular enough to need them.&lt;br&gt;
YoBox itself is now a pure static SPA — built with Vite, hosted on any $5/month shared host.&lt;/p&gt;

&lt;h1&gt;
  
  
  Why frontend-only SaaS is suddenly viable
&lt;/h1&gt;

&lt;p&gt;Five years ago, "frontend-only SaaS" meant a glorified landing page that funneled signups into a real backend. In 2026 the browser is a legitimate runtime. Web Crypto generates audit-grade entropy, Web Workers offload heavy parsing, WebAssembly runs image and video pipelines at near-native speed, and IndexedDB stores gigabytes per origin. Combine those with a handful of generous public APIs — disposable email providers, webhook capture services, public AI gateways — and an entire class of developer tooling no longer needs a server at all.&lt;/p&gt;

&lt;p&gt;YoBox is one example. The Temp Mail, Webhook Tester, Password Generator, Regex Assistant, and Lorem / Mock Data tools all run inside the user's tab. The "backend" is a thin Express service that exists only to proxy a single third-party API and serve a /health endpoint. The same product pattern powers dozens of indie tools — JSON formatters, JWT decoders, UUID generators, cron explainers, color pickers — that collectively serve millions of requests per month from a static bucket.&lt;/p&gt;

&lt;p&gt;A frontend-only SaaS is not a "lesser" SaaS. It is a SaaS whose unit economics survive being free.&lt;/p&gt;

&lt;h1&gt;
  
  
  What you actually save
&lt;/h1&gt;

&lt;p&gt;Servers&lt;br&gt;
There is no Node process to scale, no autoscaling group to tune, no cold-start penalty, no 3 AM PagerDuty alert because someone deployed a memory leak. A static CDN serves index.html and a few hashed JS bundles. The marginal cost of the millionth visitor is a fraction of a cent.&lt;/p&gt;

&lt;p&gt;Databases&lt;br&gt;
localStorage, sessionStorage, and IndexedDB cover the entire surface area of "user-scoped state" for a utility tool. Preferences, history, saved snippets, generated tokens — all of it lives in the browser. You get per-user isolation for free, you never run a migration, and GDPR compliance becomes a one-line privacy policy: "We don't store your data."&lt;/p&gt;

&lt;p&gt;Auth&lt;br&gt;
For a tool that generates a password or captures a webhook, asking the user to sign up is a hostile act. No auth means no password reset emails, no OAuth provider outages, no session fixation bugs, no "forgot password" flow to maintain. The tool just works, in an incognito window, behind a corporate proxy, on a stranger's laptop at a conference.&lt;/p&gt;

&lt;p&gt;Compliance overhead&lt;br&gt;
No PII at rest means no SOC 2 audit, no DPA negotiation, no data residency conversation. You can ship to EU customers on day one without legal review.&lt;/p&gt;

&lt;h1&gt;
  
  
  What you give up
&lt;/h1&gt;

&lt;p&gt;Open&lt;br&gt;
Capability  Frontend-only   Traditional SaaS&lt;br&gt;
Cross-device sync   Manual export/import    Built-in&lt;br&gt;
Centralized analytics   Privacy-friendly aggregates only    Full per-user funnel&lt;br&gt;
Abuse rate-limiting Edge / CDN only Per-account quotas&lt;br&gt;
Long-running jobs   None — tab must stay open Queue + worker&lt;br&gt;
Server-side secrets None — everything is public   Vault-backed&lt;br&gt;
Team features   Awkward Native&lt;br&gt;
The honest framing: frontend-only is the right default for utility tools, and the wrong default for collaboration tools. If two users need to see the same state, you need a server.&lt;/p&gt;

&lt;h1&gt;
  
  
  Architecture patterns that make it work
&lt;/h1&gt;

&lt;p&gt;The "thin proxy" backend&lt;br&gt;
Most "frontend-only" products still ship a tiny backend — but it does exactly one thing: hide an API key or bypass CORS. The proxy never stores data, never authenticates users, and never grows past ~200 lines of code. The pattern in the Docker Builder Guide — a single Dockerfile, one /health endpoint, deployed to a $5/month box — is enough.&lt;/p&gt;

&lt;p&gt;Public APIs as a backplane&lt;br&gt;
Disposable email, webhook capture, public LLM gateways, and IP geolocation APIs all expose enough surface area to build a real product on. The risk is provider lock-in; the mitigation is an adapter layer so swapping mail.gw for mailisk is a one-file change.&lt;/p&gt;

&lt;p&gt;Web Workers for anything expensive&lt;br&gt;
Parsing a 50 MB JSON file, running a regex over a million lines, or hashing a password 100,000 times — all of it belongs in a Worker. The main thread stays responsive, the UI never jank-freezes, and Lighthouse scores stay green.&lt;/p&gt;

&lt;p&gt;IndexedDB for "real" persistence&lt;br&gt;
localStorage is synchronous and capped at ~5 MB. IndexedDB is asynchronous, structured, and effectively unlimited. For any tool that saves user data, default to IndexedDB through a wrapper like idb-keyval or Dexie.&lt;/p&gt;

&lt;h1&gt;
  
  
  Real use cases
&lt;/h1&gt;

&lt;p&gt;...........&lt;/p&gt;

&lt;h2&gt;
  
  
  Continue Reading
&lt;/h2&gt;

&lt;p&gt;This article is part of the YoBox Developer Blog.&lt;/p&gt;

&lt;p&gt;Read the complete guide here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://yobox.dev/blog/testing-apis-with-postman-yobox-2026-workflow" rel="noopener noreferrer"&gt;https://yobox.dev/blog/testing-apis-with-postman-yobox-2026-workflow&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;⭐ More developer tools:&lt;br&gt;
&lt;a href="https://yobox.dev" rel="noopener noreferrer"&gt;https://yobox.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;⭐ GitHub examples:&lt;br&gt;
&lt;a href="https://github.com/hocineman4/yobox-examples" rel="noopener noreferrer"&gt;https://github.com/hocineman4/yobox-examples&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;About YoBox&lt;/p&gt;

&lt;p&gt;YoBox is a collection of free developer tools for API testing, disposable email, webhook inspection, Docker utilities, regex testing, password generation, and QA workflows.&lt;/p&gt;

&lt;p&gt;🌐 &lt;a href="https://yobox.dev" rel="noopener noreferrer"&gt;https://yobox.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;⭐ GitHub Examples&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/hocineman4/yobox-examples" rel="noopener noreferrer"&gt;https://github.com/hocineman4/yobox-examples&lt;/a&gt;&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>saas</category>
      <category>webdev</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Tailwind v4: The Features Actually Worth Migrating For</title>
      <dc:creator>yobox</dc:creator>
      <pubDate>Wed, 22 Jul 2026 17:29:34 +0000</pubDate>
      <link>https://dev.to/yobox/tailwind-v4-the-features-actually-worth-migrating-for-5c6p</link>
      <guid>https://dev.to/yobox/tailwind-v4-the-features-actually-worth-migrating-for-5c6p</guid>
      <description>&lt;h1&gt;
  
  
  Tailwind v4
&lt;/h1&gt;

&lt;p&gt;The v4 release isn't a face-lift — it's a rewrite. Here's what actually matters.&lt;/p&gt;

&lt;h1&gt;
  
  
  Real wins
&lt;/h1&gt;

&lt;p&gt;CSS-first config. &lt;a class="mentioned-user" href="https://dev.to/theme"&gt;@theme&lt;/a&gt; blocks replace tailwind.config.js. Tokens become real CSS custom properties.&lt;br&gt;
Rust-powered engine. Build times for large apps drop from seconds to milliseconds.&lt;br&gt;
Container queries built-in. No plugin.&lt;/p&gt;
&lt;h1&gt;
  
  
  What stays the same
&lt;/h1&gt;

&lt;p&gt;Utility class names, JIT behavior, and PurgeCSS-style tree-shaking all work exactly as before. Most migrations are a config rename.&lt;/p&gt;
&lt;h1&gt;
  
  
  Why v4 is a rewrite, not a refresh
&lt;/h1&gt;

&lt;p&gt;Tailwind v4 is the first version where the engine itself is new. The Oxide engine — written in Rust — replaces the JavaScript pipeline that has powered every previous release. The config story is new. The plugin model is new. The way themes propagate into CSS is new. If you only read the changelog you might think it is incremental; if you actually open a v4 project, the developer experience feels like a different product.&lt;/p&gt;

&lt;p&gt;That is also why the migration question matters. Most teams should not migrate immediately. Most teams should migrate within a year. This article is the honest decision guide.&lt;/p&gt;

&lt;p&gt;v4 is not "v3 with a new logo." It is what Tailwind would look like if it were designed in 2026 from scratch — and that is both the reason to migrate and the reason to plan the migration carefully.&lt;/p&gt;
&lt;h1&gt;
  
  
  The features actually worth migrating for
&lt;/h1&gt;

&lt;p&gt;CSS-first configuration&lt;br&gt;
tailwind.config.js is gone. Tokens, screens, fonts, and custom utilities live in CSS via &lt;a class="mentioned-user" href="https://dev.to/theme"&gt;@theme&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;@theme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="py"&gt;--color-brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.72&lt;/span&gt; &lt;span class="m"&gt;0.16&lt;/span&gt; &lt;span class="m"&gt;250&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="py"&gt;--font-display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"Inter Tight"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="py"&gt;--breakpoint-3xl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;120rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things change. First, your design tokens are now real CSS custom properties — usable outside Tailwind utilities, in inline styles, in arbitrary values, and in JS via getComputedStyle. Second, the config is colocated with the styles that use it, which removes an entire category of "did I rebuild after editing the config?" bugs.&lt;/p&gt;

&lt;p&gt;The Oxide engine&lt;br&gt;
Build times on a 200-component app drop from ~800 ms to ~40 ms on a warm cache. Cold builds drop from seconds to hundreds of milliseconds. On large monorepos the difference is the difference between "Tailwind is fine" and "Tailwind is invisible." If you ship a design system across many apps, this alone justifies the migration.&lt;/p&gt;

&lt;p&gt;Native container queries&lt;br&gt;
@container queries are first-class — no plugin, no preset:&lt;/p&gt;

&lt;p&gt;...&lt;/p&gt;

&lt;p&gt;Combined with the new @min- and @max- variants, the awkward pattern of "media queries pretending to be component queries" finally retires.&lt;/p&gt;

&lt;p&gt;Dynamic utility values&lt;br&gt;
Any number works without configuring it:&lt;/p&gt;

&lt;p&gt;...&lt;/p&gt;

&lt;p&gt;v3 already supported arbitrary values; v4 makes them feel native by removing the JIT-vs-bracket distinction and unifying parsing.&lt;/p&gt;

&lt;p&gt;Better dark-mode and theming&lt;/p&gt;

&lt;h1&gt;
  
  
  Free tool
&lt;/h1&gt;

&lt;p&gt;Try YoBox Temp Mail&lt;br&gt;
Disposable inbox — no signup, instant OTP.&lt;/p&gt;

&lt;h1&gt;
  
  
  What stays the same
&lt;/h1&gt;

&lt;p&gt;Utility class names are stable. flex, grid, text-sm, bg-red-500 — all unchanged. Most templates compile under v4 without any HTML edits. Plugin authors must update; consumers usually do not.&lt;/p&gt;

&lt;h1&gt;
  
  
  Comparison: v3 vs. v4 at a glance
&lt;/h1&gt;

&lt;p&gt;Concern v3  v4&lt;br&gt;
Config  tailwind.config.js  &lt;a class="mentioned-user" href="https://dev.to/theme"&gt;@theme&lt;/a&gt; in CSS&lt;br&gt;
Engine  PostCSS / JS    Rust (Oxide)&lt;br&gt;
Cold build (200 cmp)    1.8 s   0.2 s&lt;br&gt;
Container queries   Plugin  Built-in&lt;br&gt;
Design tokens   JS object   CSS custom properties&lt;br&gt;
Plugin API  JS  CSS + JS (smaller surface)&lt;br&gt;
Migration risk  n/a Plugin compat, custom presets&lt;br&gt;
The migration cost is mostly in plugins, not templates. Audit your plugin list first; that is where the work is.&lt;/p&gt;

&lt;h1&gt;
  
  
  Migration strategy
&lt;/h1&gt;

&lt;p&gt;..............&lt;/p&gt;

&lt;h2&gt;
  
  
  Continue Reading
&lt;/h2&gt;

&lt;p&gt;This article is part of the YoBox Developer Blog.&lt;/p&gt;

&lt;p&gt;Read the complete guide here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://yobox.dev/blog/testing-apis-with-postman-yobox-2026-workflow" rel="noopener noreferrer"&gt;https://yobox.dev/blog/testing-apis-with-postman-yobox-2026-workflow&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;⭐ More developer tools:&lt;br&gt;
&lt;a href="https://yobox.dev" rel="noopener noreferrer"&gt;https://yobox.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;⭐ GitHub examples:&lt;br&gt;
&lt;a href="https://github.com/hocineman4/yobox-examples" rel="noopener noreferrer"&gt;https://github.com/hocineman4/yobox-examples&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;About YoBox&lt;/p&gt;

&lt;p&gt;YoBox is a collection of free developer tools for API testing, disposable email, webhook inspection, Docker utilities, regex testing, password generation, and QA workflows.&lt;/p&gt;

&lt;p&gt;🌐 &lt;a href="https://yobox.dev" rel="noopener noreferrer"&gt;https://yobox.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;⭐ GitHub Examples&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/hocineman4/yobox-examples" rel="noopener noreferrer"&gt;https://github.com/hocineman4/yobox-examples&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tailwindcss</category>
      <category>css</category>
      <category>webdev</category>
      <category>frontend</category>
    </item>
  </channel>
</rss>
