Docs

Webhook troubleshooting

Fix a failed test event or delivery by symptom, with the exact messages GrowWriter shows, what each one means, and what to do.

Last updated on

Every entry below starts with what you see, says what it means in plain words, and ends with the fix. Start with the first entry: almost every first-connection problem is solved by finding the real status code.

First, find the real status code

What to do. Send an unsigned test request to your endpoint from your own computer and look only at the status line and the Location header:

Check what your endpoint really answers
curl -sS -o /dev/null -D - -X POST \
  -H "Content-Type: application/json" -d '{}' \
  https://your-site.com/webhooks/growwriter | grep -iE "^(HTTP|Location)"

How to read the result. The request is not signed, so a 401 is the good answer: your endpoint is live and rejecting unsigned requests, exactly as it should.

You getIt means
HTTP … 401 or 400Good. The endpoint runs and checks signatures. Signed requests will work.
HTTP … 3xx + Location:Your URL redirects. Use the address in Location: instead. See redirects.
HTTP … 404Wrong path, or the route isn't deployed.
HTTP … 405The route exists but doesn't accept POST.
HTTP … 500Your code crashed. Usually a missing environment variable (the secret).
Connection error / timeoutDNS, firewall, or the site isn't public.

If you get a 3xx, run the command again with the Location: address until you get a 4xx. That final address is the one to enter in GrowWriter.

The test event fails with HTTP 301, 302, 303, 307 or 308

What you see. The dialog shows Your endpoint URL redirects. Enter the address it redirects to and try again, with the redirect target on the line below.

What it means. Your site answered "go over there instead". Browsers follow that silently, so your site looks fine when you open it. GrowWriter never follows a redirect: following one could drop the request body or break the signature. So a redirect always counts as a failed delivery, and all three attempts go to the exact URL you entered.

The usual causes:

CauseTypical redirect
example.com vs www.example.comYour host redirects one to the other. Use the one it redirects to.
http:// instead of https://http://… → https://…
A trailing slash your framework adds or removes/webhook → /webhook/ or the reverse
A locale prefix your framework adds/api/webhook → /en/api/webhook
A preview URL that was reassigned*.vercel.app → your production domain
A CDN or proxy rule ("Always use HTTPS", page rules)any of the above

Fix. Copy the address from the error (or from the Location: header in the check above) and enter it as the endpoint URL. If the connect dialog is still open, click Back; if you already closed it, click Edit URL on the webhook row of the Channels page. Both keep your signing secret. Disconnecting and reconnecting instead creates a new secret, which you would have to update on your server.

"Your endpoint did not respond in time"

What it means. GrowWriter waited 10 seconds for an answer and gave up. Each of the three attempts gets its own 10 seconds.

Fix. Answer before doing slow work. The guide describes two safe patterns: keep the handler safe to run twice and let a retry finish the job, or answer 200 first and do the heavy work in a background job. See What about the 10-second limit?.

"Your endpoint rejected the delivery"

What it means. Your endpoint answered with a status that is not 2xx. The message quotes that status and the first 200 characters of your response body, so return a short plain-text reason from your handler and you will read it right in the dialog. The status tells you where to look:

  • 401 or 403: your signature check said no. See the signature never matches.
  • 404: wrong path. Compare the URL in GrowWriter with your route.
  • 405: the route doesn't accept POST.
  • 413: your server refuses bodies this large. A full article can be several hundred kilobytes. See large payloads.
  • 500: your code crashed. Check your server logs; a missing secret environment variable is the most common cause.

"View post" is missing or opens the wrong page

What it means. The link is whatever URL your endpoint returned in the body of its 2xx answer, as {"url": "https://…"}. Answer with anything else and there is no link. GrowWriter has no other way to know where your site put the post.

Fix. Return the post's public URL from your handler once the post is saved. It must be an absolute http or https URL; relative paths and other schemes are ignored. See The response for the rules. Two things to know:

  • If you answer 200 before creating the post (the background-job pattern), you can't know its URL yet, so the dialog shows no link.
  • The stored link is replaced on every successful delivery. If a link points at an old address, publish the article again and your endpoint's current answer replaces it.

"Delivery failed after several attempts"

What it means. GrowWriter could not connect to your endpoint at all in three attempts: DNS failed, the connection was refused, or the site is not reachable from the internet.

Fix. Check that the URL is live, uses https://, and is public. Addresses on private networks and localhost are blocked for security. To receive real events on your machine while developing, use a tunnel service (ngrok, cloudflared) and enter the tunnel's HTTPS address.

The URL is refused when I connect

GrowWriter checks the URL before saving it. The three messages and their meaning:

  • The webhook URL must use HTTPS: only https:// is accepted.
  • The webhook URL points to a private address and cannot be used: the host name resolves to a local or private network address (localhost, 10.x, 192.168.x, and similar). Use a public address or a tunnel.
  • The webhook URL is invalid: the URL can't be parsed, contains a username or password, or its host name can't be resolved.

GrowWriter does not test whether the endpoint answers at this point. That's what Send test event is for.

The signature never matches

