OilPriceAPI Docs
GitHub
GitHub
  • Guides

    • Authentication
    • Security & Compliance
    • API Versioning
    • Data Freshness & Update Frequency
    • Data Quality and Validation
    • Coal Price Data - Complete Guide
    • Testing & Development
    • Error Codes Reference
    • SDK Code Examples
    • Webhook Signature Verification
    • Production Deployment Checklist
    • Service Level Agreement (SLA)
    • Rate Limiting & Response Headers
    • Troubleshooting Guide
    • Incident Response Guide
    • Video Tutorials

Testing & Development

How to test your OilPriceAPI integration: what to hit, how to keep quota usage low, and how to test error paths and webhooks.

There is no sandbox or test-key mode

OilPriceAPI has one environment: production. There are no opa_test_* keys, no separate sandbox host, and no separate test quota. Every key you create is a live key that counts against your plan's monthly quota.

Test against the real API using the two facilities that do exist:

  • the keyless demo endpoint (/v1/demo/prices) — no key, no quota consumption
  • a free-tier account (200 requests/month) used as your development account :::

What to Use for What

NeedUse thisCosts quota?
Smoke test / CI without secrets/v1/demo/pricesNo — no key required
Local development against real response shapesFree-tier key (200 req/month)Yes — against the free quota
Unit testsRecorded fixtures / mock server (below)No — no network
Pre-production verificationYour production key, small request volumeYes — against your plan quota

The Demo Endpoint (No API Key)

/v1/demo/prices returns a real, cached slice of live prices and requires no authentication. Use it in CI, in getting-started docs, and in any test that just needs to prove your HTTP plumbing works.

# Full demo slice
curl "https://api.oilpriceapi.com/v1/demo/prices"

# Single commodity
curl "https://api.oilpriceapi.com/v1/demo/prices/WTI_USD"

The demo response is a slice of production data, so field names and types match the authenticated endpoints. It is not a complete commodity list and it is not intended for production traffic — see the demo endpoint reference for exactly what it returns.

Using a Free-Tier Account for Development

The free tier includes 200 requests/month. That is enough for day-to-day development if you cache aggressively and mock in unit tests.

Recommended setup:

  1. Create a second account (or a second key) that you use only for development.
  2. Store it as OILPRICE_API_KEY_DEV; keep your production key in OILPRICE_API_KEY_PROD.
  3. Never commit either. Both are live keys — a leaked "dev" key burns real quota.
# .env.development
OILPRICE_API_KEY=your_development_key
OILPRICE_BASE_URL=https://api.oilpriceapi.com/v1

# .env.production
OILPRICE_API_KEY=your_production_key
OILPRICE_BASE_URL=https://api.oilpriceapi.com/v1

Testing Strategies

1. Unit Testing (no network)

Unit tests should not call the API at all. Record one real response and assert against a fixture, so your tests are fast, offline, and quota-free.

import json
import pathlib

FIXTURE = pathlib.Path("tests/fixtures/latest_wti.json")

def test_parses_latest_price():
    payload = json.loads(FIXTURE.read_text())

    assert payload["status"] == "success"
    price = payload["data"]["price"]
    assert isinstance(price, (int, float))

Refresh the fixture occasionally from the live API so it does not drift from the real response shape.

2. Integration Testing (keyless)

Point integration tests at the demo endpoint so CI needs no secret:

// integration.test.js
describe("OilPriceAPI Integration", () => {
  const baseURL = "https://api.oilpriceapi.com/v1";

  test("demo endpoint returns prices", async () => {
    const response = await fetch(`${baseURL}/demo/prices`);
    const body = await response.json();

    expect(response.status).toBe(200);
    expect(body.status).toBe("success");
    expect(Array.isArray(body.data.prices)).toBe(true);
  });
});

If a test genuinely needs an authenticated endpoint, gate it on the presence of a key so forks and PRs from contributors do not fail:

const apiKey = process.env.OILPRICE_API_KEY;
const authed = apiKey ? test : test.skip;

authed("authenticated latest price", async () => {
  const response = await fetch(
    "https://api.oilpriceapi.com/v1/prices/latest?by_code=WTI_USD",
    { headers: { Authorization: `Token ${apiKey}` } },
  );
  expect(response.status).toBe(200);
});

3. Error Handling Testing

Error paths are the cheapest thing to test — a 401 or a 400 costs you nothing but a round trip.

import os
import requests

def test_error_handling():
    # Invalid commodity code -> 400
    response = requests.get(
        'https://api.oilpriceapi.com/v1/prices/latest?by_code=INVALID_CODE',
        headers={'Authorization': f"Token {os.environ['OILPRICE_API_KEY']}"}
    )
    assert response.status_code == 400
    data = response.json()
    # 400 shape: { "status": "fail", "data": { "error": "invalid_code", "invalid_codes": [...] } }
    assert data['status'] == 'fail'
    assert data['data']['error'] == 'invalid_code'

    # Missing authentication -> 401
    response = requests.get('https://api.oilpriceapi.com/v1/prices/latest')
    assert response.status_code == 401

One bad code fails the whole batch

by_code is all-or-nothing: if any code in a comma-separated list is invalid, the request returns 400 invalid_code and you get no prices back. Validate codes against the commodities list before batching.

See Error Codes for the full catalogue.

4. Load Testing

Load testing hits the real API and consumes real quota, and the authenticated rate limit is 60 requests per rolling 60-second window per API key — a load test that ignores it just measures our 429 handler. Keep the concurrency inside the limit and use it to verify your backoff logic, not our throughput.

