Guide to Spike API Webhooks: Features and Best Practices
Spike API gives developers one API for health and fitness data from wearables and health platforms such as Garmin and Oura. Its webhooks don't carry the health records themselves. They tell you that a user's data has changed, which metrics and time range are affected, and when users connect or disconnect a provider, so you know when and what to fetch.
Covered here: Spike's single endpoint per application, the batched array payload, X-Body-Signature verification, and a retry policy that treats anything other than exactly 200 as a failure.
What are Spike API webhooks?
Each Spike application has one webhook endpoint, set in the admin console. Spike sends every event type to it, with no choice of subscriptions. After each data update from a provider, Spike posts a notification. A record_change lists the metrics and activity types involved and the earliest and latest record times, and you call the Spike API to fetch the data for that range.
The body is a JSON array, and a single delivery can contain events for several users.
Spike API webhook features
| Feature | Details |
|---|---|
| Signature | hex HMAC-SHA256 |
| Signature header | x-body-signature |
| Body | JSON |
| Event routing | Body field [].event_type |
From Spike's webhooks documentation:
- One endpoint per application, configured in the admin console.
- Requests are signed with a shared key, also from the admin console, in
X-Body-Signature. - Your endpoint must respond with HTTP 200. Any other status, a network error or a request taking over 30 seconds counts as a failure.
- Failed deliveries are retried up to 10 times: after 5 seconds, 2 minutes, 30 minutes, 2 hours, then every 12 hours. After the last attempt the event is discarded.
Events and payloads
| Event routing | Body field [].event_type |
| Field | Description |
|---|---|
application_user_id | The application user ID you provided when getting the access token |
timestamp | Time of the event |
event_type | One of the three types above |
metrics | Metric types involved (record_change) |
activity_types | Activity types involved, if any (record_change) |
provider_slug | Provider triggering the event, e.g. garmin, oura |
earliest_record_start_at | Earliest timestamp of the records involved (ISO 8601) |
latest_record_end_at | Latest timestamp of the records involved (ISO 8601) |
A provider_ delivery from Spike's docs:
[
{
"application_user_id": "test",
"timestamp": "2025-08-29T13:21:10.703300387Z",
"event_type": "provider_integration_created",
"provider_slug": "oura"
}
]
A record_change delivery in Spike's docs holds two events for two different users, both from garmin, with metrics such as steps and distance, activity_types such as walking, and the record range in ISO 8601. Spike doesn't publish an example for provider_, so log the first one you receive. There's no event ID in any of them.
Setting up Spike API webhooks
- In the Spike admin console, set the webhook URL for your application, for example
https://example.com/webhooks/spike-api. - Copy the shared HMAC key from the admin console and store it as
SPIKE_WEBHOOK_KEY. Use it as a plain string. - Connect a test user to a provider to trigger
provider_.integration_ created
To route through Hookdeck, create a Spike API source with the shared key and set the source URL as your endpoint. For local development:
npx hookdeck-cli listen 3000 spike-api --path /webhooks/spike-api
Securing Spike API webhooks
X-Body-Signature is the hex HMAC-SHA256 of the raw body, signed "as-is" with the shared key. Spike's Go, JavaScript and PHP samples all compare the bare hex digest. Nothing time-based is signed.
Node.js verification
const crypto = require('node:crypto');
function verifySignature(rawBody, headers, secret) {
if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
const signature = headers['x-body-signature'];
if (typeof signature !== 'string') return false;
const key = Buffer.from(secret, 'utf8');
const message = rawBody;
const candidates = [signature];
const expected = crypto.createHmac('sha256', key).update(message).digest();
return candidates.some((candidate) => {
if (!/^[0-9a-fA-F]{64}$/.test(candidate)) return false;
const received = Buffer.from(candidate, 'hex');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
The handler checks that the body is an array, queues each event, and returns exactly 200. A 400 for a malformed body is still a non-200, so Spike will retry it, but a body that fails to parse won't parse on a retry either.
const express = require('express');
const app = express();
app.post('/webhooks/spike-api', express.raw({ type: '*/*' }), async (req, res) => {
const secret = process.env.SPIKE_WEBHOOK_KEY;
if (!secret) return res.sendStatus(500);
if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);
let events;
try {
events = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
if (!Array.isArray(events)) return res.sendStatus(400);
const jobs = events
.filter((e) => e && typeof e === 'object' && typeof e.event_type === 'string')
.map((e) => ({
type: e.event_type,
userId: e.application_user_id,
provider: e.provider_slug,
metrics: Array.isArray(e.metrics) ? e.metrics : [],
from: e.earliest_record_start_at,
to: e.latest_record_end_at,
}));
try {
await queue.addBulk(jobs.map((data) => ({ name: 'spike-event', data })));
} catch {
return res.sendStatus(503);
}
res.status(200).end();
});
The worker fetches records for record_change events and updates the connection status for the integration events.
The Spike API source type in Hookdeck applies this same verification. It returns 200 to Spike straight away and records any mismatch as failed verification. Spike signs no timestamp, and Hookdeck adds no time window.
Make Spike API webhooks production-ready. Hookdeck Event Gateway returns the exact
200Spike expects and keeps every notification, so a long outage doesn't run out Spike's ten retries and lose data updates.
Spike API webhook limitations and pain points
Only HTTP 200 counts
The Problem: Endpoints that answer 202 Accepted or 204 No Content get retried.
Why It Happens: Spike treats any status other than 200 as a failure, not any 2xx.
Workarounds:
- Return exactly
200, even for events you ignore. - Check frameworks and proxies that might rewrite the status.
How Hookdeck Can Help: Hookdeck replies 200 to Spike for every delivery, and your service can respond with whatever status suits it.
Events are discarded after ten retries
The Problem: An outage of a few days loses data change notifications.
Why It Happens: After the first retries, Spike waits 12 hours between attempts and discards the event after the tenth.
Workarounds:
- Fetch recent data per user from the API after any outage.
- Monitor your endpoint's error rate so you notice quickly.
How Hookdeck Can Help: Hookdeck stores the notification on first delivery and retries your service on your schedule, with replay for anything that failed.
Batched arrays and no event ID
The Problem: One request holds several events, and nothing identifies an event uniquely.
Why It Happens: The body is an array, and events carry only user, type, provider, timestamps and ranges.
Workarounds:
- Process each element independently, so one bad element doesn't fail the batch.
- Make fetches idempotent: re-fetching a time range for a user should overwrite, not append.
How Hookdeck Can Help: Hookdeck keeps the full array for each request, so after fixing the element that failed you can replay the whole batch.
No subscription choice
The Problem: You receive every event type, whether you need it or not.
Why It Happens: Spike has one endpoint per application and no per-event settings.
Workarounds:
- Ignore unwanted types and return
200. - Route by
event_typeinside your service.
How Hookdeck Can Help: Hookdeck filters can send record_change and the integration events to different destinations.
Best practices
Fetch by range, not by event
Use earliest_record_start_at and latest_record_end_at to fetch only the changed window for that user and those metrics.
Coalesce bursts
Several record_change events for the same user can arrive close together. Merge overlapping ranges in your worker before calling the API. The async processing guide covers queue design.
Make processing repeatable
Retries resend the same array. Upsert fetched records by their own IDs so a second run changes nothing. See handling duplicate webhooks with idempotency.
Track integration state
Keep a per-user, per-provider status from provider_ and _deleted, and stop fetching for deleted integrations.
Conclusion
Spike API webhooks notify you of wearable data changes and provider connections, in batched arrays signed with a hex HMAC-SHA256 in X-Body-Signature. Answer with exactly 200 within 30 seconds, fetch the changed range through the API, and plan for events being dropped after ten retries.
Hookdeck Event Gateway acknowledges Spike correctly every time and holds notifications until your service is ready.
Get started with Hookdeck for free and handle Spike API webhooks reliably in minutes.