OilPriceAPI Docs
GitHub
GitHub

Webhooks API

Plan requirement

Webhooks are available on Starter and above. Price webhooks are included on Starter, Professional, and Scale; drilling-intelligence webhooks require the Scale plan. Compare plans to access webhook notifications.

Overview

Webhooks allow you to receive real-time HTTP POST notifications when specific events occur in the OilPriceAPI system. Instead of polling for changes, we'll push updates directly to your server, enabling:

  • Real-time price monitoring - Instant notifications when oil prices change
  • Drilling intelligence alerts - Updates on rig counts, frac spreads, well permits, and DUC wells
  • Automated workflows - Trigger actions based on market events
  • Reduced API usage - No need for constant polling
  • Better performance - Event-driven architecture

Webhook Features

On Starter and above, you get:

FeatureIncluded
Price update webhooks✅ All commodities
Drilling intelligence webhooks✅ Rig counts, frac spreads, permits, DUC wells
Webhook endpoints25
Events per month250,000
Custom headers✅
Retry logic✅ Exponential backoff
HMAC signature verification✅
Commodity filtering✅
State filtering✅ Filter well permits by state
Priority support✅ Dedicated Slack channel

Quick Start

1. Create a Webhook Endpoint

curl -X POST https://api.oilpriceapi.com/v1/webhooks \
  -H 'Authorization: Token YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "url": "https://your-server.com/webhooks/oilprice",
    "events": ["price.updated", "drilling.rig_count.updated"],
    "commodity_filters": ["BRENT_CRUDE_USD", "WTI_USD", "US_RIG_COUNT"],
    "description": "Production webhook for price monitoring"
  }'

2. Handle Webhook Events

// Node.js Express example
app.post("/webhooks/oilprice", (req, res) => {
  // Verify signature
  const signature = req.headers["x-oilpriceapi-signature"];
  const timestamp = req.headers["x-oilpriceapi-signature-timestamp"];

  if (!verifyWebhookSignature(req.body, signature, timestamp)) {
    return res.status(401).send("Unauthorized");
  }

  // Process event
  const { type, data } = req.body;

  switch (type) {
    case "price.updated":
      console.log(`Price update: ${data.commodity} = $${data.value}`);
      break;
    case "drilling.rig_count.updated":
      console.log(`Rig count update: ${data.region} = ${data.value} rigs`);
      break;
  }

  res.status(200).send("OK");
});

Alert Conditions (5 Operators)

When creating webhooks with price thresholds, use these condition operators:

1. greater_than (>)

Trigger when price exceeds a value:

{
  "url": "https://your-app.com/webhook",
  "events": ["price.threshold"],
  "conditions": {
    "commodity": "BRENT_CRUDE_USD",
    "operator": "greater_than",
    "value": 80.0
  }
}

Use case: Alert when oil price spikes above budget threshold.

2. less_than (<)

Trigger when price drops below a value:

{
  "url": "https://your-app.com/webhook",
  "events": ["price.threshold"],
  "conditions": {
    "commodity": "WTI_USD",
    "operator": "less_than",
    "value": 65.0
  }
}

Use case: Alert for buying opportunities when prices dip.

3. percent_increase (% up)

Trigger on percentage increase from previous value:

{
  "url": "https://your-app.com/webhook",
  "events": ["price.threshold"],
  "conditions": {
    "commodity": "NATURAL_GAS_USD",
    "operator": "percent_increase",
    "value": 5.0,
    "timeframe": "24h"
  }
}

Use case: Alert on significant price surges for trading signals.

4. percent_decrease (% down)

Trigger on percentage decrease from previous value:

{
  "url": "https://your-app.com/webhook",
  "events": ["price.threshold"],
  "conditions": {
    "commodity": "BRENT_CRUDE_USD",
    "operator": "percent_decrease",
    "value": 3.0,
    "timeframe": "1h"
  }
}

Use case: Alert on rapid price drops for risk management.

5. between (range)

Trigger when price enters or exits a range:

