Twitch-cli EventSub test passes but API responds webhook callback verification failed

I have a verification endpoint that correctly responds with the verification request as per the Twitch CLI:

$ twitch event verify-subscription channel.follow -F https://[redacted]/ -s dfsion43jdghjyt5mry
✔ Valid response. Received challenge 26bb188c-3cd8-de37-05f6-4c57f4dc848f in body
✔ Valid content-type header. Received type text/plain
✔ Valid status code. Received status 200

But when I use that same endpoint with the API it doesn’t work:

$ curl -X POST 'https://api.twitch.tv/helix/eventsub/subscriptions' \
	-H 'Authorization: Bearer [redacted] \
	-H 'Client-Id: [redacted]' \
	-H 'Content-Type: application/json' \
	-d '{"type":"channel.follow","version":"1","condition":{"broadcaster_user_id":"468236963"},"transport":{"method":"webhook","callback":"https://[redacted]/","secret":"dfsuh437yfuhi438fgh9"}}'

That responds with:

{"data":[{"id":"1402eb0d-799f-4d1a-9674-4363c3a3bf2d","status":"webhook_callback_verification_pending","type":"channel.follow","version":"1","condition":{"broadcaster_user_id":"468236963"},"created_at":"2022-01-03T04:21:51.269457833Z","transport":{"method":"webhook","callback":"https://[redacted]/"},"cost":1}],"total":1,"max_total_cost":10000,"total_cost":1}

Then if I list out my subs:

$ curl -X GET 'https://api.twitch.tv/helix/eventsub/subscriptions' \
	-H 'Authorization: Bearer [redacted]' \
	-H 'Client-Id: [redacted]'

It comes back failed:

{"total":1,"data":[{"id":"1402eb0d-799f-4d1a-9674-4363c3a3bf2d","status":"webhook_callback_verification_failed","type":"channel.follow","version":"1","condition":{"broadcaster_user_id":"468236963"},"created_at":"2022-01-03T04:21:51.269457833Z","transport":{"method":"webhook","callback":"https://[redacted]/"},"cost":0}],"max_total_cost":10000,"total_cost":0,"pagination":{}}

So far as I understand the webhook verification should only fail if something is sufficiently wrong to cause the Twitch CLI to not pass, but the Twitch CLI is passing every time.

That’s my broadcaster id, and the OAuth token and Client ID match and are from my account.

I don’t understand what’s wrong.

You endpoint [redacted] does use a Real SSL Cert and not a self signed one?

A self signed may pass in the CLI as the certificate can be installed and trusted to the system and your CLI won’t be able to tell the difference, but a self signed won’t work for real eventsub.

Is your server/script logging the unbound requests from the internet (both at whatever is in front of your script and the script itself?)

Is your endpoint actually accessable publically? (Cli running on the same machine as the endpoint means it will naturally be accessable)

Yes. It’s a valid SSL cert generated with LetsEncrypt/certbot.

I’m not sure I understand the question. Are the received requests being written to a log file? Yes. It’s running on nginx which logs everything, but my script does not.

The nginx logs don’t affect the HTTP response, however, so I don’t think I understand what you’re asking.

Yes. It’s running on a VPS with ports 80 and 443 accessible to the world.

Twitch will make a call to you. (or anyone loading the webpage/website)

This will then appear in the nginx access logs.
Nginx they will pass the request over to your c script handler.
And then the C Script can log the request attempt as well.

I’m asking if NGINX is seeing Twitch calling your server.

You mentioned that you were using Cloudflare (over on github), if you changed this to make cloudflare Just pass the request over, cloudflare may still be blocking the request Twitch makes, as it erroneously things Twitch is a “bad script/bot”

So I’m asking if you are seeing anything in your NGINX access log, to confirm that NGINX got the hit and it wasn’t blocked by Cloudflare or other firewall type software.

If NGINX saw it, but your script did not then you need to solve why NGINX didn’t pass the request to your script.

So this reads to me as

  • yes nginx saw a POST request from Twitch
  • my script did not see a POST request from Twitch.

So this would suggest a NGINX misconfiguration

Yes. It does.

Here’s the last 5 lines of the access.log file before attempting to register the event:

ubuntu@twitchevt:~$ sudo tail -n 3 /var/log/nginx/access.log
157.245.176.143 - - [03/Jan/2022:15:06:42 +0000] "SSTP_DUPLEX_POST /sra_{BA195980-CD49-458b-9E23-C84EE0ADCD75}/ HTTP/1.1" 400 166 "-" "-"
45.146.165.37 - - [03/Jan/2022:15:40:17 +0000] "GET /console/ HTTP/1.1" 200 5 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"
216.252.208.243 - - [03/Jan/2022:15:49:14 +0000] "POST / HTTP/1.1" 200 36 "-" "twitch-cli/1.1.5"

And immediately after trying to register the event:

ubuntu@twitchevt:~$ sudo tail -n 3 /var/log/nginx/access.log
45.146.165.37 - - [03/Jan/2022:15:40:17 +0000] "GET /console/ HTTP/1.1" 200 5 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"
216.252.208.243 - - [03/Jan/2022:15:49:14 +0000] "POST / HTTP/1.1" 200 36 "-" "twitch-cli/1.1.5"
52.12.79.235 - - [03/Jan/2022:15:50:07 +0000] "POST / HTTP/1.1" 200 36 "-" "Go-http-client/1.1"

I am no longer using Cloudflare because the Twitch API is not capable of handling an HTTP response if UTF-8 encoding is specified. Cloudflare does not, so far as I can tell, have a way to disable the UTF-8 encoding from being added, so it’s not compatible with Twitch.

Anything nginx receives is passed to my script via a proxy_pass call. My script just doesn’t log anything—it doesn’t write received requests to a log file because nginx is already doing that.

I only meant my script doesn’t log the requests. It is absolutely receiving them.

Checking your code in the other post

Since it’s passing the CLI test, but not “real” EventSub

The only thing I can think of, at this point, is that you have hard coded the Content Length header and “real eventsub” used a different length/format of challenge to what the CLI uses.

As the cli here is just generating what amounts to a GUID, but real eventsub will use whatever it wants and expects you to return the challenge as it sent the challenge to you, regardless of the length/format of that challenge.

The specific format/length of a challenge is not documented it could change at any point, and even be a function of your declared shared secret when creating the subscription.

So you could update your C script to log the inbound challenge and check what you are sending back to “real eventsub”, in case real eventsub is not sending a challenge of length 36.

I am doing that, actually.

Every single request I received from the twitch-cli had a challenge payload of 36 characters so I hard-coded the length to avoid what I thought was an unneeded parsing step.

So I’ve updated my script to calculate the length of the challenge as received and it’s now working.

Thanks.