Work through this list in order. Each item explains one whole class of failures:

  1. Verify over the raw body. Compute the signature over the exact bytes you received, before any JSON parsing. Parsing and re-serializing changes the bytes.
  2. Use the whole secret. The key is the entire string, whsec_ prefix included.
  3. Check the environment variable. Same name in your code and your host, set for the environment that serves the endpoint, and redeployed after you set it.
  4. Check the five-minute window. If your server's clock is off by more than five minutes, every signature looks expired. Sync the clock.
  5. Compare the header format. X-GrowWriter-Signature is t=<unix seconds>,v1=<64 hex characters>, and the signed string is "<t>.<raw body>".

The local test signs a request the same way GrowWriter does, so you can confirm your check without waiting for a real delivery.

The test succeeds but real articles fail

What it means. The ping is tiny. A real article carries its content three times (markdown, HTML, and blocks) and is typically a few hundred kilobytes; long articles can exceed 1 MB. Servers and frameworks often cap JSON bodies well below that.

Fix. Raise the body-size limit on the route (for example express.raw({ type: 'application/json', limit: '10mb' }); Express's default is 100 KB), and make sure your save logic handles the full payload, not just the fields the test used.

The test passes, but the first article fails with a 500 on Vercel or Netlify

What it means. Your handler writes a file next to your code. Serverless hosts mount that directory read-only, so the write throws (EROFS) and the endpoint answers 500. The test passes because the ping returns before any saving happens.

Fix. Store the article in a database, a CMS API, or an object store instead of a local file. The Next.js and Express examples in the guide log the post for exactly this reason; replace the log line with your storage call. If you only need a scratch file, /tmp is writable but empties between invocations.

I lost my signing secret

What it means. The secret is shown once, in the connect dialog, and is stored encrypted. GrowWriter cannot show it again.

Fix. Disconnect the webhook on the Channels page and connect it again. That creates a new secret and retires the old one: update the environment variable on your server, redeploy, then send a test event.

The article is live on my site, but my CMS admin shows a validation error

What you see. Something like Key on object value "growwriterId" is not allowed, or a date value refused.

What it means. Your endpoint saved a field your CMS doesn't declare (usually the GrowWriter id), or a value in a format the field doesn't accept (usually a full timestamp in a date-only field).

Fix. Add a dedicated field for the id to your content model, and convert dates to the format your CMS expects. Both are explained in Save the article into your CMS.

A post silently went back to draft

What it means. The payload has no published flag. If your CMS treats a missing draft field as "draft", the post is published only until someone saves it in the admin.

Fix. Set the published state yourself, explicitly, when you handle article.published and article.updated. See rule 3.

My build fails on { or < inside the article

What it means. Article text contains ordinary characters like {, } and <. If you compile the markdown through MDX, JSX, or a templating engine, those characters are read as code.

Fix. Store the markdown as data and render it with a Markdown renderer, or escape those characters before compiling. content.html is already escaped and safe to insert directly. See rule 6.

Images are huge, missing, or their URLs changed

  • Huge: images arrive at their original resolution and can exceed 10 MB. Resize and compress them when you download them.
  • Missing: coverImage is omitted when the article has no cover; images is an empty array when there are no inline images. Check for the key before reading it.
  • URLs changed: an image that was replaced in the article gets a new URL on the next delivery. Unchanged images keep their URLs.

All three are covered in rule 5.

I received the same article twice

  • Same delivery id: a retry. Your endpoint answered late or with an error on the first attempt. Skip work you've already done for that id.
  • Different delivery id, article.published again: the article was published to a second webhook connection, or the first connection had no successful delivery yet. Upsert on article.id and the second copy overwrites the first.

I deleted an article in GrowWriter, but it is still on my site

What it means. Deleting an article (or disconnecting the webhook) sends article.unpublished with the article's id, slug and language, and nothing else. If your handler treats every non-ping event as an article to save, it either ignored the event or tried to save an article with no content and answered 500.

Fix. Handle article.unpublished before the save: look up the post by article.id, then hide it or delete it, whichever your site prefers. Every receiver example in the guide has that branch. The event follows the normal delivery rules (three attempts, then nothing), so if your endpoint was down at the time the post stays; GrowWriter has no later retry.

The slug contains non-Latin characters, or didn't change with the title

The slug is derived from the title once, when the brief is generated. Latin accents are folded (qué → que), but letters from other scripts are kept, so a Japanese title produces a Japanese slug: percent-encode it before putting it in a URL. It is never empty. Editing the title in the editor keeps the slug, so the post's address stays stable; only picking a new title in the writer produces a new slug. Never use the slug as a file name or a key; use article.id, and treat the slug as display data.

"Too many requests. Please try again later"

What it means. Your account hit a limit: 20 publish requests per hour, or 60 test events per hour. Nothing is lost. The article stays in your Library and the connection stays as it was.

Fix. Wait a few minutes, then publish or send the test event again. If you publish to several channels at once, each one counts as a request.

The webhook row says "Not verified"

What it means. The endpoint has never answered a test event or a publish. GrowWriter checks the address when you connect but does not call it until you do.

Fix. Click Send test event on the webhook row. When it succeeds, the row turns Connected. If it fails, start with the status check, then redirects or rejected. Publishing to a Not verified webhook is allowed, and the first successful delivery verifies it too.

Still stuck?

Contact us from the app and include: the delivery id (from the X-GrowWriter-Delivery header or the error), the time of the attempt, the status your server returned, and the output of the status check. With those four things we can usually answer in one reply.

On this page