{
  "url": "https://your-app.com/webhook",
  "events": ["price.threshold"],
  "conditions": {
    "commodity": "WTI_USD",
    "operator": "between",
    "min_value": 70.0,
    "max_value": 75.0,
    "trigger_on": "exit"
  }
}

Use case: Alert when price breaks out of trading range.

Combining Conditions

Create complex alerts with multiple conditions:

{
  "url": "https://your-app.com/webhook",
  "events": ["price.threshold"],
  "conditions": [
    {
      "commodity": "WTI_USD",
      "operator": "greater_than",
      "value": 75.0
    },
    {
      "commodity": "BRENT_CRUDE_USD",
      "operator": "greater_than",
      "value": 80.0
    }
  ],
  "condition_logic": "any"
}
LogicDescription
anyTrigger if ANY condition is met
allTrigger only if ALL conditions are met

Available Events

Price Events

price.updated

Triggered whenever a commodity price is updated.

{
  "id": "evt_1a2b3c4d5e",
  "type": "price.updated",
  "created_at": "2025-08-03T14:30:00Z",
  "data": {
    "commodity": "BRENT_CRUDE_USD",
    "name": "Brent Crude Oil",
    "value": 78.45,
    "currency": "USD",
    "unit": "barrel",
    "change_percent": 2.3,
    "previous_value": 76.69,
    "timestamp": "2025-08-03T14:30:00Z"
  }
}

price.significant_change

Triggered when a price changes by more than 5%.

{
  "id": "evt_2b3c4d5e6f",
  "type": "price.significant_change",
  "created_at": "2025-08-03T14:30:00Z",
  "data": {
    "commodity": "NATURAL_GAS",
    "name": "Natural Gas",
    "value": 3.85,
    "currency": "USD",
    "unit": "mmBtu",
    "change_percent": 6.2,
    "threshold_exceeded": "5%",
    "alert_type": "surge",
    "timestamp": "2025-08-03T14:30:00Z"
  }
}

Analytics Alert Events

Analytics alerts

Analytics alerts are available on Professional and above. See Price Alerts API for setup details.

analytics_alert.triggered

Triggered when a statistical or technical analysis condition is met.

Analytics Types:

  • z_score - Statistical anomaly detection (z-score exceeds threshold)
  • rsi - RSI overbought/oversold signals
  • volatility_spike - Volatility exceeds historical norms
  • trend_reversal - Momentum-based trend change detection
{
  "id": "evt_ana123xyz",
  "type": "analytics_alert.triggered",
  "created_at": "2025-12-28T10:30:00Z",
  "data": {
    "alert_id": "987fcdeb-51a2-3c4d-e567-890123456789",
    "alert_name": "WTI Z-Score Alert",
    "commodity_code": "WTI_USD",
    "analytics_type": "z_score",
    "condition": "z-score exceeds ±2.5",
    "current_value": 2.73,
    "threshold": 2.5,
    "details": {
      "z_score": 2.73,
      "mean": 72.5,
      "std_dev": 1.85,
      "current_price": 77.55
    },
    "triggered_at": "2025-12-28T10:30:00Z"
  }
}

RSI Alert Example:

{
  "id": "evt_rsi456abc",
  "type": "analytics_alert.triggered",
  "created_at": "2025-12-28T10:30:00Z",
  "data": {
    "alert_id": "123e4567-e89b-12d3-a456-426614174000",
    "alert_name": "Brent RSI Alert",
    "commodity_code": "BRENT_CRUDE_USD",
    "analytics_type": "rsi",
    "condition": "RSI overbought/oversold (30/70)",
    "current_value": 73.5,
    "threshold": 70,
    "details": {
      "rsi": 73.5,
      "signal": "overbought",
      "current_price": 82.15
    },
    "triggered_at": "2025-12-28T10:30:00Z"
  }
}

Drilling Intelligence Events

drilling.rig_count.updated

Weekly rig count updates from Baker Hughes.

