> ## Documentation Index
> Fetch the complete documentation index at: https://docs.spikeapi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> To receive data updates, configure a webhook endpoint via the admin console. After each data update from the provider, a webhook event will be sent to your endpoint.

```mermaid theme={null}
sequenceDiagram
    participant P as Provider
    participant S as Spike API
    participant W as Your Webhook
    
    P->>S: New health data received
    S->>S: Process and store data
    
    S->>+W: POST webhook event<br/>X-Body-Signature header
    
    alt Success (HTTP 200)
        W-->>S: 200 OK
        Note over S: Event delivered successfully
    else Failure (non-200, timeout, or network error)
        W-->>S: Error response
        Note over S: Retry after 5 seconds
        S->>+W: POST webhook event (retry)
        W-->>S: Error response
        Note over S: Retry after 2 minutes
        S->>+W: POST webhook event (retry)
        W-->>S: Error response
        Note over S: Continue retrying up to 10 attempts<br/>Final retries every 12 hours
        S->>+W: POST webhook event (final retry)
        W-->>S: Error response
        Note over S: Event discarded after all retries
    end
```

Your endpoint **must respond with HTTP 200** to acknowledge a successful receipt. If the request fails due to a network error, exceeds 30 seconds to complete or returns any status code other than 200, the system will retry the request **up to 10 times** with exponential backoff.
Retries are timed at first after 5 sec, then 2 min, 30 min, 2 hours and then the rest every 12 hours. After the final attempt, the event will be discarded.

## Webhook event Payload

* **application\_user\_id** The application user ID you've provided when getting the access token
* **timestamp** Time of the event
* **event\_type**
  * **record\_change** new or updated data received from the provider
  * **provider\_integration\_created** user has integrated with a provider
  * **provider\_integration\_deleted** user integration has been deleted
* **metrics** metric types involved with the event
* **activity\_types** activity types if any involved with the event
* **provider\_slug** provider triggering the event
* **earliest\_record\_start\_at** earliest timestamp of records involved with the event in ISO 8601 format
* **latest\_record\_end\_at** latest timestamp of records involved with the event in ISO 8601 format

### Example Payload

* **record\_change**

```json theme={null}
[
  {
    "application_user_id": "User1",
    "timestamp": "2025-04-15T13:33:55.271331177Z",
    "event_type": "record_change",
    "metrics": ["calories_burned_active", "distance", "steps"],
    "activity_types": ["sedentary", "walking"],
    "provider_slug": "garmin",
    "earliest_record_start_at": "2025-04-15T09:30:00Z",
    "latest_record_end_at": "2025-04-15T13:33:00Z"
  },
  {
    "application_user_id": "User2",
    "timestamp": "2025-04-15T13:33:55.271331177Z",
    "event_type": "record_change",
    "metrics": ["calories_burned_active"],
    "activity_types": ["walking"],
    "provider_slug": "garmin",
    "earliest_record_start_at": "2025-04-15T09:30:00Z",
    "latest_record_end_at": "2025-04-15T13:33:00Z"
  }
]
```

* **provider\_integration\_created**

```json theme={null}
[
  {
    "application_user_id": "test",
    "timestamp": "2025-08-29T13:21:10.703300387Z",
    "event_type": "provider_integration_created",
    "provider_slug": "oura"
  }
]
```

## Signature

Each webhook event is signed using an HMAC-SHA256 signature for verification. The signature is included in the **X-Body-Signature** header.

The signature is computed by signing the raw request body **as-is** using a shared secret key. You can retrieve this key from the admin console.

To verify authenticity:

1. Compute the HMAC-SHA256 hash of the request body using the shared key.
2. Compare the result to the value in the X-Body-Signature header.

<img src="https://mintcdn.com/trial-7e3c8695/tLxMhymXagiR2q29/images/console_client_webhook.png?fit=max&auto=format&n=tLxMhymXagiR2q29&q=85&s=53d1b31b647c42fd2a166e9313c8eb78" alt="Console Client Webhook Pn" width="1604" height="716" data-path="images/console_client_webhook.png" />

## Code Examples

