Ionhour Docs

Integration Examples

Ready-to-use code snippets for Node.js, Python, Go, shell scripts, CI/CD, and containers.

Copy-paste examples for integrating IonHour heartbeat pings into your services and pipelines. All examples use the public ping endpoint which requires no authentication.

Replace YOUR_TOKEN with the token from your check's settings.

Shell / Cron

Basic Crontab

# Run backup every hour, ping on success
0 * * * * /usr/local/bin/backup.sh && curl -sf https://app.failsignal.com/api/signals/ping/YOUR_TOKEN > /dev/null 2>&1

With Retry

# Retry the ping up to 3 times with 5-second delay between attempts
0 * * * * /usr/local/bin/backup.sh && curl -sf --retry 3 --retry-delay 5 --max-time 10 https://app.failsignal.com/api/signals/ping/YOUR_TOKEN > /dev/null 2>&1

With Payload

#!/bin/bash
START=$(date +%s%3N)
./my-job.sh
EXIT_CODE=$?
END=$(date +%s%3N)
DURATION=$((END - START))

if [ $EXIT_CODE -eq 0 ]; then
  curl -sf -X POST https://app.failsignal.com/api/signals/ping/YOUR_TOKEN \
    -H "Content-Type: application/json" \
    -d "{\"duration\": $DURATION, \"exit_code\": $EXIT_CODE}" > /dev/null 2>&1
fi

Node.js

Simple (Built-in fetch)

async function runJob() {
  // ... your job logic ...

  await fetch('https://app.failsignal.com/api/signals/ping/YOUR_TOKEN');
}

With Payload and Error Handling

async function runJob() {
  const start = Date.now();

  // ... your job logic ...

  const duration = Date.now() - start;

  try {
    await fetch('https://app.failsignal.com/api/signals/ping/YOUR_TOKEN', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ duration, rows_processed: 1500 }),
      signal: AbortSignal.timeout(10_000),
    });
  } catch {
    // Ping failure shouldn't crash your job
  }
}

Express Middleware (Health Check Heartbeat)

const FAILSIGNAL_TOKEN = process.env.FAILSIGNAL_TOKEN;
const PING_INTERVAL = 5 * 60 * 1000; // 5 minutes

setInterval(async () => {
  try {
    await fetch(
      `https://app.failsignal.com/api/signals/ping/${FAILSIGNAL_TOKEN}`,
      { signal: AbortSignal.timeout(10_000) }
    );
  } catch {
    console.error('IonHour ping failed');
  }
}, PING_INTERVAL);

Python

Simple

import requests

def run_job():
    # ... your job logic ...

    requests.get(
        "https://app.failsignal.com/api/signals/ping/YOUR_TOKEN",
        timeout=10,
    )

With Payload

import requests
import time

def run_job():
    start = time.time()

    # ... your job logic ...

    duration_ms = int((time.time() - start) * 1000)

    try:
        requests.post(
            "https://app.failsignal.com/api/signals/ping/YOUR_TOKEN",
            json={"duration": duration_ms, "status": "completed"},
            timeout=10,
        )
    except requests.RequestException:
        pass  # Don't let ping failure crash your job

Django Management Command

from django.core.management.base import BaseCommand
import requests

class Command(BaseCommand):
    help = "Run nightly cleanup and ping IonHour"

    def handle(self, *args, **options):
        # ... your cleanup logic ...

        requests.get(
            "https://app.failsignal.com/api/signals/ping/YOUR_TOKEN",
            timeout=10,
        )
        self.stdout.write(self.style.SUCCESS("Cleanup complete"))

Go

Simple

package main

import (
	"net/http"
	"time"
)

func pingIonHour(token string) {
	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Get("https://app.failsignal.com/api/signals/ping/" + token)
	if err != nil {
		return // Don't let ping failure crash your job
	}
	defer resp.Body.Close()
}

func main() {
	// ... your job logic ...

	pingIonHour("YOUR_TOKEN")
}

With Payload

package main