{
  "id": "evt_3c4d5e6f7g",
  "type": "drilling.rig_count.updated",
  "created_at": "2025-08-03T17:00:00Z",
  "data": {
    "commodity": "US_RIG_COUNT",
    "region": "United States",
    "value": 540,
    "previous_value": 535,
    "change": 5,
    "change_percent": 0.93,
    "unit": "rigs",
    "source": "Baker Hughes",
    "breakdown": {
      "oil_rigs": 425,
      "gas_rigs": 110,
      "misc_rigs": 5
    },
    "timestamp": "2025-08-03T17:00:00Z"
  }
}

drilling.frac_spread.updated

Daily hydraulic fracturing spread counts by basin.

{
  "id": "evt_4d5e6f7g8h",
  "type": "drilling.frac_spread.updated",
  "created_at": "2025-08-03T14:00:00Z",
  "data": {
    "commodity": "PERMIAN_FRAC_SPREADS",
    "region": "Permian Basin",
    "value": 125,
    "previous_value": 123,
    "change": 2,
    "change_percent": 1.63,
    "unit": "spreads",
    "source": "Primary Energy",
    "timestamp": "2025-08-03T14:00:00Z"
  }
}

drilling.well_permit.new

Fires when a newly filed well permit is ingested. Permits are collected from each state's regulatory portal on a schedule — daily for 14 states, weekly for 13 — so delivery follows the ingest cadence for that state, not the moment of filing. See Well Permits for the per-state schedule. Requires the Scale plan.

{
  "id": "evt_wp_1a2b3c4d5e",
  "event": "drilling.well_permit.new",
  "created_at": "2025-12-31T14:30:00Z",
  "data": {
    "api_number": "42-003-12345-00-00",
    "api_number_raw": "42003123450000",
    "state_code": "TX",
    "county": "Midland",
    "permit_number": "849367",
    "permit_type": "new_drill",
    "permit_status": "approved",
    "permit_date": "2025-12-31",
    "operator": {
      "name": "ConocoPhillips Company",
      "name_raw": "CONOCOPHILLIPS CO",
      "number": "123456"
    },
    "well": {
      "name": "UNIVERSITY 42-3 #1H",
      "number": "1H",
      "type": "oil"
    },
    "location": {
      "latitude": 31.9686,
      "longitude": -102.0779,
      "county": "Midland",
      "section": "42",
      "township": "3S",
      "range": "37E"
    },
    "target": {
      "formation": "Wolfcamp",
      "formation_raw": "WOLFCAMP A",
      "total_depth_proposed": 12500
    },
    "provenance": {
      "source": "texas_rrc",
      "source_url": "https://www.rrc.texas.gov/...",
      "fetched_at": "2025-12-31T14:25:00Z",
      "data_as_of": "2025-12-31"
    }
  }
}

drilling.well_permits.batch

Daily batch notification with all new permits for a state. Useful for a single summarized payload instead of one event per permit.

{
  "id": "evt_wpb_6f7g8h9i0j",
  "event": "drilling.well_permits.batch",
  "created_at": "2025-12-31T20:00:00Z",
  "data": {
    "state": "TX",
    "count": 47,
    "permits": [
      {
        "api_number": "42-003-12345-00-00",
        "operator": {
          "name": "ConocoPhillips Company",
          "name_raw": "CONOCOPHILLIPS CO"
        },
        "well": {
          "name": "UNIVERSITY 42-3 #1H",
          "type": "oil"
        },
        "target": {
          "formation": "Wolfcamp",
          "total_depth_proposed": 12500
        }
      }
    ],
    "truncated": false,
    "batch_timestamp": "2025-12-31T20:00:00Z"
  }
}

Well Permits Access