# 60 requests, well inside the 60/min window, to exercise retry/backoff paths
ab -n 60 -c 2 \
   -H "Authorization: Token $OILPRICE_API_KEY" \
   https://api.oilpriceapi.com/v1/prices/latest

If you need higher sustained throughput than the standard lane allows, contact support@oilpriceapi.com rather than load-testing your way into a throttle.

CI/CD Integration

GitHub Actions

name: API Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Install dependencies
        run: npm ci

      # Unit + demo-endpoint integration tests need no secret at all.
      - name: Run tests
        env:
          # Optional: only set this if you want the authenticated tests to run.
          # It is a LIVE key and consumes your monthly quota.
          OILPRICE_API_KEY: ${{ secrets.OILPRICE_API_KEY }}
        run: npm test

Mock Server for Offline Testing

For testing without network access, stand up a mock that mirrors the real response shape:

// mock-server.js
const express = require("express");
const app = express();

// Mock responses — shape mirrors the live API
const mockData = {
  "/v1/prices/latest": {
    status: "success",
    data: {
      price: 78.45,
      formatted: "$78.45",
      currency: "USD",
      code: "WTI_USD",
      created_at: new Date().toISOString(),
      type: "spot_price",
    },
  },
};

app.get("/v1/*", (req, res) => {
  const path = req.path;
  if (mockData[path]) {
    res.json(mockData[path]);
  } else {
    res.status(404).json({
      status: "error",
      error: { code: "NOT_FOUND" },
    });
  }
});

app.listen(3001, () => {
  console.log("Mock API server running on http://localhost:3001");
});

Testing Webhooks

Webhook deliveries are real HTTP POSTs from our servers, and every delivery is signed — there is no "simulated" webhook mode. Test against a tunnelled local server.

Using ngrok

# Start your local webhook server
node webhook-server.js

# In another terminal, expose it via ngrok
ngrok http 3000

# Configure the resulting HTTPS URL as your webhook endpoint in the dashboard
# https://abc123.ngrok.io/webhook

Webhook Test Server

Verify the signature exactly as you would in production — the HMAC is computed over payload.timestamp, not over the payload alone. See Webhook Signature Verification for the canonical implementation.

// webhook-test-server.js
const express = require("express");
const crypto = require("crypto");

const app = express();
app.use(express.json());

app.post("/webhook", (req, res) => {
  const signature = req.headers["x-oilpriceapi-signature"];
  const timestamp = req.headers["x-oilpriceapi-signature-timestamp"];
  const payload = JSON.stringify(req.body);

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

  const valid =
    signature &&
    signature.length === expectedSig.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSig));

  if (!valid) {
    return res.status(401).send("Invalid signature");
  }

  // Respond quickly, process async
  res.status(200).send("OK");
  processWebhookAsync(req.body);
});

app.listen(3000);

Testing Best Practices

1. Keep keys out of your repo

  • Never commit a key — every key is a live key
  • Use environment variables and separate dev/production keys
  • Rotate immediately if a key leaks

2. Mock in unit tests, hit the network sparingly

  • Unit tests: fixtures, no network, no quota
  • Integration tests: the keyless demo endpoint
  • Authenticated tests: a handful, gated on a key being present

3. Test rate-limit handling

  • The limit is 60 requests per rolling 60 seconds per key (see Rate Limiting)
  • Implement exponential backoff and honour Retry-After
  • Handle 429 and 402 (quota exhausted) distinctly — they mean different things

4. Test error scenarios

  • Network failures and timeouts
  • Invalid commodity codes (400, and remember it fails the whole batch)
  • Missing/invalid key (401)
  • Quota exhausted (402) and throttled (429)

5. Watch your quota

  • Every response carries X-RateLimit-Remaining and X-RateLimit-Used — log them in CI
  • Cache in development; prices update roughly every 5 minutes, so refetching in a tight loop buys you nothing

6. Validate response structure

// Validate response structure
function validatePriceResponse(response) {
  assert(response.status === "success");
  assert(typeof response.data === "object");
  assert(typeof response.data.price === "number");
  assert(response.data.price > 0);
  assert(typeof response.data.currency === "string");
}

Going to Production

  1. Swap the key — point OILPRICE_API_KEY at your production key.
  2. Update webhook URLs — replace tunnel URLs with your real HTTPS endpoints and confirm signature verification is enabled.
  3. Verify access — run a small, real request volume and check X-RateLimit-* headers.
  4. Monitor — response times, error rates, and quota burn-down.

See the Production Checklist for the full list.

Troubleshooting

My key doesn't start with opa_test_ or opa_live_

That is expected. Keys have no prefix — earlier versions of these docs described a test/live key scheme that does not exist. Use the key exactly as the dashboard shows it.

401 Unauthorized

  • The header is Authorization: Token YOUR_KEY — note the Token scheme prefix
  • Confirm the key is still active in the dashboard

402 Payment Required

You have exhausted your monthly quota. This is not a rate limit — waiting will not clear it until the quota resets on the 1st. Upgrade or wait.

429 Too Many Requests

You exceeded 60 requests in a rolling 60-second window. Back off and honour Retry-After.

Webhooks not arriving

  • The endpoint must be HTTPS and publicly reachable
  • Respond within 30 seconds, or the delivery is treated as failed and retried
  • Check that your signature verification isn't rejecting valid payloads (the HMAC covers payload.timestamp)

Support

For integration issues:

  • Email: support@oilpriceapi.com
  • Include the X-Request-Id from a failing response
  • Describe expected vs actual behaviour
Last Updated: 7/13/26, 10:16 AM
Prev
Coal Price Data - Complete Guide
Next
Error Codes Reference