<CodeGroup>
  ```go [Go] theme={null}
  package main

  import (
  	"crypto/hmac"
  	"crypto/sha256"
  	"encoding/hex"
  	"encoding/json"
  	"fmt"
  	"io"
  	"net/http"
  	"time"
  )

  // PushEvent represents a webhook event from the health data provider
  const hmacKey = "HMAC_KEY_FROM_ADMIN_CONSOLE"

  type PushEvent struct {
  	ApplicationUserID     string    `json:"application_user_id"`      // ID of the application user
  	Timestamp             time.Time `json:"timestamp"`                // Event timestamp
  	EventType             string    `json:"event_type"`               // Type of event
  	Metrics               []string  `json:"metrics"`                  // List of metrics
  	ActivityTypes         []string  `json:"activity_types"`           // List of activity types
  	ProviderSlug          string    `json:"provider_slug"`            // Provider identifier
  	EarliestRecordStartAt time.Time `json:"earliest_record_start_at"` // Start of data range
  	LatestRecordEndAt     time.Time `json:"latest_record_end_at"`     // End of data range
  }

  func main() {
  	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  		// Verify HMAC signature
  		signature := r.Header.Get("X-Body-Signature")
  		if signature == "" {
  			http.Error(w, "Missing signature", http.StatusBadRequest)
  			return
  		}

  		// Read and verify the request body
  		body, err := io.ReadAll(r.Body)
  		if err != nil {
  			http.Error(w, "Failed to read body", http.StatusInternalServerError)
  			return
  		}

  		// Calculate HMAC
  		hm := hmac.New(sha256.New, []byte(hmacKey))
  		hm.Write(body)
  		if signature != hex.EncodeToString(hm.Sum(nil)) {
  			http.Error(w, "Invalid signature", http.StatusUnauthorized)
  			return
  		}

  		// Parse events
  		var events []PushEvent
  		if err := json.Unmarshal(body, &events); err != nil {
  			http.Error(w, "Invalid JSON", http.StatusBadRequest)
  			return
  		}

  		// Process events
  		for _, event := range events {
  			fmt.Printf("Received event: %+v\n", event)
  		}

  		w.WriteHeader(http.StatusOK)
  		w.Write([]byte("OK"))
  	})

  	fmt.Println("Starting server on port 8000")
  	http.ListenAndServe(":8000", nil)
  }
  ```

  ```javascript [JavaScript] theme={null}
  const crypto = require("crypto");

  const HMAC_KEY = "HMAC_KEY_FROM_ADMIN_CONSOLE";

  class PushEvent {
    constructor(data) {
      this.application_user_id = data.application_user_id;
      this.timestamp = new Date(data.timestamp);
      this.event_type = data.event_type;
      this.metrics = data.metrics;
      this.activity_types = data.activity_types;
      this.provider_slug = data.provider_slug;
      this.earliest_record_start_at = new Date(data.earliest_record_start_at);
      this.latest_record_end_at = new Date(data.latest_record_end_at);
    }
  }

  const server = require("http").createServer((req, res) => {
    if (req.method !== "POST") {
      res.statusCode = 405;
      res.end("Method not allowed");
      return;
    }

    let body = "";
    req.on("data", (chunk) => (body += chunk));
    req.on("end", () => {
      // Verify signature
      const signature = req.headers["x-body-signature"];
      if (!signature) {
        res.statusCode = 400;
        res.end("Missing signature");
        return;
      }

      // Calculate HMAC
      const hmac = crypto.createHmac("sha256", HMAC_KEY);
      hmac.update(body);
      if (signature !== hmac.digest("hex")) {
        res.statusCode = 401;
        res.end("Invalid signature");
        return;
      }

      // Parse events
      let events = JSON.parse(body).map((data) => new PushEvent(data));

      // Process events
      for (const event of events) {
        console.log("Received event:", event);
      }

      res.statusCode = 200;
      res.end("OK");
    });
  });

  server.listen(8000);
  ```

  ```php [PHP] theme={null}
  <?php

  class PushEvent {
      public string $application_user_id;
      public DateTime $timestamp;
      public string $event_type;
      public array $metrics;
      public array $activity_types;
      public string $provider_slug;
      public DateTime $earliest_record_start_at;
      public DateTime $latest_record_end_at;
  }

  const HMAC_KEY = 'HMAC_KEY_FROM_ADMIN_CONSOLE';

  function handleRequest() {
      // Verify HMAC signature
      $signature = $_SERVER['HTTP_X_BODY_SIGNATURE'] ?? '';
      if (empty($signature)) {
          http_response_code(400);
          echo 'Missing signature';
          return;
      }

      // Read and verify request body
      $body = file_get_contents('php://input');
      if ($body === false) {
          http_response_code(500);
          echo 'Failed to read body';
          return;
      }

      // Calculate HMAC
      $hmac = hash_hmac('sha256', $body, HMAC_KEY);
      if ($signature !== $hmac) {
          http_response_code(401);
          echo 'Invalid signature';
          return;
      }

      // Parse events
      $events = json_decode($body, true);
      if (json_last_error() !== JSON_ERROR_NONE) {
          http_response_code(400);
          echo 'Invalid JSON';
          return;
      }

      // Process events
      foreach ($events as $eventData) {
          $event = new PushEvent();
          $event->application_user_id = $eventData['application_user_id'];
          $event->timestamp = new DateTime($eventData['timestamp']);
          $event->event_type = $eventData['event_type'];
          $event->metrics = $eventData['metrics'];
          $event->activity_types = $eventData['activity_types'];
          $event->provider_slug = $eventData['provider_slug'];
          $event->earliest_record_start_at = new DateTime($eventData['earliest_record_start_at']);
          $event->latest_record_end_at = new DateTime($eventData['latest_record_end_at']);

          echo "Received event: " . print_r($event, true) . "\n";
      }

      http_response_code(200);
      echo 'OK';
  }

  // Handle the request
  handleRequest();
  ```

  ```python [Python] theme={null}
  import hmac
  import hashlib
  import json
  from datetime import datetime
  from http.server import HTTPServer, BaseHTTPRequestHandler
  from typing import List, Optional


  class PushEvent:
      """Represents a health data push event from a provider."""
      def __init__(self, data: dict):
          self.application_user_id: str = data['application_user_id']
          self.timestamp: datetime = datetime.fromisoformat(data['timestamp'])
          self.event_type: str = data['event_type']
          self.metrics: List[str] = data['metrics']
          self.activity_types: List[str] = data['activity_types']
          self.provider_slug: str = data['provider_slug']
          self.earliest_record_start_at: datetime = datetime.fromisoformat(data['earliest_record_start_at'])
          self.latest_record_end_at: datetime = datetime.fromisoformat(data['latest_record_end_at'])


  class RequestHandler(BaseHTTPRequestHandler):
      """Handles incoming webhook requests with HMAC verification."""

      HMAC_KEY = "HMAC_KEY_FROM_ADMIN_CONSOLE"

      def _verify_signature(self, body: bytes, signature: Optional[str]) -> bool:
          """Verifies the HMAC signature of the request body."""
          if not signature:
              return False
          h = hmac.new(self.HMAC_KEY.encode(), body, hashlib.sha256)
          return signature == h.hexdigest()

      def do_POST(self):
          """Handles POST requests with webhook events."""
          # Read request body
          content_length = int(self.headers.get('Content-Length', 0))
          body = self.rfile.read(content_length)

          # Verify signature
          if not self._verify_signature(body, self.headers.get("X-Body-Signature")):
              self.send_error(401, "Invalid signature")
              return

          # Parse and process events
          try:
              events = [PushEvent(data) for data in json.loads(body)]
              for event in events:
                  print(f"Received event: {event.__dict__}")
          except (json.JSONDecodeError, KeyError, ValueError):
              self.send_error(400, "Invalid JSON")
              return

          # Send success response
          self.send_response(200)
          self.end_headers()
          self.wfile.write(b"OK")


  def main():
      """Starts the HTTP server."""
      server = HTTPServer(('', 8000), RequestHandler)
      server.serve_forever()


  if __name__ == "__main__":
      main()
  ```

  ```typescript [TypeScript] theme={null}
  import { createHmac } from "crypto";
  import { createServer, IncomingMessage, ServerResponse } from "http";

  // Configuration
  const HMAC_KEY = "HMAC_KEY_FROM_ADMIN_CONSOLE";
  const PORT = 8000;

  // Type definitions
  interface PushEvent {
    application_user_id: string;
    timestamp: Date;
    event_type: string;
    metrics: string[];
    activity_types: string[];
    provider_slug: string;
    earliest_record_start_at: Date;
    latest_record_end_at: Date;
  }

  // Helper functions
  const verifySignature = (
    body: string,
    signature: string | undefined
  ): boolean => {
    if (!signature) return false;
    const hmac = createHmac("sha256", HMAC_KEY);
    hmac.update(body);
    return signature === hmac.digest("hex");
  };

  const parseEvents = (body: string): PushEvent[] => {
    return JSON.parse(body).map((data: any) => ({
      ...data,
      timestamp: new Date(data.timestamp),
      earliest_record_start_at: new Date(data.earliest_record_start_at),
      latest_record_end_at: new Date(data.latest_record_end_at),
    }));
  };

  // Server setup
  const server = createServer((req: IncomingMessage, res: ServerResponse) => {
    // Only accept POST requests
    if (req.method !== "POST") {
      res.statusCode = 405;
      res.end("Method not allowed");
      return;
    }

    // Collect request body
    let body = "";
    req.on("data", (chunk) => (body += chunk));

    req.on("end", () => {
      try {
        // Verify request signature
        if (
          !verifySignature(
            body,
            req.headers["x-body-signature"] as string | undefined
          )
        ) {
          res.statusCode = 401;
          res.end("Invalid signature");
          return;
        }

        // Parse and process events
        const events = parseEvents(body);
        events.forEach((event) => console.log("Received event:", event));

        // Send success response
        res.statusCode = 200;
        res.end("OK");
      } catch (error) {
        // Handle parsing errors
        res.statusCode = 400;
        res.end("Invalid request");
      }
    });
  });

  // Start server
  server.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
  });
  ```
</CodeGroup>
