Webhooks API
Reservoir Mastery Exclusive
Webhooks are available exclusively to Reservoir Mastery subscribers ($129/month). This premium feature includes both price and drilling intelligence webhooks. Upgrade your plan 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
Reservoir Mastery Webhook Features
As a Reservoir Mastery subscriber, 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
Production Boost & Reservoir Mastery
Analytics alerts are available to Production Boost ($45/month) and Reservoir Mastery ($129/month) subscribers. 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
Real-time notification when a new well permit is filed. Requires Well Permits Add-on subscription.
{
"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 summarized updates rather than real-time.
{
"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 Pricing
Well permits webhooks require a Well Permits Add-on subscription:
- Starter: 1 state ($49/mo)
- Regional: TX, OK, NM ($99/mo)
- US Complete: All US ($349/mo) :::
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 States (26 total)
| State | Code | Update Timing | Source |
|---|---|---|---|
| Texas | TX | Daily ~2 PM CT | Railroad Commission |
| Oklahoma | OK | Daily ~3 PM CT | Corporation Commission |
| New Mexico | NM | Daily ~3 PM MT | Oil Conservation Division |
| Colorado | CO | Daily ~4 PM MT | COGCC |
| North Dakota | ND | Daily ~5 PM CT | DMR |
| Pennsylvania | PA | Daily ~6 PM ET | DEP |
| Wyoming | WY | Daily ~4 PM MT | WOGCC |
| Montana | MT | Daily ~4 PM MT | MBOGC |
| Louisiana | LA | Daily ~5 PM CT | DNR |
| Kansas | KS | Daily ~4 PM CT | KCC |
| California | CA | Daily ~5 PM PT | DOGGR |
| Alaska | AK | Daily ~3 PM AKT | AOGCC |
| Ohio | OH | Daily ~5 PM ET | ODNR |
| West Virginia | WV | Daily ~5 PM ET | DEP |
| Arkansas | AR | Daily ~4 PM CT | AOGC |
| Mississippi | MS | Daily ~4 PM CT | MSOBG |
| Utah | UT | Daily ~4 PM MT | DOGM |
| Nebraska | NE | Daily ~4 PM CT | NOGCC |
| New York | NY | Daily ~5 PM ET | DEC |
| Michigan | MI | Daily ~5 PM ET | EGLE |
| Florida | FL | Daily ~5 PM ET | DEP |
| Indiana | IN | Daily ~5 PM ET | DNR |
| Tennessee | TN | Daily ~5 PM CT | TDEC |
| Kentucky | KY | Daily ~5 PM ET | DMM |
| Virginia | VA | Daily ~5 PM ET | DMME |
| Illinois | IL | Daily ~4 PM CT | DNR |
| Alabama | AL | Daily ~4 PM CT | 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 active Reservoir Mastery 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: [email protected]
- Reservoir Mastery: 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
As a Reservoir Mastery subscriber, 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 26 supported US states
- All states now have daily update pipelines
- Documented update timing for each state
Version 2.0 (August 2025)
- Webhooks now exclusive to Reservoir Mastery plan
- 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