<?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: Emma</title>
    <description>The latest articles on DEV Community by Emma (@emma_teelylabs).</description>
    <link>https://dev.to/emma_teelylabs</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%2F4130158%2Fc965ccef-d5f1-4aea-9abf-ecf99fc59bc4.jpg</url>
      <title>DEV Community: Emma</title>
      <link>https://dev.to/emma_teelylabs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/emma_teelylabs"/>
    <language>en</language>
    <item>
      <title>Testing webhook retries deterministically: a fault sequence per Idempotency-Key</title>
      <dc:creator>Emma</dc:creator>
      <pubDate>Fri, 18 Sep 2026 02:07:58 +0000</pubDate>
      <link>https://dev.to/emma_teelylabs/testing-webhook-retries-deterministically-a-fault-sequence-per-idempotency-key-1k19</link>
      <guid>https://dev.to/emma_teelylabs/testing-webhook-retries-deterministically-a-fault-sequence-per-idempotency-key-1k19</guid>
      <description>&lt;p&gt;Two weeks ago I wrote about a webhook receiver that can be slow and flaky on purpose. A reader left a comment that changed how I think about it: "a 10% error rate tells me my sender retries. It doesn't tell me the retry is &lt;em&gt;correct&lt;/em&gt;, and I can't put 10% in CI." He was right. The bug I actually care about is a specific one:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;My service sends the webhook.&lt;/li&gt;
&lt;li&gt;The receiver gets it, processes it, and the connection dies before my service reads the 200.&lt;/li&gt;
&lt;li&gt;My service retries with the same idempotency key.&lt;/li&gt;
&lt;li&gt;Does the receiver create a second order, or does it recognise the key and say "already done"?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No amount of random failure reproduces step 2 on demand. So the endpoint now has a third mode, and it's the one I'd use first.&lt;/p&gt;

&lt;h2&gt;
  
  
  A list of steps, walked per key
&lt;/h2&gt;

&lt;p&gt;You give the endpoint a JSON list of steps. Every distinct &lt;code&gt;Idempotency-Key&lt;/code&gt; (or whatever header you name) walks the list in order: the first call with a key gets step one, the second call with the same key gets step two, a new key starts again from step one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"key_header"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Idempotency-Key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"on_end"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"repeat_last"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"steps"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"drop"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"respond"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;received&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:true}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;respond&lt;/code&gt;: a status, a body and an optional &lt;code&gt;delay_ms&lt;/code&gt;, each defaulting to the endpoint's normal settings&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;drop&lt;/code&gt;: log the request, then cut the connection without delivering a body&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;timeout&lt;/code&gt;: hold the connection for &lt;code&gt;delay_ms&lt;/code&gt; (up to 30 seconds), then cut it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The example above is the duplicate-side-effect test. First delivery: logged, connection cut. Retry with the same key: 200. If your sender did the right thing you see exactly two requests in the log with the same key, and one order in your database.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;on_end&lt;/code&gt; decides what happens after the last step. The default repeats the last step forever, which is what you want for a correct sender: once it has its 200 it stops, and if it retries anyway it keeps getting 200. &lt;code&gt;loop&lt;/code&gt; starts over from step one, for soak tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some things I got wrong the first time
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The key is hashed before it's stored.&lt;/strong&gt; You pick the header, so the value might be an order number, an email, anything. The counter table only needs to tell keys apart, so it keeps a hash. You also can't pick &lt;code&gt;Authorization&lt;/code&gt; or &lt;code&gt;Cookie&lt;/code&gt; as the key header; the form refuses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No key means a shared counter.&lt;/strong&gt; If a sender doesn't send the header at all, every call shares one sequence. That's usually a bug in the sender, and the request log makes it visible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Counters reset when you save the endpoint&lt;/strong&gt; with a different sequence, and there's a "Reset sequence" button plus &lt;code&gt;DELETE /v1/endpoints/{id}/sequence&lt;/code&gt; for the start of a CI job. Counters nobody has touched for 7 days are dropped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every response carries &lt;code&gt;X-Supercrontab-Sequence-Step&lt;/code&gt;&lt;/strong&gt;, so when a test fails you can see which step the sender was on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest limit
&lt;/h2&gt;