Well permit webhooks require the Scale plan, which carries the Energy Intelligence entitlement for /v1/ei/* (well_permits_data). Without that entitlement, subscribing to these events returns 403 Forbidden.

For current plan pricing, see the pricing page.

State Filtering for Well Permits

Filter well permit webhooks by state to receive only the permits you care about:

{
  "url": "https://your-app.com/webhook",
  "events": ["drilling.well_permit.new", "drilling.well_permits.batch"],
  "state_filters": ["TX", "OK", "NM"],
  "description": "Permian Basin well permits only"
}

When state_filters is empty (default): You receive permits for ALL states.

When state_filters has values: You only receive permits for the specified states.

Supported State Filters (27 total)

State filters are accepted for all configured well-permit states. Source freshness varies by state portal, so use the well-permits API response metadata and state counts as the live source of truth before making freshness-sensitive workflows.

StateCodeSource
TexasTXRailroad Commission
OklahomaOKCorporation Commission
New MexicoNMOil Conservation Division
ColoradoCOECMC
North DakotaNDDMR
PennsylvaniaPADEP
WyomingWYWOGCC
MontanaMTMBOGC
LouisianaLADNR
KansasKSKCC
CaliforniaCACalGEM
AlaskaAKAOGCC
OhioOHODNR
West VirginiaWVDEP
ArkansasARAOGC
MississippiMSMSOGB
UtahUTDOGM
NebraskaNENOGCC
New YorkNYDEC
MichiganMIEGLE
FloridaFLDEP
IndianaINDNR
TennesseeTNTDEC
KentuckyKYDMM
VirginiaVAVirginia Energy
IllinoisILDNR
AlabamaALAOGB

Filtering Best Practice

Start with the states most relevant to your operations. You can always update your webhook to add more states later.

drilling.well_permit.updated (Legacy)

Daily drilling permit issuances from state agencies.

{
  "id": "evt_5e6f7g8h9i",
  "type": "drilling.well_permit.updated",
  "created_at": "2025-08-03T15:00:00Z",
  "data": {
    "commodity": "TEXAS_WELL_PERMITS",
    "region": "Texas",
    "value": 842,
    "previous_value": 798,
    "change": 44,
    "change_percent": 5.51,
    "unit": "permits",
    "source": "Texas Railroad Commission",
    "timestamp": "2025-08-03T15:00:00Z"
  }
}

drilling.duc_well.updated

Monthly Drilled but Uncompleted well inventories.

{
  "id": "evt_6f7g8h9i0j",
  "type": "drilling.duc_well.updated",
  "created_at": "2025-08-15T16:00:00Z",
  "data": {
    "commodity": "PERMIAN_DUC_WELLS",
    "region": "Permian Basin",
    "value": 3542,
    "previous_value": 3489,
    "change": 53,
    "change_percent": 1.52,
    "unit": "wells",
    "source": "EIA DPR",
    "timestamp": "2025-08-15T16:00:00Z"
  }
}

API Usage Events

api.limit.warning

Triggered when API usage reaches 80% of monthly limit.

{
  "id": "evt_7g8h9i0j1k",
  "type": "api.limit.warning",
  "created_at": "2025-08-25T10:00:00Z",
  "data": {
    "resource": "api_requests",
    "usage": 200000,
    "limit": 250000,
    "percentage": 80,
    "period": "2025-08"
  }
}

api.limit.exceeded

Triggered when monthly API limit is exceeded.

Subscription Events

subscription.updated

Triggered when subscription plan or status changes.

subscription.cancelled

Triggered when a subscription is cancelled.

Webhook Configuration

Creating Webhooks

POST /v1/webhooks

Request Body:

{
  "url": "https://your-app.com/webhooks/oilprice",
  "events": ["price.updated", "drilling.well_permit.new"],
  "commodity_filters": ["BRENT_CRUDE_USD", "WTI_USD"],
  "state_filters": ["TX", "OK", "NM"],
  "description": "Production webhook",
  "headers": {
    "X-Custom-Header": "value"
  },
  "timeout_seconds": 30,
  "max_retries": 3,
  "signing_enabled": true
}

Response:

{
  "id": "wh_1a2b3c4d5e6f7g8h",
  "url": "https://your-app.com/webhooks/oilprice",
  "status": "active",
  "events": ["price.updated", "drilling.well_permit.new"],
  "commodity_filters": ["BRENT_CRUDE_USD", "WTI_USD"],
  "state_filters": ["TX", "OK", "NM"],
  "secret": "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
  "created_at": "2025-08-03T12:00:00Z",
  "available_states": [
    "TX",
    "OK",
    "NM",
    "CO",
    "ND",
    "PA",
    "WY",
    "MT",
    "LA",
    "KS",
    "CA",
    "AK",
    "OH",
    "WV",
    "AR",
    "MS",
    "UT",
    "NE",
    "NY",
    "MI",
    "FL",
    "IN",
    "TN",
    "KY",
    "VA",
    "IL",
    "AL"
  ]
}

Secret Storage

The webhook secret is only shown once during creation. Store it securely as it's required for signature verification.

Listing Webhooks

GET /v1/webhooks

Updating Webhooks

PATCH /v1/webhooks/{webhook_id}

Deleting Webhooks

DELETE /v1/webhooks/{webhook_id}

Testing Webhooks

POST /v1/webhooks/{webhook_id}/test

{
  "event_type": "price.updated"
}

Security

Signature Verification

All webhooks include HMAC-SHA256 signatures for verification. Always verify signatures to ensure webhook authenticity.

Headers sent with each webhook:

  • X-OilPriceAPI-Event: Event type
  • X-OilPriceAPI-Event-ID: Unique event identifier
  • X-OilPriceAPI-Signature: HMAC-SHA256 signature
  • X-OilPriceAPI-Signature-Timestamp: Unix timestamp

Interactive Signature Verifier

Test your signature verification implementation directly in your browser:

Verification Examples

::: code-group

const crypto = require("crypto");

function verifyWebhookSignature(payload, signature, timestamp) {
  // Prevent replay attacks (5 minute window)
  if (Date.now() - parseInt(timestamp) * 1000 > 300000) {
    return false;
  }

  const payloadString = JSON.stringify(payload);
  const expectedSignature = crypto
    .createHmac("sha256", process.env.WEBHOOK_SECRET)
    .update(`${payloadString}.${timestamp}`)
    .digest("hex");

  return signature === expectedSignature;
}
import hmac
import hashlib
import time
import json

def verify_webhook_signature(payload, signature, timestamp, secret):
    # Prevent replay attacks (5 minute window)
    if int(time.time()) - int(timestamp) > 300:
        return False

    payload_string = json.dumps(payload, separators=(',', ':'))
    expected_signature = hmac.new(
        secret.encode(),
        f"{payload_string}.{timestamp}".encode(),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(expected_signature, signature)
require 'openssl'
require 'json'

def verify_webhook_signature(payload, signature, timestamp, secret)
  # Prevent replay attacks (5 minute window)
  return false if Time.now.to_i - timestamp.to_i > 300

  payload_string = payload.to_json
  expected_signature = OpenSSL::HMAC.hexdigest(
    'SHA256',
    secret,
    "#{payload_string}.#{timestamp}"
  )

  ActiveSupport::SecurityUtils.secure_compare(expected_signature, signature)
end
function verifyWebhookSignature($payload, $signature, $timestamp, $secret) {
    // Prevent replay attacks (5 minute window)
    if (time() - intval($timestamp) > 300) {
        return false;
    }

    $payloadString = json_encode($payload);
    $expectedSignature = hash_hmac(
        'sha256',
        $payloadString . '.' . $timestamp,
        $secret
    );

    return hash_equals($expectedSignature, $signature);
}
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"strconv"
	"time"
)

func VerifyWebhookSignature(payload interface{}, signature, timestamp, secret string) bool {
	// Prevent replay attacks (5 minute window)
	ts, err := strconv.ParseInt(timestamp, 10, 64)
	if err != nil {
		return false
	}
	if time.Now().Unix()-ts > 300 {
		return false
	}

	// Serialize payload to JSON
	payloadBytes, err := json.Marshal(payload)
	if err != nil {
		return false
	}

	// Calculate expected signature
	message := string(payloadBytes) + "." + timestamp
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(message))
	expectedSignature := hex.EncodeToString(mac.Sum(nil))

	// Constant-time comparison
	return hmac.Equal([]byte(expectedSignature), []byte(signature))
}
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import com.google.gson.Gson;

public class WebhookVerifier {
    private static final Gson gson = new Gson();

    public static boolean verifyWebhookSignature(
            Object payload,
            String signature,
            String timestamp,
            String secret) {

        // Prevent replay attacks (5 minute window)
        long ts = Long.parseLong(timestamp);
        if (System.currentTimeMillis() / 1000 - ts > 300) {
            return false;
        }

        try {
            // Serialize payload to JSON
            String payloadString = gson.toJson(payload);

            // Calculate expected signature
            String message = payloadString + "." + timestamp;
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKey = new SecretKeySpec(
                secret.getBytes(StandardCharsets.UTF_8),
                "HmacSHA256"
            );
            mac.init(secretKey);
            byte[] hmacBytes = mac.doFinal(
                message.getBytes(StandardCharsets.UTF_8)
            );

            // Convert to hex
            StringBuilder hexString = new StringBuilder();
            for (byte b : hmacBytes) {
                hexString.append(String.format("%02x", b));
            }
            String expectedSignature = hexString.toString();

            // Constant-time comparison
            return MessageDigest.isEqual(
                expectedSignature.getBytes(StandardCharsets.UTF_8),
                signature.getBytes(StandardCharsets.UTF_8)
            );
        } catch (Exception e) {
            return false;
        }
    }
}
using System;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

public static class WebhookVerifier
{
    public static bool VerifyWebhookSignature(
        object payload,
        string signature,
        string timestamp,
        string secret)
    {
        // Prevent replay attacks (5 minute window)
        if (!long.TryParse(timestamp, out long ts))
            return false;

        var currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
        if (currentTime - ts > 300)
            return false;

        // Serialize payload to JSON
        var payloadString = JsonSerializer.Serialize(payload);

        // Calculate expected signature
        var message = $"{payloadString}.{timestamp}";
        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
        var hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
        var expectedSignature = BitConverter.ToString(hashBytes)
            .Replace("-", "")
            .ToLowerInvariant();

        // Constant-time comparison
        return CryptographicOperations.FixedTimeEquals(
            Encoding.UTF8.GetBytes(expectedSignature),
            Encoding.UTF8.GetBytes(signature)
        );
    }
}
use hmac::{Hmac, Mac};
use sha2::Sha256;
use std::time::{SystemTime, UNIX_EPOCH};

type HmacSha256 = Hmac<Sha256>;

pub fn verify_webhook_signature(
    payload: &serde_json::Value,
    signature: &str,
    timestamp: &str,
    secret: &str,
) -> bool {
    // Prevent replay attacks (5 minute window)
    let ts: i64 = match timestamp.parse() {
        Ok(t) => t,
        Err(_) => return false,
    };

    let current_time = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs() as i64;

    if current_time - ts > 300 {
        return false;
    }

    // Serialize payload to JSON
    let payload_string = match serde_json::to_string(payload) {
        Ok(s) => s,
        Err(_) => return false,
    };

    // Calculate expected signature
    let message = format!("{}.{}", payload_string, timestamp);
    let mut mac = match HmacSha256::new_from_slice(secret.as_bytes()) {
        Ok(m) => m,
        Err(_) => return false,
    };
    mac.update(message.as_bytes());
    let result = mac.finalize();
    let expected_signature = hex::encode(result.into_bytes());

    // Constant-time comparison
    constant_time_eq(expected_signature.as_bytes(), signature.as_bytes())
}

fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
    if a.len() != b.len() {
        return false;
    }
    a.iter().zip(b.iter()).fold(0, |acc, (x, y)| acc | (x ^ y)) == 0
}

// Cargo.toml dependencies:
// hmac = "0.12"
// sha2 = "0.10"
// hex = "0.4"
// serde_json = "1"

:::

Delivery & Reliability

Retry Logic

Failed webhook deliveries are automatically retried with exponential backoff:

  1. First retry: 1 minute after failure
  2. Second retry: 5 minutes after first retry
  3. Third retry: 15 minutes after second retry

After 3 failed attempts, the webhook event is marked as failed and can be retrieved via the API.

Delivery Requirements

Your webhook endpoint must:

  • Accept HTTPS requests (TLS 1.2 or higher)
  • Respond within 30 seconds
  • Return a 2xx status code for successful receipt
  • Handle duplicate deliveries idempotently

Monitoring Deliveries

View webhook event history and delivery status:

GET /v1/webhooks/{webhook_id}/events

Response:

{
  "webhook_events": [
    {
      "id": "we_1a2b3c4d5e",
      "event_id": "evt_9z8y7x6w5v",
      "event_type": "price.updated",
      "status": "delivered",
      "attempts": 1,
      "response_status_code": 200,
      "delivery_duration_ms": 245,
      "created_at": "2025-08-03T14:30:00Z"
    }
  ],
  "pagination": {
    "current_page": 1,
    "total_pages": 10,
    "total_count": 248
  }
}

Best Practices

1. Idempotency

Always handle webhooks idempotently using the event_id:

// Store processed event IDs to prevent duplicate processing
const processedEvents = new Set();

app.post("/webhook", (req, res) => {
  const eventId = req.body.id;

  if (processedEvents.has(eventId)) {
    return res.status(200).send("Already processed");
  }

  // Process event
  processWebhook(req.body);
  processedEvents.add(eventId);

  res.status(200).send("OK");
});

2. Async Processing

Acknowledge webhooks quickly and process asynchronously:

app.post("/webhook", async (req, res) => {
  // Quick validation
  if (!isValidWebhook(req)) {
    return res.status(401).send("Unauthorized");
  }

  // Acknowledge immediately
  res.status(200).send("OK");

  // Process asynchronously
  await jobQueue.add("process-webhook", {
    event: req.body,
    receivedAt: new Date(),
  });
});

3. Error Handling

Implement robust error handling:

app.post("/webhook", async (req, res) => {
  try {
    await processWebhook(req.body);
    res.status(200).send("OK");
  } catch (error) {
    console.error("Webhook processing error:", error);

    // Return 500 to trigger retry
    res.status(500).send("Internal error");
  }
});

4. Filtering

Use commodity and state filters to reduce noise:

{
  "url": "https://your-app.com/webhook",
  "events": ["price.updated", "drilling.well_permit.new"],
  "commodity_filters": ["BRENT_CRUDE_USD", "WTI_USD"],
  "state_filters": ["TX", "OK", "NM"],
  "description": "Crude oil prices + Permian Basin permits"
}

Use Cases

Real-Time Trading Dashboard

// Update trading dashboard when prices change
app.post("/webhooks/prices", async (req, res) => {
  const { type, data } = req.body;

  if (type === "price.updated") {
    // Update in-memory cache
    priceCache.set(data.commodity, data);

    // Broadcast to connected clients
    io.emit("price-update", {
      commodity: data.commodity,
      price: data.value,
      change: data.change_percent,
      timestamp: data.timestamp,
    });

    // Check trading rules
    await checkTradingAlerts(data);
  }

  res.status(200).send("OK");
});

Drilling Activity Monitor

// Monitor drilling activity for investment decisions
app.post("/webhooks/drilling", async (req, res) => {
  const { type, data } = req.body;

  switch (type) {
    case "drilling.rig_count.updated":
      await updateRigCountDashboard(data);
      await checkSupplyIndicators(data);
      break;

    case "drilling.frac_spread.updated":
      await updateCompletionActivity(data);
      await forecastProduction(data);
      break;

    case "drilling.duc_well.updated":
      await analyzeInventoryTrends(data);
      await predictCompletionTiming(data);
      break;
  }

  res.status(200).send("OK");
});

Automated Alerts

// Send alerts based on significant changes
app.post("/webhooks/alerts", async (req, res) => {
  const { type, data } = req.body;

  if (type === "price.significant_change") {
    // Send email alert
    await sendEmail({
      to: alertRecipients,
      subject: `${data.alert_type.toUpperCase()}: ${data.commodity}`,
      body: `${data.name} ${data.alert_type} by ${data.change_percent}% to $${data.value}`,
    });

    // Send SMS for critical commodities
    if (criticalCommodities.includes(data.commodity)) {
      await sendSMS({
        to: smsRecipients,
        message: `ALERT: ${data.commodity} ${data.alert_type} ${data.change_percent}%`,
      });
    }

    // Trigger trading actions
    await executeTradingStrategy(data);
  }

  res.status(200).send("OK");
});

Troubleshooting

Common Issues

Webhook Not Receiving Events

  1. Check endpoint status:

    GET /v1/webhooks/{webhook_id}
    

    Ensure status is "active" and no consecutive failures.

  2. Verify URL accessibility:

    • Must be HTTPS
    • Must be publicly accessible
    • Check firewall rules
  3. Review event filters:

    • Ensure subscribed to correct events
    • Check commodity filters

Signature Verification Failing

  1. Check timestamp:

    • Ensure server time is synchronized
    • Verify timestamp parsing
  2. Verify payload formatting:

    • Use raw request body, not parsed JSON
    • Ensure no middleware is modifying the body
  3. Confirm secret:

    • Use the exact secret from webhook creation
    • Check for trailing spaces or newlines

Missing Events

  1. Check delivery logs:

    GET /v1/webhooks/{webhook_id}/events
    
  2. Verify subscription:

    • Must have an active Starter (or higher) subscription
    • Check monthly event limits
  3. Review filters:

    • Commodity filters may be too restrictive
    • Event types might not match expectations

Support

For webhook implementation support:

  • Documentation: docs.oilpriceapi.com/webhooks
  • Email: support@oilpriceapi.com
  • Scale: Dedicated Slack channel for priority support

Testing & Development

Webhook Test Harness

We provide an open-source Webhook Test Harness for local development and testing. This tool lets you:

  • Capture and inspect webhooks in real-time with a web dashboard
  • Verify signatures to ensure your verification code is working correctly
  • Test all 14 event types with realistic sample payloads
  • Debug webhook payloads before deploying to production

Quick Start

# Clone the harness
git clone https://github.com/OilpriceAPI/webhook-test-harness.git
cd webhook-test-harness

# Install and run
npm install
npm start

# Open dashboard at http://localhost:3333

Create a Public Tunnel

To receive real webhooks from OilPriceAPI, expose your local harness:

# Cloudflare Tunnel (recommended - free, no signup)
cloudflared tunnel --url http://localhost:3333

# You'll get a URL like: https://random-words.trycloudflare.com
# Register this URL + /webhook in your OilPriceAPI dashboard

Configure Webhook Secret

After creating a webhook in the OilPriceAPI dashboard, configure the secret:

curl -X POST http://localhost:3333/api/secret \
  -H "Content-Type: application/json" \
  -d '{"secret": "YOUR_WEBHOOK_SECRET_HERE"}'

Resources

  • GitHub Repository: github.com/OilpriceAPI/webhook-test-harness
  • CLI Commands: Send test webhooks, list captures, view statistics
  • Docker Support: One-command deployment with docker-compose up

See the full documentation for all features and configuration options.

Limits & Performance

On Starter and above, you get:

  • 25 webhook endpoints - Configure multiple endpoints for different use cases
  • 250,000 events per month - Generous limit for all your webhook needs
  • 500 events per minute - High-throughput delivery rate
  • 30-second timeout - Ample time for processing
  • 3 retry attempts - Reliable delivery with exponential backoff

Changelog

Version 2.1 (January 2026)

  • Added state_filters for well permit webhooks
  • Filter permits by any of 27 configured US state filters
  • Clarified that source freshness varies by state portal
  • Documented sources for each configured state filter

Version 2.0 (August 2025)

  • Webhooks available on Starter and above
  • Added comprehensive drilling intelligence webhooks
  • Improved retry logic with exponential backoff
  • Added commodity filtering
  • Enhanced security with replay protection

Version 1.0 (January 2025)

  • Initial webhook implementation
  • Price update events
  • Basic retry logic
  • HMAC signature verification
Last Updated: 7/13/26, 10:11 AM