Skip to content

Webhooks & Notifications

When running batch processes on the SCo2-API, it is important to be able to track the progress of the process and be notified when it is completed. This is where webhooks and notifications come in.

Webhooks

Webhooks are a way for an app to provide other applications with real-time information.

Endpoints

The webhook system provides the following endpoints:

  • POST /webhooks/register: Register a webhook URL to receive notifications
  • DELETE /webhooks/remove: Remove the registered webhook
  • POST /webhooks/test: Send a test webhook to verify your webhook server is working

Register a Webhook

Register a webhook to receive notifications when batch processes are completed. You can only register one webhook address per user.

Important: - Only one webhook address can be registered per user - The webhook address must be a valid HTTPS URL (HTTP allowed only in development with emulators) - You will receive a client_secret that you must store securely for webhook signature validation - You cannot choose which events to receive - you will receive all webhook notifications

Python

register_webhook.py
import requests
import json

request_data = {
    "url": "https://your-server.com/webhooks"
}

headers = {
    'Authorization': "Bearer <authorization_token>",
    'Content-Type': 'application/json'
}

response = requests.post(
    "https://epoch-sco2-api.com/webhooks/register", 
    json=request_data, 
    headers=headers
)

json_result = json.loads(response.content)
print(json_result)
# Response: {"url": "https://your-server.com/webhooks", "client_secret": "..."}
# IMPORTANT: Store the client_secret securely - you'll need it to validate webhook signatures

JavaScript

register_webhook.js
const axios = require('axios');

const requestData = {
  url: "https://your-server.com/webhooks"
};

const headers = {
  Authorization: "Bearer <authorization_token>",
  'Content-Type': 'application/json'
};

axios.post('https://epoch-sco2-api.com/webhooks/register', requestData, { headers })
  .then(response => {
    console.log(response.data);
    // IMPORTANT: Store the client_secret from response.data.client_secret
  })
  .catch(error => {
    console.error('Error:', error);
  });

cURL

1
2
3
4
curl -X POST https://epoch-sco2-api.com/webhooks/register \
  -H "Authorization: Bearer <authorization_token>" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://your-server.com/webhooks"}'

Remove a Webhook

To remove your registered webhook:

remove_webhook.py
import requests

headers = {
    'Authorization': "Bearer <authorization_token>"
}

response = requests.delete(
    "https://epoch-sco2-api.com/webhooks/remove",
    headers=headers
)

print(response.json())
# Response: {"message": "Webhook removed"}

Test a Webhook

To send a test webhook to verify your webhook server is working:

test_webhook.py
import requests

headers = {
    'Authorization': "Bearer <authorization_token>"
}

response = requests.post(
    "https://epoch-sco2-api.com/webhooks/test",
    headers=headers
)

print(response.json())
# Response: {"message": "Webhook sent successfully"}

Webhook Events

Currently, the following webhook event types are supported:

  • batch_zonal_stats_complete: Sent when a batch zonal statistics operation completes
  • test_event: Sent when using the /webhooks/test endpoint

Receiving Webhooks

When a batch process is completed, you will receive a POST request at your registered webhook URL.

Webhook Headers

Each webhook request includes the following headers:

Header Description
Host The host of the API (api.epoch.com)
User-Agent Epoch-Webhooks/1.0
Content-Type application/json
X-Epoch-Webhook-Type The event type that triggered the webhook (e.g., batch_zonal_stats_complete)
X-Epoch-Webhook-Signature The HMAC SHA256 signature for validating the webhook (format: t={timestamp}, v1={signature})

Webhook Payload

The webhook body contains the following structure:

{
    "id": "ec26f2760191469fbd7920bef6729cba",
    "type": "batch_zonal_stats_complete",
    "data": {
        "collection_name": "brazil_pasture",
        "description": "brazil_pasture",
        "urls": {
          "biomass_emissions": "https://epoch-sco2-api.com/fetch_biomass_emissions?filename=0x102dfff79ed1477398b5cf1360730fbf20676b2cb5dbc7841f9c27ec808d12c2&monitoring_start=2017-01-01&monitoring_end=2024-09-18",
          "biodiversity": "https://epoch-sco2-api.com/fetch_biodiversity?filename=0x102dfff79ed1477398b5cf1360730fbf20676b2cb5dbc7841f9c27ec808d12c2&monitoring_start=2017-01-01&monitoring_end=2024-09-18",
          "deforestation_check": "https://epoch-sco2-api.com/fetch_deforestation_check?filename=0x102dfff79ed1477398b5cf1360730fbf20676b2cb5dbc7841f9c27ec808d12c2&monitoring_start=2017-01-01&monitoring_end=2024-09-18",
          "deforestation_check_eudr": "https://epoch-sco2-api.com/fetch_deforestation_check?filename=0x102dfff79ed1477398b5cf1360730fbf20676b2cb5dbc7841f9c27ec808d12c2&monitoring_start=2021-01-01&monitoring_end=2024-09-18",
          "export_eudr_dds": "https://epoch-sco2-api.com/export_eudr_dds?filename=0x102dfff79ed1477398b5cf1360730fbf20676b2cb5dbc7841f9c27ec808d12c2&monitoring_start=2021-01-01&monitoring_end=2024-09-18"
        }
    },
    "created_at": "2024-09-18T15:48:06.515699"
}

