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:
| Feature | Included |
|---|---|
| Price update webhooks | ✅ All commodities |
| Drilling intelligence webhooks | ✅ Rig counts, frac spreads, permits, DUC wells |
| Webhook endpoints | 25 |
| Events per month | 250,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"
}
| Logic | Description |
|---|---|
any | Trigger if ANY condition is met |
all | Trigger 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 signalsvolatility_spike- Volatility exceeds historical normstrend_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.
| State | Code | Source |
|---|---|---|
| Texas | TX | Railroad Commission |
| Oklahoma | OK | Corporation Commission |
| New Mexico | NM | Oil Conservation Division |
| Colorado | CO | ECMC |
| North Dakota | ND | DMR |
| Pennsylvania | PA | DEP |
| Wyoming | WY | WOGCC |
| Montana | MT | MBOGC |
| Louisiana | LA | DNR |
| Kansas | KS | KCC |
| California | CA | CalGEM |
| Alaska | AK | AOGCC |
| Ohio | OH | ODNR |
| West Virginia | WV | DEP |
| Arkansas | AR | AOGC |
| Mississippi | MS | MSOGB |
| Utah | UT | DOGM |
| Nebraska | NE | NOGCC |
| New York | NY | DEC |
| Michigan | MI | EGLE |
| Florida | FL | DEP |
| Indiana | IN | DNR |
| Tennessee | TN | TDEC |
| Kentucky | KY | DMM |
| Virginia | VA | Virginia Energy |
| Illinois | IL | DNR |
| Alabama | AL | AOGB |
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 typeX-OilPriceAPI-Event-ID: Unique event identifierX-OilPriceAPI-Signature: HMAC-SHA256 signatureX-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:
- First retry: 1 minute after failure
- Second retry: 5 minutes after first retry
- 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
Check endpoint status:
GET /v1/webhooks/{webhook_id}Ensure status is "active" and no consecutive failures.
Verify URL accessibility:
- Must be HTTPS
- Must be publicly accessible
- Check firewall rules
Review event filters:
- Ensure subscribed to correct events
- Check commodity filters
Signature Verification Failing
Check timestamp:
- Ensure server time is synchronized
- Verify timestamp parsing
Verify payload formatting:
- Use raw request body, not parsed JSON
- Ensure no middleware is modifying the body
Confirm secret:
- Use the exact secret from webhook creation
- Check for trailing spaces or newlines
Missing Events
Check delivery logs:
GET /v1/webhooks/{webhook_id}/eventsVerify subscription:
- Must have an active Starter (or higher) subscription
- Check monthly event limits
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_filtersfor 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