Webhooks#
Receive signed JSON events when Old Cars Data observes auction changes or first ingests a bid. Create and manage the same subscriptions through the public API or Account → Webhooks. Changes made through either interface appear in the other.
A verified email address is required. Creating, editing, or resuming subscriptions, rotating secrets, sending tests, and retrying failed deliveries require an active 1K, 10K, or 100K plan. After a downgrade, you can still inspect, pause, and delete existing subscriptions. Endpoint allowances are 1 endpoint on the $49/month 1K plan, 10 endpoints on the $199/month 10K plan, and unlimited endpoints on the $699/month 100K plan. Paused endpoints count toward the allowance when creating another endpoint. Each endpoint can subscribe to multiple event types and filters; these limits count destinations, rather than deliveries.
Create a Subscription#
Deploy a public HTTPS endpoint that accepts JSON POST requests on port 443. Private or reserved networks, localhost, URL credentials, fragments, and redirects are unsupported.
In the account UI, select Add endpoint, choose event types, and optionally filter by auction ID, make, and source. Or use your existing API key:
curl -X POST https://api.oldcarsdata.com/webhooks \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-domain.com/webhooks", "description": "Track my auction", "event_types": ["bid.created", "auction.updated", "auction.ended"], "filters": { "auction_id": "12345" } }'The response contains the subscription in data and its one-time signing secret. Save the secret securely on your server; listing or retrieving subscriptions never reveals it again.
{ "data": { "id": "whe_5dbfd308-d0b7-4c11-b0d9-fd2ca67cbe7a", "url": "https://your-domain.com/webhooks", "description": "Track my auction", "event_types": ["bid.created", "auction.updated", "auction.ended"], "filters": { "auction_id": "12345" }, "enabled": true, "created_at": "2026-10-18T18:00:00Z", "updated_at": "2026-10-18T18:00:00Z" }, "secret": "whsec_BASE64_SIGNING_KEY"}Events and Filters#
| Event# | Trigger |
|---|---|
auction.created# | A new eligible source auction row is first persisted. |
auction.updated# | Meaningful stored auction fields change, excluding changes reported as auction.ended. |
auction.ended# | The source reports a transition from active or coming soon into sold, reserve not met, result unavailable, canceled, or withdrawn. |
bid.created# | A newly persisted bid row is first observed, including a historical bid newly ingested during a backfill. |
webhook.test# | An explicit test is requested through the account UI or API. This type cannot be selected as a subscription event. |
Auction changes include title, status, price, currency, year, make, model, auction end time/date, bid count, sale type, and price type. Scrape timestamps, views, watchers, and comment counts do not trigger updates. The initial and final state within a transaction determine the event: a change reversed before commit produces no event. An ended transition produces auction.ended rather than a second auction.updated for that change.
Passing the scheduled end time does not itself fire an event. Updates caused only by normalization do not fire events. Capture happens atomically with committed source writes; normalization runs separately. A normalization rebuild does not generate another set of webhook events. Our API may briefly lag the source observation represented by a webhook.
Optional filters combine with AND:
| Filter# | Behavior |
|---|---|
auction_id# | Exact auction API ID, supplied as a positive integer string. Bid events match their captured parent auction ID. |
make# | Exact, case-insensitive canonical make name, such as Porsche. |
source# | Exact, case-insensitive source name, such as bringatrailer or carsandbids. |
An omitted filter accepts every value. Model filtering is unsupported. Keep IDs as strings; converting them to JavaScript numbers can lose precision. Bid make/source filters use the parent auction's routing context. Canonical make routing uses an existing mapping or an unambiguous exact canonical name match. An unknown make keeps the observation pending only when an otherwise matching subscription needs a make filter; auction-only and source-only subscriptions do not wait for make maintenance. Required missing parent or source context also keeps an observation pending. Confirmed source exclusions and aliases are suppressed. Bid events follow their parent auction's eligibility rules, including exclusion of parent auctions ending before 2016; an old bid placement timestamp alone does not exclude a bid.
New subscriptions receive future observations only. Creation does not replay existing auctions or bids. Changing the destination URL, selected events, or filter behavior establishes a new activation boundary for future observations; already queued deliveries keep their captured payload. Description edits, reordering events, and changing make/source capitalization preserve that boundary. Resuming skips observations made while paused.
A historical bid first ingested after activation can still produce bid.created, even if its placement time is much older. Repeated polls and updates to an existing bid do not produce another event. Bid IDs identify persisted rows; upstream changes in bid identity can represent the same real-world bid with a different row ID. Use the bid history API for source coverage and the wider bid trail.
Manage Subscriptions Through the API#
All routes use Authorization: Bearer YOUR_API_KEY. Management remains available when your data-request quota is exhausted; normal burst limits still apply.
| Method# | Route | Result |
|---|---|---|
GET# | /webhooks | List your subscriptions. |
POST# | /webhooks | Create a subscription and reveal its secret once. |
GET# | /webhooks/{id} | Retrieve one subscription. |
PATCH# | /webhooks/{id} | Edit URL, description, event types, filters, or enabled state. |
DELETE# | /webhooks/{id} | Delete the subscription and its delivery history. |
POST# | /webhooks/{id}/test | Queue a test; at most one per endpoint per minute. |
POST# | /webhooks/{id}/secret/rotate | Rotate the secret; the new secret is returned in data.secret once. |
GET# | /webhooks/{id}/deliveries | Inspect the 20 most recent deliveries. |
POST# | /webhooks/{id}/deliveries/{deliveryId}/retry | Retry a failed delivery using its original event ID and payload. |
PATCH applies only the supplied top-level fields. A supplied filters object replaces all existing filters; use "filters": {} to clear them. For example:
curl -X PATCH https://api.oldcarsdata.com/webhooks/whe_YOUR_ID \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"event_types":["bid.created"],"filters":{"make":"Porsche","source":"bringatrailer"}}'Pause with {"enabled":false} and resume with {"enabled":true}. Unknown fields, duplicate or unsupported event types, invalid filters, and malformed destinations are rejected. Errors distinguish invalid input (400), missing/invalid authentication (401), account requirements (403), missing owned resources (404), and endpoint limits or test cooldown (409).
Event Payload#
Every event has id, type, api_version, created_at, and data. Version 2026-10-17 uses string IDs and integer prices/amounts. created_at is the UTC observation time in Old Cars Data, rather than a source-supplied timestamp. Event IDs are opaque strings.
Auction Event#
Auction events include data.auction with immutable captured source facts. These fields do not claim an exact historical normalized API response. data.changes identifies meaningful fields that changed; it is empty for a new auction.
{ "id": "evt_4d776fd1-3de0-460b-a692-4b181215f121", "type": "auction.updated", "api_version": "2026-10-17", "created_at": "2026-10-18T18:30:00.000Z", "data": { "auction": { "id": "12345", "source": "bringatrailer", "url": "https://bringatrailer.com/listing/example", "title": "1990 Porsche 911 Carrera Coupe", "status": "active", "price": 68000, "currency": "USD", "year": 1990, "make": "Porsche", "model": "911", "auction_end_at": "2026-10-19T19:00:00", "auction_end_date": "2026-10-19", "bid_count": 14, "sale_type": "auction", "price_type": null }, "changes": { "price": { "before": 67000, "after": 68000 }, "bid_count": { "before": 13, "after": 14 } } }}Fields may be null when unavailable. Auction make/model reflect captured source facts; the canonical make used for filtering may differ. auction_end_at is a source wall-clock timestamp without a UTC offset; do not assume UTC or append Z. auction_end_date supports date-only sources. Handle new source status values defensively.
Bid Event#
bid.created includes immutable captured bid details and a frozen parent auction ID, without bidder identity:
{ "id": "evt_bid_98765", "type": "bid.created", "api_version": "2026-10-17", "created_at": "2026-10-18T18:30:00.000Z", "data": { "bid": { "id": "98765", "auction_id": "12345", "amount": 68000, "currency": "USD", "bid_at": "2026-10-17T18:29:45" } }}bid_at is a source-reported wall-clock timestamp without a UTC offset. A webhook's captured parent ID is never silently reassigned if source records later change identity. An observation with unresolved parent identity remains pending. The current API can change or remove an auction after an event was captured.
Test Event#
A test uses the same envelope with type: "webhook.test" and data: {"message":"Your Old Cars Data webhook is connected."}.
Verify Signatures#
Deliveries follow Standard Webhooks. Every request includes:
| Header# | Value |
|---|---|
webhook-id# | The stable event ID, also present in the payload. |
webhook-timestamp# | Unix timestamp in seconds for this attempt. |
webhook-signature# | One or more space-separated v1,BASE64_SIGNATURE values. |
The signed bytes are {webhook-id}.{webhook-timestamp}.{raw body}. Secrets use a whsec_ prefix followed by a base64-encoded signing key. Use a maintained verification library to check the raw body, signature, and timestamp tolerance:
import { Webhook } from 'standardwebhooks'const verifier = new Webhook(process.env.WEBHOOK_SIGNING_SECRET!)export async function POST(request: Request) { const rawBody = await request.text() let event: unknown try { event = verifier.verify(rawBody, { 'webhook-id': request.headers.get('webhook-id') ?? '', 'webhook-timestamp': request.headers.get('webhook-timestamp') ?? '', 'webhook-signature': request.headers.get('webhook-signature') ?? '', }) } catch { return new Response('Invalid webhook signature', { status: 400 }) } // Validate the event and durably enqueue it, deduplicating by event.id. await enqueueEvent(event) return new Response(null, { status: 204 })}Install standardwebhooks in your receiver. Verify before parsing or reserializing the body. Keep secrets on your server. After rotation, attempts carry signatures from the new and immediately previous secret for 24 hours, allowing either key to verify during rollout. Rotation reveals the new secret once; rotating again replaces the previous-key overlap.
Delivery and Retries#
- A dedicated AWS worker continuously checks for queued deliveries; polling is normally about once per second, and backlogs can add delay. This is an observation feed, not an immediate source-time alert.
- Your endpoint has 10 seconds to respond. Any
2xxstatus succeeds; other statuses, redirects, timeouts, and network failures count as failures. - Each delivery has up to six attempts: the initial attempt, then retries after approximately 1 minute, 5 minutes, 30 minutes, 2 hours, and 8 hours following each failure.
- Delivery is at least once while attempts remain. Events can arrive more than once or out of order. Deduplicate by stable event ID and make downstream work safe to repeat.
- Retries preserve the original event ID and body, with a fresh timestamp and signature. A delivery can fail permanently after its retry budget is exhausted.
Return a successful response after your own durable handoff; run slow work in the background. A lost response can cause a duplicate even when your receiver processed the first request.
Inspect and Control Deliveries#
The UI's View deliveries and the API's deliveries route show the latest 20 deliveries, including status, attempts, latest HTTP response/error, scheduling details, and the public payload. Private capture metadata and encrypted secrets are never returned.
When a plan is downgraded or expires, excess endpoints are paused, keeping the oldest active endpoints within the new allowance. Settings and history remain available, and queued work for paused endpoints is canceled. Enforcement runs during management requests and each worker run, with entitlement checked again before sending. A request already in flight can still complete. Upgrading does not automatically resume paused endpoints. Pause a retained endpoint before resuming another when the active allowance is full. Delete saved endpoints or upgrade to free capacity for creating another endpoint.
Retry delivery starts a new six-attempt budget for a failed delivery. The endpoint and paid plan must be active. Pause cancels queued deliveries and further retries, including after resume. An HTTP request already in flight can still complete: a successful real delivery is recorded and consumes one unit; a failed attempt is canceled. Delete removes the subscription and its delivery history. Deletion returns HTTP 409 while a delivery attempt is active; pause the endpoint and retry deletion after the attempt finishes.
Terminal payloads and delivery history are eligible for cleanup after 30 days. Compact source identity records remain to prevent duplicate bid events after cleanup. Pending observations and unfinished deliveries do not silently expire. Keep your own durable record when you need a longer audit trail.
Delivery usage and monthly allowance#
API and MCP requests share one monthly allowance with successful webhook deliveries:
| Plan# | Monthly requests + deliveries | Webhook endpoints |
|---|---|---|
| $49 / month# | 1,000 | 1 |
| $199 / month# | 10,000 | 10 |
| $699 / month# | 100,000 | Unlimited |
One real event successfully delivered to one endpoint counts as one request. Sending the same event to two endpoints counts as two. Failed attempts, automatic retries that fail, webhook tests, and endpoint management are free. A successful retry charges once for that delivery.
When the shared allowance is exhausted, new deliveries wait without consuming retry attempts. They resume when capacity is available, including after an upgrade or the next billing period. In-flight requests temporarily reserve capacity, so a failed request can also release a slot. Pending work is retained while it waits. Unlimited endpoints on the 100K plan still share its 100,000-unit monthly allowance.
Account Overview shows usage by source, including Webhook deliveries. The Webhooks and Billing pages show the shared allowance and reset date. Endpoint delivery history shows queued work, attempts, the last HTTP result, and any quota deferral.