&lt;p&gt;The endpoints run on Cloudflare Workers. There I can cut a connection in the middle of the body, but not before the status line goes out. So &lt;code&gt;drop&lt;/code&gt; looks like a truncated response: curl exits with code 18, Python raises &lt;code&gt;IncompleteRead&lt;/code&gt;, Go's body read returns an error. It is not a TCP reset. For every HTTP client I've tried, that lands in the same "the request failed, retry it" path, which is the path under test. If your client treats a truncated 200 as success, that itself is a finding.&lt;/p&gt;

&lt;p&gt;Auth still runs first. A call with a bad token gets 401 and does &lt;em&gt;not&lt;/em&gt; consume a step, so a sender that's fumbling credentials can't accidentally eat the &lt;code&gt;drop&lt;/code&gt; you were waiting to see.&lt;/p&gt;

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

&lt;p&gt;Nothing on the free plan: 3 endpoints, 30 requests a minute, 500 a day, up to 20 steps per sequence. A sequence with a timeout step holds a connection for up to 30 seconds, which counts as one request.&lt;/p&gt;

&lt;p&gt;The API reference for it is at &lt;a href="https://supercrontab.com/docs#sequence" rel="noopener noreferrer"&gt;https://supercrontab.com/docs#sequence&lt;/a&gt; and the form is at &lt;a href="https://supercrontab.com/webhook-tester" rel="noopener noreferrer"&gt;https://supercrontab.com/webhook-tester&lt;/a&gt;. If there's a step type you're missing (a random pick between two steps came up already), say so below.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>testing</category>
      <category>devops</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I wanted a webhook receiver that could be slow and flaky on purpose, so I made one</title>
      <dc:creator>Emma</dc:creator>
      <pubDate>Thu, 17 Sep 2026 15:27:06 +0000</pubDate>
      <link>https://dev.to/emma_teelylabs/i-wanted-a-webhook-receiver-that-could-be-slow-and-flaky-on-purpose-so-i-made-one-4c2c</link>
      <guid>https://dev.to/emma_teelylabs/i-wanted-a-webhook-receiver-that-could-be-slow-and-flaky-on-purpose-so-i-made-one-4c2c</guid>
      <description>&lt;p&gt;There are plenty of "paste this URL and see what arrives" webhook testers, and they're fine for the first half of the job: is my service sending the right headers and body? What none of them did for me was the second half: what does my service do when the &lt;em&gt;receiver&lt;/em&gt; is having a bad day? Times out. Returns 500. Works four times out of five.&lt;/p&gt;

&lt;p&gt;That second half is where the expensive bugs live. Duplicate orders because a retry fired after a slow 200. Alerts that never went off because nobody tested the 500 path. So I built a receiver that can misbehave on request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boring half: see what you sent
&lt;/h2&gt;

