Webhook callback routine not receiving calls

Photon is not receiving callbacks. The GET to the target site succeeds, and returns desired data. But the routine that has subscribed is not getting called. This is code that previously worked, but has stopped sometime in the past week, not sure exactly when. From the CLI particle subscribe:

{"data":"73.142.34.33","ttl":60,"published_at":"2026-09-18T14:13:39.090Z","coreid":"3f002c001047353138383138","name":"utc_offset"}
{"data":"","ttl":60,"published_at":"2026-09-18T14:13:39.151Z","coreid":"particle-internal","name":"hook-sent/utc_offset"}
{"data":"utc_offset: -0400\ntime_zone:America/New_York\nip:54.86.143.216","ttl":60,"published_at":"2026-09-18T14:13:39.251Z","coreid":"particle-internal","name":"hook-response/get_utc_offset_3f002c001047353138383138/0"}

{"data":"false","ttl":60,"published_at":"2026-09-18T14:13:39.864Z","coreid":"3f002c001047353138383138","name":"particle/device/updates/pending"}
{"data":"{"device":{"network":{"signal":{"at":"Wi-Fi","strength":74,"strength_units":"%","strengthv":-63,"strengthv_units":"dBm","strengthv_type":"RSSI","quality":64.51,"quality_units":"%","qualityv":29,"qualityv_units":"dB","qualityv_type":"SNR"},"connection":{"status":"connected","error":0,"disconnects":0,"attempts":1,"disconnect_reason":"none"}},"cloud":{"connection":{"status":"connected","error":0,"attempts":1,"disconnects":0,"disconnect_reason":"none"},"coap":{"transmit":0,"retransmit":0,"unack":0,"round_trip":0},"publish":{"rate_limited":0}},"system":{"uptime":7,"memory":{"used":32648,"total":82944}}},"service":{"device":{"status":"ok"},"cloud":{"uptime":2,"publish":{"sent":3}},"coap":{"round_trip":69}}}","ttl":60,"published_at":"2026-09-18T14:13:40.810Z","coreid":"3f002c001047353138383138","name":"spark/device/diagnostics/update"}

What is your device code? If you can't share the whole thing, what does the subscription code look like?

Code follows. I have included the setup code from the main .ino, followed by the entire file that includes the functions that trigger the hook and should receive the callback.

#include "hardware.h"
#include "timezone.h"

int tzStatus = -1; // -1, failed to get tz, otherwise okay

void setup() {
ipaddr_setup(); // get the ip address to get timezone and utc offset
pinMode(LE_MIN, OUTPUT);
pinMode(LE_HR, OUTPUT);
pinMode(COLON, OUTPUT);
// SPI used to load values into LED drivers
SPI.setDataMode(SPI_MODE0);
SPI.setBitOrder(MSBFIRST);
SPI.begin();

Serial.begin(9600);
Serial.print("serial connected");
Particle.subscribe("hook-response/utc_offset", get_utc_offset);

// these are functions to control/monitor clock
Particle.function("webTZ" , webTZ);
Particle.variable("ipaddr", ipaddr);		// response from asking for ip from particle/device/ip
Particle.variable("ipdata", ipdata);		// response from asking for timezone name from web
Particle.variable("tzName", tzName);      // time zone name extracted from ipdata
Particle.variable("tzOffset", tzOffset);     // offset extracted from  tzdb
Particle.variable("tz", tz);
Particle.variable("tzStatus", tzStatus);
Particle.variable("utc_offset_called", get_utc_offset_called); //DEBUG

Particle.syncTime();
tzStatus=setTZ();
} // end setup()

============================================================

//
// /\*  module figures out time zone from ip address \*\*\*\*\*\*\*\*\*\*\*\*
//   http://ipapi.co is used to get timezone name and utc offset
//   this is done through the Particle webhook facility
//   \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
#include 
#include "spark_wiring_string.h"
#include "Particle.h"

String ipdata;
String tzName;      // time zone name extracted web response
String tzOffset;     // offset extracted from web response
String ipaddr;      // public ipaddress for this device
double tz = -4.; // timezone offset from UTC in hours
// automatic timezone setting,  default is on, can be off via web function
int autoTZ = 1;
volatile bool callBack = false;
int get_utc_offset_called = 0;  //DEBUG