Payload Fields

Field Type Description
id string Unique identifier for the webhook event (use for deduplication)
type string The event type (same as X-Epoch-Webhook-Type header)
data object Event-specific data containing collection information and fetch URLs
created_at string ISO 8601 timestamp of when the webhook was created (not the send time, due to possible retries)

Using Fetch URLs

The urls object in the webhook payload contains pre-formatted URLs to fetch the processed data. When using these URLs, you must include your Bearer token in the Authorization header. The data returned is a streaming response - see the Fetch API documentation for more information.

Webhook Security

Signature Validation

All webhooks include an HMAC SHA256 signature in the X-Epoch-Webhook-Signature header. You must validate this signature to ensure the webhook was sent by Epoch and not a malicious actor.

The signature format is: t={timestamp}, v1={signature}

Where: - t is a Unix timestamp of when the webhook was signed - v1 is the HMAC SHA256 signature of the payload

Validation Example (Python)

validate_webhook.py
import hmac
import hashlib
import json
from fastapi import Request

async def is_webhook_signature_valid(request: Request, client_secret: str) -> bool:
    """Validate the webhook signature."""
    # Extract the signature header
    signature_header = request.headers.get("X-Epoch-Webhook-Signature")
    if not signature_header:
        return False

    # Parse the signature header
    try:
        parts = signature_header.split(", ")
        timestamp = parts[0].split("=")[1]
        version, signature = parts[1].split("=")
    except (IndexError, ValueError):
        return False

    # Get the raw JSON body
    body = await request.body()
    payload = json.loads(body)
    payload_str = json.dumps(payload, separators=(',', ':'), sort_keys=True)

    # Create the message to sign: "{timestamp}.{payload}"
    message = f"{timestamp}.{payload_str}"

    # Compute the HMAC SHA256 signature
    computed_signature = hmac.new(
        client_secret.encode("utf-8"), 
        message.encode("utf-8"), 
        hashlib.sha256
    ).hexdigest()

    # Use constant-time comparison to prevent timing attacks
    return hmac.compare_digest(computed_signature, signature)

Validation Example (JavaScript)

validate_webhook.js
const crypto = require('crypto');

function isWebhookSignatureValid(request, clientSecret) {
  const signatureHeader = request.headers['x-epoch-webhook-signature'];
  if (!signatureHeader) {
    return false;
  }

  // Parse the signature header
  const parts = signatureHeader.split(', ');
  const timestamp = parts[0].split('=')[1];
  const signature = parts[1].split('=')[1];

  // Get the raw JSON body
  const payload = JSON.stringify(request.body);

  // Create the message to sign: "{timestamp}.{payload}"
  const message = `${timestamp}.${payload}`;

  // Compute the HMAC SHA256 signature
  const computedSignature = crypto
    .createHmac('sha256', clientSecret)
    .update(message)
    .digest('hex');

  // Use constant-time comparison
  return crypto.timingSafeEqual(
    Buffer.from(computedSignature),
    Buffer.from(signature)
  );
}

Best Practices

  1. Always validate the signature using the client_secret you received during registration
  2. Store the client_secret securely - never expose it in client-side code or logs
  3. Use the id field to deduplicate webhook events (webhooks may be retried)
  4. Return HTTP 200 quickly - process the webhook asynchronously if needed
  5. IP address restrictions (optional): Consider restricting your webhook endpoint to only accept requests from Epoch's IP addresses (contact support for IP ranges)

Retry Logic

If a webhook fails to send (non-200 response), the system will automatically retry up to 3 times with exponential backoff:

  • First retry: After 10 seconds
  • Second retry: After 30 seconds
  • Third retry: After 60 seconds

After all retries are exhausted, the webhook delivery is considered failed. Ensure your webhook endpoint returns HTTP 200 quickly to avoid unnecessary retries.

Email Notifications

While webhooks are ideal for receiving notifications about data availability with pre-formatted endpoints to fetch it,

Epoch also enabled Email notifications for users who want to receive the EUDR DDS reports directly in their inbox.

To enable email notifications for the EUDR DDS, you need to set the eudr_dds boolean to True as part of the request parameters for the batch process.