import (
	"bytes"
	"encoding/json"
	"net/http"
	"time"
)

func pingWithPayload(token string, payload map[string]interface{}) {
	body, _ := json.Marshal(payload)
	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Post(
		"https://app.failsignal.com/api/signals/ping/"+token,
		"application/json",
		bytes.NewBuffer(body),
	)
	if err != nil {
		return
	}
	defer resp.Body.Close()
}

func main() {
	start := time.Now()

	// ... your job logic ...

	pingWithPayload("YOUR_TOKEN", map[string]interface{}{
		"duration": time.Since(start).Milliseconds(),
		"status":   "completed",
	})
}

GitHub Actions

After a Workflow Step

steps:
  - name: Run scheduled task
    run: ./scripts/nightly-build.sh

  - name: Ping IonHour
    if: success()
    run: curl -sf https://app.failsignal.com/api/signals/ping/${{ secrets.FAILSIGNAL_TOKEN }}

With Deployment Window

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Start deployment window
        id: deploy-start
        run: |
          RESPONSE=$(curl -s -X POST https://app.failsignal.com/api/deployments \
            -H "Authorization: Bearer ${{ secrets.FAILSIGNAL_API_KEY }}" \
            -H "Content-Type: application/json" \
            -d "{
              \"projectId\": ${{ vars.PROJECT_ID }},
              \"name\": \"${{ github.ref_name }}\",
              \"version\": \"${{ github.sha }}\",
              \"author\": \"${{ github.actor }}\",
              \"autoPause\": true
            }")
          echo "deployment_id=$(echo $RESPONSE | jq -r '.id')" >> $GITHUB_OUTPUT

      - name: Deploy
        run: ./deploy.sh

      - name: End deployment window
        if: always()
        run: |
          curl -s -X PUT "https://app.failsignal.com/api/deployments/${{ steps.deploy-start.outputs.deployment_id }}/end" \
            -H "Authorization: Bearer ${{ secrets.FAILSIGNAL_API_KEY }}"

Scheduled Workflow (Synthetic Monitor)

name: Heartbeat
on:
  schedule:
    - cron: '*/5 * * * *'  # Every 5 minutes

jobs:
  ping:
    runs-on: ubuntu-latest
    steps:
      - name: Health check
        run: |
          STATUS=$(curl -sf -o /dev/null -w '%{http_code}' https://api.yourservice.com/health)
          if [ "$STATUS" = "200" ]; then
            curl -sf https://app.failsignal.com/api/signals/ping/${{ secrets.FAILSIGNAL_TOKEN }}
          fi

Docker

Healthcheck

HEALTHCHECK --interval=5m --timeout=10s --retries=1 \
  CMD curl -sf https://app.failsignal.com/api/signals/ping/YOUR_TOKEN || exit 1

Entrypoint Wrapper

# entrypoint.sh
#!/bin/sh
set -e

# Run the actual command
"$@"

# Ping IonHour on success
curl -sf --max-time 10 https://app.failsignal.com/api/signals/ping/$FAILSIGNAL_TOKEN > /dev/null 2>&1 || true
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["./my-job"]

Kubernetes

CronJob with Ping

apiVersion: batch/v1
kind: CronJob
metadata:
  name: nightly-cleanup
spec:
  schedule: "0 2 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: cleanup
              image: myapp:latest
              command:
                - /bin/sh
                - -c
                - |
                  ./cleanup.sh && \
                  curl -sf https://app.failsignal.com/api/signals/ping/$FAILSIGNAL_TOKEN
              env:
                - name: FAILSIGNAL_TOKEN
                  valueFrom:
                    secretKeyRef:
                      name: failsignal
                      key: token
          restartPolicy: OnFailure

Systemd Timer

# /etc/systemd/system/backup.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
ExecStartPost=/usr/bin/curl -sf https://app.failsignal.com/api/signals/ping/YOUR_TOKEN
# /etc/systemd/system/backup.timer
[Timer]
OnCalendar=hourly
Persistent=true

[Install]
WantedBy=timers.target