int setTZ() {
// returns status  -1 fail, 1 success
// if successful, will have set argument tz to timezone
// const char \*cipaddr = ipaddr.c_str();
// Particle.publish("utc_offset", cipaddr, PRIVATE);
callBack = false;
Particle.publish("utc_offset", ipaddr, PRIVATE);
unsigned long start = millis();
while (!callBack && millis() - start < 10000) {
Particle.process();
delay(50);
}
if (callBack || !autoTZ)
return 1;
// return -1;
return 1; //debug to keep from repeated calls
}

void get_utc_offset(const char \*event, const char \*data) {
//  call back from Particle webhook handler
// Gets the data from ipapi.co and sets timezone utc offset
get_utc_offset_called = 1;
ipdata = String(data);
// extract utc_offset, return as String
int beg = ipdata.indexOf("utc_offset:") + strlen("utc_offset:") ;
int end = ipdata.indexOf("\\n", beg);
tzOffset = ipdata.substring(beg, end);
int tzlen = tzOffset.length();
tz = tzOffset.substring(0, tzlen-2).toFloat() +
tzOffset.substring(tzlen-2).toFloat()/60.;
// set global variable tzname from web data
beg = ipdata.indexOf("time_zone:") + strlen("time_zone:");
// int end = ipdata.indexOf(",", beg)-1;
tzName = ipdata.substring(beg);
if (autoTZ) {  // auto tz has been overwritten from the web
Time.zone(tz);
}
callBack = true;
}

int webTZ(String tzval) {  // only called from particle console to override automatic
// override automatic time zone
// tzval is GMT offset, if "off", go back to automatic
if (tzval.equals("off") ) {
autoTZ = 1;
setTZ();
} else {
autoTZ = 0;
tz = tzval.toFloat();
Time.zone(tz);
}
return int(10. \* tz);  // can only return int
}

void get_ip_callback(const char \*topic, const char \*data) {
// callback from particle.io device/ip
// ipaddr =  "{"ipaddr":"" + String(data) + ""}";
ipaddr =  data;
callBack = true;
}

void ipaddr_setup() {
Particle.subscribe("particle/device/ip", get_ip_callback);
callBack = false;
Particle.publish("particle/device/ip");
unsigned long start = millis();
while (!callBack && millis() - start < 10000) {
Particle.process();
delay(50);
}
}

There are a couple issues, but the main one is that you can't subscribe then immediately publish in current versions of Device OS. Because the subscriptions are deferred until a future time between loop calls, if you immediately publish the subscription won't have been made yet, so you won't get the response.

The other issue is that syncTime is asynchronous, so if you check for the callback immediately after calling it, it won't have completed yet. Also, a sync time is done after every cloud connection, so calling it immediately after connecting is never necessary if you register your callback early enough.

I suspect that if this exact code worked before, it's because it was running with system thread disabled, which probably caused the asynchronous code to execute almost synchronously and it happened to work. You could try disabling threading to see if that makes it work again.

The best solution is to reorganize your code into a finite state machine so the asynchronous parts can execute without blocking loop.

Rick,

If I subscribe in setup(), can I publish at the top of loop()? If not, how do I know when the subscribe has taken effect? I didn't get your comment on syncTime, since I am not doing a callback based on it.

David

What you should do is subscribe in setup(). Then wait in loop() until Particle.connected() is true, then do anything that requires publishing. The subscription handlers will be set by then.

Sorry, I now see that callback is set from the IP address get_ip_callback, so that should be OK, as long as you wait until connected before publishing. But you still don't need to call Particle.syncTime(). Just wait until Time.isValid() is true.

Moved the publish into loop(). eliminated syncTime(). Not waiting for Particle.connected() to be true. That cannot be the problem with the callback since the publish to making it out to the web through the webhook. In addition I am now calling setTz() in once per minute in loop(), if it fails, and the callback to get_utc_offset() never happens.

The reason you have to wait for Particle.connected is because Particle.publish will block until cloud connected, then it will immediately publish. But because you just connected, the subscription has not occurred yet.

I would think that if I need to wait for connected before I subscribe, then I should wait in setup, not in loop. I have now done that, and it makes no difference, still not getting the callback. As I noted above, I can repeat the call to publish once a minute and still not get the callback, so it could not have been a race condition

You need to wait for connected before publish, but not before subscribe. Subscribe queues the subscriptions, publish does not.

But now that I look more carefully I think the problem is that you're subscribing to "hook-response/utc_offset" but the event name in the log is "hook-response/get_utc_offset".

That was the problem. Thank you for your help