[SOLVED] Webhook subscription never receiving topics or showing up in subscriptions endpoint

I know this is similar to other posts, but I read them all and none of the other posts have a solution that works for me. I develop REST APIs for a living, so I am not new to this at all, but I am totally stumped. I am trying to make webhook subscriptions with a Django webserver.
I can request the subscription just fine. Twitch sends back a 202, and my server gets the challenge request. I send a Response with the challenge string and a 200 status, but then I never get any topics. Also when I poll the https://api.twitch.tv/helix/webhooks/subscriptions endpoint it always shows 0 subscriptions.

I have checked my nginx logs and never get a POST request from Twitch to any subscription endpoint I requested. I have tried using and not using trailing slashes on the callback url, but have the exact same results. Does anyone know what I am doing wrong?

Here is my callback handler code:

    @action(methods=['get', 'post'], detail=True, permission_classes=[AllowAny])
    def twitchstreamstatus(self, request, pk=None):
        """Change the status of a stream.

        Also handle Twitch stream webhook challenges.
        """
        logger.debug('Subscription webhook call for stream %s', pk)
        try:
            stream = TwitchStream.objects.get(pk=pk)
        except TwitchStream.NotFound:
            logger.warning('Stream %s called by Twitch webhook but not found',
                           pk)
            return Response()

        # This is a subscription verification challenge
        if request.method == 'GET':
            logger.info(f'Challenge request for stream {pk}: {stream.display_name}')
            token = request.query_params.get('hub.challenge').rstrip()
            logger.debug('Returning token response: %s', token)
            return Response(token)

        data = request.data.copy()['data']
        if not data or data == []:
            logger.debug('Stream offline callback for stream %s', stream.name)
            stream.go_offline()
            return Response()

        logger.debug('Stream online callback for stream %s', stream.name)
        stream.go_online(data[0]['game_id'])
        return Response()

and this is my subscription request code, which works since I am getting the challenge request just fine:

    headers = {
        'Client-ID': settings.TWITCH_CLIENT_ID,
        'Content-Type': 'application/json'
    }
    sub_url = 'https://api.twitch.tv/helix/webhooks/hub'
    payload = {
        'hub.callback': f'{settings.SS_API_URL}/streams/{stream.id}/twitchstreamstatus/',
        'hub.mode': 'subscribe',
        'hub.topic': f'https://api.twitch.tv/helix/streams?user_id={stream.twitch_id}',
        'hub.lease_seconds': int(os.environ.get('TWITCH_WEBHOOK_LEASE_SECONDS', 864000)),
        'hub.secret': settings.TWITCH_WEBHOOK_SECRET
    }
    logger.debug('Subscription payload: %s', payload)
    r = requests.post(sub_url, json=payload, headers=headers)

Any help is very much appreciated.

With how far you’re getting, and the fact that the subscriptions endpoint has 0 subscriptions, are you sure the lease time is being set correctly? For example it seems you’re using an env variable, is it set and if so is it greater than 0?

I’m not all that familiar with Python so just suggesting what comes to mind, devs more experienced in the language will be better able to see if there’s any syntax or functionality issues which may be at fault.

What content-type are you using for echoing the challenge? it has to be text/plain. application/json wont work and will end in your subscription not being created

2 Likes

Thanks for the reply. Yeah I am sure the lease time is greater than 0. I have it set to 864000 be default and in my tests its being set to 86400.

I will check this when I get a second and be sure. It could be sending back json data as the default. Thanks for the idea.

Thank you Syzuna! It was defaulting to a json response rather than text/plain. Forcing the content type fixed the issue. Much appreciated!!

For any other python Django people looking for this, you need to use the Django HttpResponse rather than the DRF Response class and specify the content type as text/plain like so:
HttpResponse('your string', content_type='text/plain')

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.