&lt;p&gt;You create an endpoint (takes about a minute: &lt;a href="https://supercrontab.com/webhook-tester" rel="noopener noreferrer"&gt;https://supercrontab.com/webhook-tester&lt;/a&gt;), get a URL like &lt;code&gt;https://yourslug.supercrontab.com/&lt;/code&gt;, point your service at it. Every request shows up in a log with the method, path, headers, body (up to 16 KB), the caller's IP and the status you answered with. &lt;code&gt;Authorization&lt;/code&gt;, &lt;code&gt;Cookie&lt;/code&gt; and API key headers are redacted in the log, on purpose, because I didn't want to run a service that stores other people's tokens.&lt;/p&gt;

&lt;p&gt;Sub-paths and trailing slashes work, so &lt;code&gt;/orders/42/&lt;/code&gt; reaches the same endpoint and you see the path in the log.&lt;/p&gt;

&lt;h2&gt;
  
  
  The useful half: make it misbehave
&lt;/h2&gt;

&lt;p&gt;Same form, a few more fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;status code, so 202 for "accepted" or 500 when you want to see your alerting fire&lt;/li&gt;
&lt;li&gt;delay in milliseconds, to find out whether your sender times out and what it does after&lt;/li&gt;
&lt;li&gt;error rate, so one call in five fails, which is what a flaky partner actually looks like&lt;/li&gt;
&lt;li&gt;auth: Basic, Bearer, an API key header, or an HMAC-SHA256 signature over the body&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My default setup is "202, 3 seconds, 10% errors". If your sender gets through a day of that with correct retries and no duplicated side effects, you're in decent shape.&lt;/p&gt;

&lt;p&gt;The HMAC one caught a real bug for me: I was signing the pretty-printed JSON and sending the compact one. The endpoint answered 401 and I stared at it for ten minutes before I understood why.&lt;/p&gt;

&lt;h2&gt;
  
  
  The third mode: a scripted fault sequence
&lt;/h2&gt;

&lt;p&gt;A random error rate tells you whether your sender retries at all. It doesn't reproduce one specific bug on demand, and you can't put "10% of the time" in CI. A reader of this post asked for deterministic fault sequences keyed by an idempotency key, and that turned out to be the right shape, so it's in.&lt;/p&gt;

&lt;p&gt;You give the endpoint a list of steps. Each &lt;code&gt;Idempotency-Key&lt;/code&gt; (or any header you name) walks the steps in order: call one gets step one, call two gets step two. Three actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;respond&lt;/code&gt;: a status and body, optionally after a delay&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;drop&lt;/code&gt;: log the request, then cut the connection before the body is delivered&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;timeout&lt;/code&gt;: hold for up to 30 seconds, then cut&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The one I use most reproduces the classic duplicate side-effect bug: the first delivery is logged and the connection is cut, the retry with the same key gets 200.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"steps"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"drop"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"respond"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the last step the endpoint repeats it, so a correct sender stays green; a new key starts again from step one. There's a "Reset sequence" button and a &lt;code&gt;DELETE /v1/endpoints/{id}/sequence&lt;/code&gt; call for the start of a CI job.&lt;/p&gt;

&lt;p&gt;One honest limit: the endpoints run on Cloudflare Workers, and there I can cut the connection mid-body but not before the status line. So &lt;code&gt;drop&lt;/code&gt; looks like a truncated response (curl exit 18, Python &lt;code&gt;IncompleteRead&lt;/code&gt;, Go body read error) rather than a reset. For most senders that's the same failure path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing the loop
&lt;/h2&gt;

&lt;p&gt;The same account has a cron scheduler, so you can schedule a job to call your endpoint every 15 minutes with a Bearer token and watch the job's run history and the endpoint's log agree with each other. I leave one running as a canary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it needs an account
&lt;/h2&gt;

&lt;p&gt;Anonymous receivers get used as drop boxes for stuff nobody should be storing. Tying every receiver to an account keeps the logs private, keeps the service clean, and lets you delete everything when you're done. The free plan has 3 endpoints, 30 requests a minute, 500 a day, and doesn't expire.&lt;/p&gt;

&lt;p&gt;If you want to try only the response side first, without an account, there are 112 static demo endpoints (8 formats x 14 status codes) at &lt;a href="https://supercrontab.com/mock" rel="noopener noreferrer"&gt;https://supercrontab.com/mock&lt;/a&gt;, for example &lt;code&gt;https://demo-json-503.supercrontab.com/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two things I still want: replaying a logged request to another URL, and exporting a log as a HAR. If you'd use either, tell me which first.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>devops</category>
      <category>testing</category>
      <category>showdev</category>
    </item>
    <item>
      <title>112 mock endpoints that return whatever HTTP status you ask for (no signup)</title>
      <dc:creator>Emma</dc:creator>
      <pubDate>Thu, 17 Sep 2026 15:27:02 +0000</pubDate>
      <link>https://dev.to/emma_teelylabs/112-mock-endpoints-that-return-whatever-http-status-you-ask-for-no-signup-59dd</link>
      <guid>https://dev.to/emma_teelylabs/112-mock-endpoints-that-return-whatever-http-status-you-ask-for-no-signup-59dd</guid>
      <description>&lt;p&gt;Last month I was writing a client for a partner API and wanted to check what happens when they return a 503. Simple question. The honest answer was "I don't know, I'd have to fake it", and faking it meant either monkeypatching fetch or spinning up an express server that returns 503, which I've done about forty times in my life and never kept.&lt;/p&gt;

&lt;p&gt;So this time I kept it, and put it on a domain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; https://demo-json-503.supercrontab.com/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="m"&gt;503&lt;/span&gt;
&lt;span class="na"&gt;content-type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"alpha"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"beta"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"gamma"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hostname is the whole API. &lt;code&gt;demo-{format}-{status}.supercrontab.com&lt;/code&gt;, where format is one of json, xml, txt, csv, html, yaml, rss, js and status is one of 200, 201, 204, 400, 401, 403, 404, 409, 422, 429, 500, 502, 503, 504. That's 112 URLs. CORS is wide open so you can hit them from a browser tab too.&lt;/p&gt;

&lt;p&gt;There's an index page with the sample body, curl and fetch snippets for each one, and a note about whether a client should retry that particular status: &lt;a href="https://supercrontab.com/mock" rel="noopener noreferrer"&gt;https://supercrontab.com/mock&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Things I've actually used them for this week
&lt;/h2&gt;

&lt;p&gt;Retry logic, mostly. Point the client at &lt;code&gt;demo-json-503&lt;/code&gt;, watch it back off and retry. Then point it at &lt;code&gt;demo-json-400&lt;/code&gt; and make sure it &lt;em&gt;doesn't&lt;/em&gt; retry, because a 400 won't get better on its own. The second case is where I've seen the most bugs in real code, including my own.&lt;/p&gt;

&lt;p&gt;The CSV and XML ones are nice for parser tests. A tiny well-formed body, so you can have one integration test that touches the network and the rest can be unit tests with the same fixture.&lt;/p&gt;

&lt;p&gt;And error screens. &lt;code&gt;fetch('https://demo-json-429.supercrontab.com/')&lt;/code&gt; from the frontend, see the rate-limit banner, done. No backend changes, no feature flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  If the static ones aren't enough
&lt;/h2&gt;

&lt;p&gt;They're static on purpose, but sometimes you need a URL that returns &lt;em&gt;your&lt;/em&gt; body, or is slow, or fails only some of the time. With a free account you get an endpoint at &lt;code&gt;yourslug.supercrontab.com&lt;/code&gt; where you set the status, the content type, the body (up to 64 KB), a delay in ms, and an error rate. 10% error rate is a surprisingly good imitation of a flaky vendor.&lt;/p&gt;

&lt;p&gt;You can also put auth on it (Basic, Bearer, an API key header, or HMAC over the body) if you want to check that your client actually sends credentials the way you think it does. And it logs every request it receives, headers and body, so it doubles as a webhook receiver.&lt;/p&gt;

&lt;p&gt;Free plan limits: 3 endpoints, 30 requests a minute, 500 a day. Enough for tests, useless for production traffic, which is the idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  httpbin?
&lt;/h2&gt;

&lt;p&gt;I still use httpbin, it's great. It just doesn't do non-JSON formats, and it doesn't give you a URL with your own body and delay that a teammate or a CI job can hit tomorrow. That's the gap.&lt;/p&gt;

&lt;p&gt;If there's a status code or format you need and it's not there, say so in the comments, adding one is a five-line change.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>testing</category>
      <category>api</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
