Webhooks

Sends signed webhooks to customer endpoints with retries and delivery logs, and verifies incoming ones. $1.00 per 100K messages + $5.00 per 100K deliveries. The base URL is https://www.motor.dev/webhooks, and the OpenAPI 3.1 specification describes each endpoint.

Try it

Sign in to try the API here with one of your team's keys. Requests use that key's permissions and spending limit. Standard charges apply.

Quick start

Webhooks sends signed events from your application to your customers' servers, with retries, history, and replay. You manage everything from your backend with a secret key (motor_sk_...). Every POST needs an Idempotency-Key: send a new value for each operation and the same value when you retry it.

1. Create a tenant for one of your customers:

curl https://api.motor.dev/webhooks/tenants \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"external_id": "customer-42", "name": "Acme Corp"}'

2. Add an event type to your catalog. Motor refuses messages of types that the catalog doesn't have:

curl https://api.motor.dev/webhooks/event_types \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"name": "invoice.paid", "description": "An invoice was paid."}'

3. Add your customer's endpoint. The response includes signing_secret (whsec_...) once. Give it to your customer so they can verify deliveries:

curl https://api.motor.dev/webhooks/endpoints \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"tenant_id": "wdt_...", "url": "https://hooks.acme.example/motor", "events": ["invoice.*"]}'

4. Send a test message to the endpoint's real receiver. It's a paid motor.test message that skips filters and transforms:

curl https://api.motor.dev/webhooks/endpoints/wde_.../test \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"payload": {"test": true}}'

5. Publish real events:

curl https://api.motor.dev/webhooks/messages \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: invoice-paid-inv_42" \
  -d '{"tenant_id": "wdt_...", "event_type": "invoice.paid", "payload": {"invoice_id": "inv_42", "amount": "49.00"}}'

Motor answers 202 once the message, its deliveries, and its charge are stored. It doesn't wait for the receiver. Follow progress with GET /messages/wdm_... and GET /deliveries?message_id=wdm_....

Webhooks is live-only: there's no test mode. Omit Motor-Mode or send Motor-Mode: live. To try things out, point a separate tenant at your own staging receiver.

Publishing messages

Each message has one tenant, one event type, and immutable bytes. Send JSON as payload, or other content as body_base64 with a content_type (application/json, text/plain, application/octet-stream, or application/x-www-form-urlencoded). Bodies can be up to 1 MiB.

Motor delivers your payload's exact bytes. It removes whitespace outside strings but never rewrites the JSON: key order, escapes, and number spellings such as 100, 1.50, and 1e2 reach receivers as you wrote them, and signatures cover those bytes.

By default a message goes to every enabled or paused endpoint of its tenant whose events and filter match. endpoint_ids narrows that to a list of up to 100 endpoints. A message reaches at most 100 endpoints, with at most 8 MiB of transformed bodies in all. Motor freezes the endpoints and each delivery's bytes when it accepts the message: later endpoints don't receive it, and later changes to filters or transforms don't rewrite it.

Everything about how to send (the URL, custom headers, authorization, signing keys, timeout, and retry delays) comes from the endpoint each time Motor sends. Fixing an endpoint's URL moves its scheduled and retrying deliveries to the new URL.

deliver_at schedules a message up to 7 days ahead. Deliveries stop at expires_at, which defaults to 72 hours after deliver_at. A retry with the same Idempotency-Key and the same meaning, even with different whitespace, key order, or number spelling, returns the first response and keeps the first bytes. external_id is for your own lookups; it doesn't deduplicate.

Message statuses come from their first deliveries: scheduled, queued, delivering, succeeded, partially_failed, failed, canceled, or no_destinations. Replays have their own statuses and never change these.

Verifying deliveries

Motor signs every delivery with the Standard Webhooks scheme, so receivers can use its maintained libraries. Each request has:

  • webhook-id: the message ID. It's the same on retries and replays: deduplicate on it.
  • webhook-timestamp: Unix seconds of this attempt.
  • webhook-signature: v1, and the Base64 HMAC-SHA256 of the ID, ., the timestamp, ., and the exact body, with the key in the endpoint's signing_secret after whsec_. During a key rotation there are two signatures, separated by a space.
  • Motor-Delivery-Id, Motor-Attempt-Id, and Motor-Event-Type: for diagnostics. The signature doesn't cover them, so put the event type in your payload if receivers must trust it.
from standardwebhooks import Webhook

wh = Webhook("whsec_...")
event = wh.verify(request.body, request.headers)  # raises if invalid

Receivers should compare signatures in constant time, reject timestamps more than 5 minutes from now, and record the webhook-id in the same transaction as their side effects. Deliveries can arrive more than once and out of order. Any 2xx response counts as received.

Motor does not support static sender IP addresses. Sender addresses may change, so receivers requiring source-IP allowlisting are unsupported. Verify every delivery's signature.egress_ipv4_cidrs in GET /capabilities is empty. Deliveries use IPv4 only, so an endpoint's host needs an IPv4 address.

Rotate a key with POST /endpoints/wde_.../rotate_secret and If-Match. The old key keeps signing for overlap_seconds (a day by default); 0 retires it at once.

Filters and transforms

An endpoint's events lists the types it wants: exact names, invoice.* for everything under invoice., or *. A filter then tests the JSON body with conditions on JSON Pointers:

"filter": {
  "all": [
    {"path": "/amount", "op": "gte", "value": 1000},
    {"path": "/currency", "op": "in", "value": ["usd", "eur"]}
  ]
}

Combine conditions with all, any, and not, up to 5 levels and 50 conditions. Operators are eq, ne, in, exists, gt, gte, lt, lte, and prefix. Numbers compare exactly, so 1.0e3 equals 1000. A missing value differs from null: only exists: false matches it.

A transform builds a new JSON object from pointers and literals:

"transform": {
  "fields": [
    {"name": "invoice", "from": "/invoice_id", "required": true},
    {"name": "source", "value": "billing"}
  ]
}

Fields are sorted by name, and copied values keep their exact bytes. If a transform fails, such as for a missing required field, Motor refuses the whole message with 422 transform_failed. To check a filter and transform without sending or paying, call POST /previews with an endpoint_id and a payload.

Retries, replay, and recovery

Motor retries connection errors, timeouts, 408, 425, 429, and 5xx. The default delays are 15 seconds, 1 minute, 5 minutes, 15 minutes, 1 hour, 4 hours, 12 hours, and 1 day, each with up to 20% added, for nine attempts at most. A longer Retry-After on a 429 or 503 wins. Redirects and other 4xx responses fail at once. Automatic retries are included in the delivery price.

A 410, or a destination that resolves to a private address, disables the endpoint and cancels its waiting deliveries. After ten failures in a row, the endpoint's circuit opens for a minute; then one delivery probes it, and each failed probe doubles the wait up to an hour. Fix the receiver, then call POST /endpoints/wde_.../resume.

  • POST /deliveries/wdd_.../retry_now makes a delivery that waits to retry due now. It uses its existing attempts and costs nothing.
  • POST /deliveries/wdd_.../replay sends a finished delivery again as a new paid generation, with the same webhook-id. "configuration": "current" applies the endpoint's current transform.
  • POST /replays replays an endpoint's failed and expired deliveries in a window of up to 30 days. Each batch is charged as it's created; the job stops at the key's spending limit or at max_cost_usd.
  • POST /messages/wdm_.../cancel stops deliveries that haven't started. POST /messages/wdm_.../redact erases a message's content for good and cancels all of its deliveries.

Failed and expired deliveries are your dead letters: list them with GET /deliveries?status=failed. Poll GET /events for failures, disabled endpoints, and finished replays.

Receiving other providers' webhooks

A source gives another provider a receive URL. Motor verifies each request, deduplicates it, and turns it into a message of the source's tenant, which then fans out like any other. Create one with the provider's secret:

curl https://api.motor.dev/webhooks/sources \
  -H "Authorization: Bearer $MOTOR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"tenant_id": "wdt_...", "name": "Stripe live", "verification": {"kind": "stripe", "secret": "whsec_..."}, "event_type": "stripe.event", "event_type_path": "/type", "event_type_prefix": "stripe."}'

The response includes receive_url once. Verifiers are standard_webhooks, stripe, github, shopify, slack, twilio, bearer, and hmac, a template for other signed providers. Motor checks the original bytes and the URL as called, and refuses a request whose security header appears twice.

Requests with the same provider event ID, or for most verifiers the same body within 24 hours, are duplicates: Motor answers 200 with the first message's ID and doesn't charge again. Slack gets an empty 200 (and its URL verification challenge), and Twilio gets <Response/>. Motor answers only after the message and its charge are stored, so a provider that sees an error retries safely.

Every mapped event type must be in your catalog. Captures are charged to the source's billing_key_id, the key that created it by default. Rotate the URL with rotate_token and the secret with rotate_secret, each with an overlap of up to a day.

Pricing

Every accepted message is paid. There's no free allowance.

MeterPriceCharged for
webhooks.ingest_64k$1.00 / 100,000Each 64 KiB of a message, at least one unit, even with no destinations
webhooks.delivery_16k$5.00 / 100,000Each 16 KiB of each endpoint's delivery, at least one unit
ExampleCharge
1,000,000 one-KiB messages, one endpoint each$60
The same messages, three endpoints each$160
One 100-KiB message, two endpoints$0.00072
Replay one of those deliveries$0.00035
One one-KiB message that matches no endpoint$0.00001

The price of a message, with its units and rate version, is in its price. Automatic retries are included. Replays and endpoint tests are charged like new deliveries. Failed, canceled, and expired deliveries aren't refunded. Charges count toward each key's monthly spending limit; work that would exceed it gets 402 spending_limit. See daily charges in Usage.

Limits and retention

LimitDefault
Message admissions and source captures100 a second
Configuration writes20 a second
Reads100 a second
Tenants / event types / endpoints / sources10,000 / 1,000 / 10,000 / 1,000
Endpoints of a tenant100
Deliveries waiting to be sent100,000
Stored message content10 GiB
Requests in flight to all your endpoints3
Requests in flight for all teams6

GET /capabilities shows your effective limits. To raise one, contact support@motor.dev.

Delivery capacity is small for now. Motor sends at most 3 requests of your team at once, and 6 for all teams together, and these can't be raised yet. Each request holds its place until the receiver answers, so throughput depends on how fast your receivers are: at the 30-second timeout, 3 requests are about 0.1 attempts a second. The dispatch_rate of 1,000 attempts a second and the admission_rate of 100 messages a second are ceilings that Motor hasn't yet shown under load, so GET /capabilities marks them "measured": false. A team can also store at most 4 messages at once: publish with at most 4 requests in flight, and retry a 429 after its Retry-After.

Message content and attempt history stay 30 days by default; set payload_retention_days (1 to 30) with PATCH /settings. Content stays at least until its deliveries expire. Receiver response bodies are kept only if you turn on response_body_logging, for up to 7 days. Operational events stay 30 days, and idempotency keys and source event IDs 35 days.

Migrating from Svix, Hookdeck, or Convoy

Motor doesn't import from other providers or impersonate their APIs. Move with a script that you can run more than once:

  1. List your applications or tenants and endpoints with the provider's API: Svix applications and endpoints, Hookdeck connections and destinations, or Convoy projects and endpoints.
  2. Create a Motor tenant for each, with the provider's ID as external_id, and its endpoints, each with a stable Idempotency-Key such as migrate-endpoint-<old ID>, so a rerun creates nothing twice.
  3. Create your event types in Motor's catalog. For inbound traffic (Svix Ingest, Hookdeck sources, Convoy sources), create Motor sources and enter each provider's signing secret yourself; Motor never fetches credentials from another vendor.
  4. Send a paid test message to each endpoint with POST /endpoints/wde_.../test.
  5. Publish each event to both providers for a while, with your own event ID in the payload, so receivers can ignore the copy.
  6. Switch your publisher to Motor and stop the old one.

Tell your receivers about two changes: Motor signs with Standard Webhooks headers and new whsec_ keys (Convoy and Hookdeck customers change their verification code; Svix customers keep it and update the key), and message IDs change, so deduplicate on your own event ID during the switch.

Rates and settings

GET/webhooks/capabilities

Get capabilities

Returns the region, delivery addresses, source verifiers, content types, hard bounds, and your team's effective limits.

ParameterLocationDescription
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

200The capabilities.

{
  "service_id": "webhooks",
  "region": "us-east-1",
  "egress_ipv4_cidrs": [],
  "ipv6_delivery": false,
  "verification_kinds": [
    "standard_webhooks",
    "stripe",
    "github",
    "hmac",
    "shopify",
    "slack",
    "twilio",
    "bearer"
  ],
  "content_types": [
    "application/json",
    "text/plain",
    "application/octet-stream",
    "application/x-www-form-urlencoded"
  ],
  "limits": {
    "max_body_bytes": {
      "value": 1048576,
      "unit": "bytes"
    },
    "max_fanout_endpoints": {
      "value": 100,
      "unit": "endpoints"
    },
    "max_transformed_bytes_per_message": {
      "value": 8388608,
      "unit": "bytes"
    },
    "max_attempts": {
      "value": 9,
      "unit": "attempts"
    },
    "delivery_window_hours": {
      "value": 72,
      "unit": "hours"
    },
    "max_schedule_days": {
      "value": 7,
      "unit": "days"
    },
    "max_endpoints_per_tenant": {
      "value": 100,
      "unit": "endpoints"
    },
    "read_requests_per_second": {
      "value": 100,
      "unit": "requests per second"
    },
    "config_writes_per_second": {
      "value": 20,
      "unit": "requests per second"
    },
    "admission_rate": {
      "value": 100,
      "unit": "messages per second",
      "measured": false
    },
    "source_requests_per_second": {
      "value": 50,
      "unit": "requests per second"
    },
    "dispatch_rate": {
      "value": 1000,
      "unit": "attempts per second",
      "measured": false
    },
    "max_concurrency": {
      "value": 3,
      "unit": "requests"
    },
    "max_endpoint_concurrency": {
      "value": 3,
      "unit": "requests"
    },
    "service_concurrency": {
      "value": 6,
      "unit": "requests across all teams"
    },
    "max_tenants": {
      "value": 10000,
      "unit": "tenants"
    },
    "max_event_types": {
      "value": 1000,
      "unit": "event types"
    },
    "max_endpoints": {
      "value": 10000,
      "unit": "endpoints"
    },
    "max_sources": {
      "value": 1000,
      "unit": "sources"
    },
    "max_pending_deliveries": {
      "value": 100000,
      "unit": "deliveries"
    },
    "max_content_bytes": {
      "value": 10737418240,
      "unit": "bytes"
    },
    "max_active_replays": {
      "value": 10,
      "unit": "jobs"
    }
  },
  "rate_version": "2026-10-08"
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

422 Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`Motor-Mode` must be live or test."
  }
}

429Too many reads: 100 a second, bursts of 200 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

GET/webhooks/rates

Get rates

Returns the published rates. webhooks.ingest_64k is $0.00001 for each 64 KiB of an accepted message's body, at least 1 unit, charged even when the message has no destinations. webhooks.delivery_16k is $0.00005 for each 16 KiB of each delivery generation's body, at least 1 unit per endpoint. Automatic retries are free; a replay is a new paid delivery generation; an endpoint test pays ingestion and delivery. Accepted work keeps the rate version that priced it. There is no free allowance. Usage by meter is at GET /meta/usage.

ParameterLocationDescription
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

200The rates.

{
  "currency": "USD",
  "version": "2026-10-08",
  "effective_at": "2026-10-08T00:00:00Z",
  "meters": [
    {
      "id": "webhooks.ingest_64k",
      "unit": "unit",
      "unit_bytes": 65536,
      "unit_price_usd": "0.00001"
    },
    {
      "id": "webhooks.delivery_16k",
      "unit": "unit",
      "unit_bytes": 16384,
      "unit_price_usd": "0.00005"
    }
  ]
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

422 Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`Motor-Mode` must be live or test."
  }
}

429Too many reads: 100 a second, bursts of 200 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

GET/webhooks/settings

Get settings

Returns your team's retention and logging settings and its effective limits. The ETag is "wd:settings:<configuration_version>".

ParameterLocationDescription
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

200The settings.

{
  "configuration_version": "1",
  "payload_retention_days": 30,
  "response_body_logging": false,
  "response_retention_days": 7,
  "approved_configuration_version": "1",
  "limits": {
    "max_body_bytes": {
      "value": 1048576,
      "unit": "bytes"
    },
    "max_fanout_endpoints": {
      "value": 100,
      "unit": "endpoints"
    },
    "max_transformed_bytes_per_message": {
      "value": 8388608,
      "unit": "bytes"
    },
    "max_attempts": {
      "value": 9,
      "unit": "attempts"
    },
    "delivery_window_hours": {
      "value": 72,
      "unit": "hours"
    },
    "max_schedule_days": {
      "value": 7,
      "unit": "days"
    },
    "max_endpoints_per_tenant": {
      "value": 100,
      "unit": "endpoints"
    },
    "read_requests_per_second": {
      "value": 100,
      "unit": "requests per second"
    },
    "config_writes_per_second": {
      "value": 20,
      "unit": "requests per second"
    },
    "admission_rate": {
      "value": 100,
      "unit": "messages per second",
      "measured": false
    },
    "source_requests_per_second": {
      "value": 50,
      "unit": "requests per second"
    },
    "dispatch_rate": {
      "value": 1000,
      "unit": "attempts per second",
      "measured": false
    },
    "max_concurrency": {
      "value": 3,
      "unit": "requests"
    },
    "max_endpoint_concurrency": {
      "value": 3,
      "unit": "requests"
    },
    "service_concurrency": {
      "value": 6,
      "unit": "requests across all teams"
    },
    "max_tenants": {
      "value": 10000,
      "unit": "tenants"
    },
    "max_event_types": {
      "value": 1000,
      "unit": "event types"
    },
    "max_endpoints": {
      "value": 10000,
      "unit": "endpoints"
    },
    "max_sources": {
      "value": 1000,
      "unit": "sources"
    },
    "max_pending_deliveries": {
      "value": 100000,
      "unit": "deliveries"
    },
    "max_content_bytes": {
      "value": 10737418240,
      "unit": "bytes"
    },
    "max_active_replays": {
      "value": 10,
      "unit": "jobs"
    }
  }
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

422 Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`Motor-Mode` must be live or test."
  }
}

429Too many reads: 100 a second, bursts of 200 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

PATCH/webhooks/settings

Update settings

Changes retention and logging. Omitted fields keep their values; an unchanged request keeps the version. A shorter payload_retention_days applies to stored messages at once (bodies of active deliveries stay until they expire); a longer one never restores erased bytes. Limits can't be changed here: spending is bounded by your key and account limits.

ParameterLocationDescription
If-MatchheaderThe ETag of the settings from your last read, in quotes, such as "wd:wde_6mNbVc8xZaS2dFgH4jKlQw:3". Missing is 428 precondition_required; stale is 412 revision_conflict (the response carries the current ETag); malformed is 422 invalid_field.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Request body
{
  "payload_retention_days": 14,
  "response_body_logging": true
}
FieldTypeDescription
payload_retention_daysintegerDays to keep message and delivery bodies, 1 to 30.
response_body_loggingbooleanWhether attempts keep up to 4 KiB of response bodies.
response_retention_daysintegerDays to keep response bodies, 1 to 7.
Responses

200The settings, with a new ETag if they changed.

{
  "configuration_version": "2",
  "payload_retention_days": 14,
  "response_body_logging": true,
  "response_retention_days": 7,
  "approved_configuration_version": "1",
  "limits": {
    "max_body_bytes": {
      "value": 1048576,
      "unit": "bytes"
    },
    "max_fanout_endpoints": {
      "value": 100,
      "unit": "endpoints"
    },
    "max_transformed_bytes_per_message": {
      "value": 8388608,
      "unit": "bytes"
    },
    "max_attempts": {
      "value": 9,
      "unit": "attempts"
    },
    "delivery_window_hours": {
      "value": 72,
      "unit": "hours"
    },
    "max_schedule_days": {
      "value": 7,
      "unit": "days"
    },
    "max_endpoints_per_tenant": {
      "value": 100,
      "unit": "endpoints"
    },
    "read_requests_per_second": {
      "value": 100,
      "unit": "requests per second"
    },
    "config_writes_per_second": {
      "value": 20,
      "unit": "requests per second"
    },
    "admission_rate": {
      "value": 100,
      "unit": "messages per second",
      "measured": false
    },
    "source_requests_per_second": {
      "value": 50,
      "unit": "requests per second"
    },
    "dispatch_rate": {
      "value": 1000,
      "unit": "attempts per second",
      "measured": false
    },
    "max_concurrency": {
      "value": 3,
      "unit": "requests"
    },
    "max_endpoint_concurrency": {
      "value": 3,
      "unit": "requests"
    },
    "service_concurrency": {
      "value": 6,
      "unit": "requests across all teams"
    },
    "max_tenants": {
      "value": 10000,
      "unit": "tenants"
    },
    "max_event_types": {
      "value": 1000,
      "unit": "event types"
    },
    "max_endpoints": {
      "value": 10000,
      "unit": "endpoints"
    },
    "max_sources": {
      "value": 1000,
      "unit": "sources"
    },
    "max_pending_deliveries": {
      "value": 100000,
      "unit": "deliveries"
    },
    "max_content_bytes": {
      "value": 10737418240,
      "unit": "bytes"
    },
    "max_active_replays": {
      "value": 10,
      "unit": "jobs"
    }
  }
}

400The body isn't valid JSON, isn't an object, or has a duplicate key (invalid_json).

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Duplicate key \"name\" at byte 31."
  }
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

412If-Match isn't the current settings ETag (revision_conflict).

{
  "error": {
    "code": "revision_conflict",
    "message": "The settings changed since you read them. Read them again and retry with their new ETag."
  }
}

413The body is larger than 1 MiB (body_too_large).

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 1048576 bytes."
  }
}

415The body isn't application/json (unsupported_media_type).

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json."
  }
}

422A field is invalid or unknown, or If-Match is malformed (invalid_field). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`payload_retention_days` must be from 1 to 30."
  }
}

428If-Match is missing (precondition_required).

{
  "error": {
    "code": "precondition_required",
    "message": "Send the ETag of the resource in the If-Match header. Read the resource to get its current ETag."
  }
}

429Too many configuration writes: 20 a second, bursts of 40 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

Tenants

POST/webhooks/tenants

Create a tenant

Creates one of your customers. external_id is your ID for it: unique in your team, immutable, and reserved for 90 days after the tenant is deleted. At most 10,000 tenants by default.

ParameterLocationDescription
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. A retry with the same key and the same request (compared semantically, after defaults) returns the first response with Idempotency-Replayed: true for 35 days; a different request with the key is 409 idempotency_conflict, and a retry while the first is running is 409 request_in_progress with Retry-After: 1. A failed validation doesn't use the key. A missing key is 422 invalid_field.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Request body
{
  "external_id": "acct_1042",
  "name": "Acme Robotics",
  "metadata": {
    "plan": "growth"
  }
}
FieldTypeDescription
external_idrequiredstringYour ID for the customer: 1 to 128 letters, digits, _, ., :, and -.
namerequiredstring1 to 128 UTF-8 bytes.
metadata
Responses

201The tenant.

{
  "id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
  "external_id": "acct_1042",
  "name": "Acme Robotics",
  "metadata": {
    "plan": "growth"
  },
  "status": "active",
  "version": "1",
  "created_at": "2026-10-09T18:22:01Z",
  "updated_at": "2026-10-09T18:22:01Z"
}

400The body isn't valid JSON, isn't an object, or has a duplicate key (invalid_json).

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Duplicate key \"name\" at byte 31."
  }
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

409Another tenant has the external_id, including a deleted one within 90 days (resource_in_use, with resource_id), or the team is at its tenant limit (quota_exceeded). Also idempotency_conflict for a different request with the same Idempotency-Key, and request_in_progress while it's running.

{
  "error": {
    "code": "resource_in_use",
    "message": "Tenant wdt_1aB2cD3eF4gH5iJ6kL7mN8 has the external ID acct_1042. A deleted tenant keeps its external ID for 90 days.",
    "resource_id": "wdt_1aB2cD3eF4gH5iJ6kL7mN8"
  }
}

413The body is larger than 1 MiB (body_too_large).

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 1048576 bytes."
  }
}

415The body isn't application/json (unsupported_media_type).

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json."
  }
}

422A field is missing, invalid, or unknown, or Idempotency-Key is missing (invalid_field). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`external_id` must be 1 to 128 characters of letters, digits, `_`, `.`, `:`, and `-`."
  }
}

429Too many configuration writes: 20 a second, bursts of 40 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

GET/webhooks/tenants

List tenants

Lists tenants, deleted ones included, oldest first.

ParameterLocationDescription
external_idqueryOnly the tenant with this external ID.
limitqueryFrom 1 to 200; 50 by default.
cursorquerynext_cursor from the previous page. It's bound to your team, this list and its filters; another is 422 invalid_cursor. Results are oldest first; records changed between pages may move or disappear.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

200A page of tenants.

{
  "data": [
    {
      "id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
      "external_id": "acct_1042",
      "name": "Acme Robotics",
      "metadata": {
        "plan": "growth"
      },
      "status": "active",
      "version": "1",
      "created_at": "2026-10-09T18:22:01Z",
      "updated_at": "2026-10-09T18:22:01Z"
    }
  ],
  "next_cursor": null
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

422limit is invalid (invalid_field), or the cursor isn't for this list (invalid_cursor). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_cursor",
    "message": "The cursor isn't valid for this list. Use next_cursor from the previous response.",
    "field": "cursor"
  }
}

429Too many reads: 100 a second, bursts of 200 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

GET/webhooks/tenants/{id}

Get a tenant

Returns a tenant, active or deleted, with its ETag.

ParameterLocationDescription
idpathThe tenant's ID, wdt_.... Another team's ID is 404.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

200The tenant.

{
  "id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
  "external_id": "acct_1042",
  "name": "Acme Robotics",
  "metadata": {
    "plan": "growth"
  },
  "status": "active",
  "version": "1",
  "created_at": "2026-10-09T18:22:01Z",
  "updated_at": "2026-10-09T18:22:01Z"
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The tenant doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The tenant doesn't exist."
  }
}

422 Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`Motor-Mode` must be live or test."
  }
}

429Too many reads: 100 a second, bursts of 200 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

PATCH/webhooks/tenants/{id}

Update a tenant

Changes the name or the metadata. Omitted fields keep their values; metadata replaces all pairs. An unchanged request keeps the version.

ParameterLocationDescription
idpathThe tenant's ID, wdt_.... Another team's ID is 404.
If-MatchheaderThe ETag of the tenant from your last read, in quotes, such as "wd:wde_6mNbVc8xZaS2dFgH4jKlQw:3". Missing is 428 precondition_required; stale is 412 revision_conflict (the response carries the current ETag); malformed is 422 invalid_field.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Request body
{
  "name": "Acme Robotics Inc."
}
FieldTypeDescription
namestring
metadata
Responses

200The tenant.

{
  "id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
  "external_id": "acct_1042",
  "name": "Acme Robotics Inc.",
  "metadata": {
    "plan": "growth"
  },
  "status": "active",
  "version": "2",
  "created_at": "2026-10-09T18:22:01Z",
  "updated_at": "2026-10-09T18:22:03Z"
}

400The body isn't valid JSON, isn't an object, or has a duplicate key (invalid_json).

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Duplicate key \"name\" at byte 31."
  }
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The tenant doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The tenant doesn't exist."
  }
}

412If-Match is stale (revision_conflict). The response's ETag is current: read the resource again and retry.

{
  "error": {
    "code": "revision_conflict",
    "message": "The resource changed since you read it. Read it again and retry with its new ETag.",
    "resource_id": "wde_6mNbVc8xZaS2dFgH4jKlQw"
  }
}

413The body is larger than 1 MiB (body_too_large).

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 1048576 bytes."
  }
}

415The body isn't application/json (unsupported_media_type).

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json."
  }
}

422A field is invalid or unknown, or If-Match is malformed (invalid_field). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`name` must be from 1 to 128 bytes."
  }
}

428If-Match is missing (precondition_required).

{
  "error": {
    "code": "precondition_required",
    "message": "Send the ETag of the resource in the If-Match header. Read the resource to get its current ETag."
  }
}

429Too many configuration writes: 20 a second, bursts of 40 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

DELETE/webhooks/tenants/{id}

Delete a tenant

Deletes the tenant, its endpoints, and its sources at once: no new messages or captures, queued deliveries are canceled, and its content is erased. Requests already in flight may complete. Deletion can't be undone; charges already made stay.

ParameterLocationDescription
idpathThe tenant's ID, wdt_.... Another team's ID is 404.
If-MatchheaderThe ETag of the tenant from your last read, in quotes, such as "wd:wde_6mNbVc8xZaS2dFgH4jKlQw:3". Missing is 428 precondition_required; stale is 412 revision_conflict (the response carries the current ETag); malformed is 422 invalid_field.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

204Deleted.

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The tenant doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The tenant doesn't exist."
  }
}

412If-Match is stale (revision_conflict). The response's ETag is current: read the resource again and retry.

{
  "error": {
    "code": "revision_conflict",
    "message": "The resource changed since you read it. Read it again and retry with its new ETag.",
    "resource_id": "wde_6mNbVc8xZaS2dFgH4jKlQw"
  }
}

422If-Match is malformed (invalid_field). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`If-Match` must be one strong ETag in quotes."
  }
}

428If-Match is missing (precondition_required).

{
  "error": {
    "code": "precondition_required",
    "message": "Send the ETag of the resource in the If-Match header. Read the resource to get its current ETag."
  }
}

429Too many configuration writes: 20 a second, bursts of 40 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

Event types

POST/webhooks/event_types

Create an event type

Adds a type to your catalog. Messages need an active type; sources may only emit active types. Names are immutable and unique, archived ones included. At most 1,000 types by default.

ParameterLocationDescription
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. A retry with the same key and the same request (compared semantically, after defaults) returns the first response with Idempotency-Replayed: true for 35 days; a different request with the key is 409 idempotency_conflict, and a retry while the first is running is 409 request_in_progress with Retry-After: 1. A failed validation doesn't use the key. A missing key is 422 invalid_field.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Request body
{
  "name": "invoice.paid",
  "description": "An invoice was paid in full.",
  "example": {
    "invoice_id": "inv_42",
    "amount": "49.00"
  }
}
FieldTypeDescription
namerequiredstringLowercase dot-separated words, such as invoice.paid. Immutable. webhooks.* and motor.* are reserved.
descriptionstringAt most 4,096 bytes. Empty by default.
exampleAny JSON of at most 16 KiB, kept exactly as written, or null (the default).
Responses

201The event type.

{
  "id": "wdy_9QwErT5yUiOp3aSdFgHjKz",
  "name": "invoice.paid",
  "description": "An invoice was paid in full.",
  "example": {
    "invoice_id": "inv_42",
    "amount": "49.00"
  },
  "archived": false,
  "version": "1",
  "created_at": "2026-10-09T18:22:01Z",
  "updated_at": "2026-10-09T18:22:01Z"
}

400The body isn't valid JSON, isn't an object, or has a duplicate key (invalid_json).

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Duplicate key \"name\" at byte 31."
  }
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

409The name exists, archived or not (resource_in_use, with resource_id; reactivate an archived type with PATCH and archived: false), or the team is at its limit (quota_exceeded). Also idempotency_conflict for a different request with the same Idempotency-Key, and request_in_progress while it's running.

{
  "error": {
    "code": "resource_in_use",
    "message": "The event type invoice.paid exists.",
    "resource_id": "wdy_9QwErT5yUiOp3aSdFgHjKz"
  }
}

413The body is larger than 1 MiB (body_too_large).

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 1048576 bytes."
  }
}

415The body isn't application/json (unsupported_media_type).

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json."
  }
}

422The name is invalid or reserved (webhooks.*, motor.*), a field is invalid or unknown, or Idempotency-Key is missing (invalid_field). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`name` can't start with webhooks. or motor., which Motor reserves."
  }
}

429Too many configuration writes: 20 a second, bursts of 40 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

GET/webhooks/event_types

List event types

Lists your catalog, archived types included, oldest first.

ParameterLocationDescription
namequeryOnly the type with this name.
limitqueryFrom 1 to 200; 50 by default.
cursorquerynext_cursor from the previous page. It's bound to your team, this list and its filters; another is 422 invalid_cursor. Results are oldest first; records changed between pages may move or disappear.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

200A page of event types.

{
  "data": [
    {
      "id": "wdy_9QwErT5yUiOp3aSdFgHjKz",
      "name": "invoice.paid",
      "description": "An invoice was paid in full.",
      "example": {
        "invoice_id": "inv_42",
        "amount": "49.00"
      },
      "archived": false,
      "version": "1",
      "created_at": "2026-10-09T18:22:01Z",
      "updated_at": "2026-10-09T18:22:01Z"
    }
  ],
  "next_cursor": null
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

422limit is invalid (invalid_field), or the cursor isn't for this list (invalid_cursor). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`limit` must be from 1 to 200."
  }
}

429Too many reads: 100 a second, bursts of 200 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

GET/webhooks/event_types/{id}

Get an event type

Returns an event type with its ETag.

ParameterLocationDescription
idpathThe event type's ID, wdy_.... Another team's ID is 404.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

200The event type.

{
  "id": "wdy_9QwErT5yUiOp3aSdFgHjKz",
  "name": "invoice.paid",
  "description": "An invoice was paid in full.",
  "example": {
    "invoice_id": "inv_42",
    "amount": "49.00"
  },
  "archived": false,
  "version": "1",
  "created_at": "2026-10-09T18:22:01Z",
  "updated_at": "2026-10-09T18:22:01Z"
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The event type doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The event type doesn't exist."
  }
}

422 Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`Motor-Mode` must be live or test."
  }
}

429Too many reads: 100 a second, bursts of 200 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

PATCH/webhooks/event_types/{id}

Update an event type

Changes the description or the example, or reactivates an archived type with archived: false. The name can't change.

ParameterLocationDescription
idpathThe event type's ID, wdy_.... Another team's ID is 404.
If-MatchheaderThe ETag of the event type from your last read, in quotes, such as "wd:wde_6mNbVc8xZaS2dFgH4jKlQw:3". Missing is 428 precondition_required; stale is 412 revision_conflict (the response carries the current ETag); malformed is 422 invalid_field.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Request body
{
  "archived": false
}
FieldTypeDescription
descriptionstringAt most 4,096 bytes.
exampleAny JSON of at most 16 KiB, or null.
archivedbooleanOnly false, which reactivates the type. DELETE archives it.
Responses

200The event type.

{
  "id": "wdy_9QwErT5yUiOp3aSdFgHjKz",
  "name": "invoice.paid",
  "description": "An invoice was paid in full.",
  "example": {
    "invoice_id": "inv_42",
    "amount": "49.00"
  },
  "archived": false,
  "version": "3",
  "created_at": "2026-10-09T18:22:01Z",
  "updated_at": "2026-10-09T18:22:03Z"
}

400The body isn't valid JSON, isn't an object, or has a duplicate key (invalid_json).

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Duplicate key \"name\" at byte 31."
  }
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The event type doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The event type doesn't exist."
  }
}

412If-Match is stale (revision_conflict). The response's ETag is current: read the resource again and retry.

{
  "error": {
    "code": "revision_conflict",
    "message": "The resource changed since you read it. Read it again and retry with its new ETag.",
    "resource_id": "wde_6mNbVc8xZaS2dFgH4jKlQw"
  }
}

413The body is larger than 1 MiB (body_too_large).

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 1048576 bytes."
  }
}

415The body isn't application/json (unsupported_media_type).

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json."
  }
}

422A field is invalid or unknown, archived is true, or If-Match is malformed (invalid_field). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`archived` can only be false, which reactivates the type. DELETE archives it."
  }
}

428If-Match is missing (precondition_required).

{
  "error": {
    "code": "precondition_required",
    "message": "Send the ETag of the resource in the If-Match header. Read the resource to get its current ETag."
  }
}

429Too many configuration writes: 20 a second, bursts of 40 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

DELETE/webhooks/event_types/{id}

Archive an event type

Archives the type: new messages and captures of it are refused with 422 invalid_event_type, while retained messages stay readable and replayable. Archiving an archived type changes nothing.

ParameterLocationDescription
idpathThe event type's ID, wdy_.... Another team's ID is 404.
If-MatchheaderThe ETag of the event type from your last read, in quotes, such as "wd:wde_6mNbVc8xZaS2dFgH4jKlQw:3". Missing is 428 precondition_required; stale is 412 revision_conflict (the response carries the current ETag); malformed is 422 invalid_field.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

204Archived.

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The event type doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The event type doesn't exist."
  }
}

412If-Match is stale (revision_conflict). The response's ETag is current: read the resource again and retry.

{
  "error": {
    "code": "revision_conflict",
    "message": "The resource changed since you read it. Read it again and retry with its new ETag.",
    "resource_id": "wde_6mNbVc8xZaS2dFgH4jKlQw"
  }
}

422If-Match is malformed (invalid_field). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`If-Match` must be one strong ETag in quotes."
  }
}

428If-Match is missing (precondition_required).

{
  "error": {
    "code": "precondition_required",
    "message": "Send the ETag of the resource in the If-Match header. Read the resource to get its current ETag."
  }
}

429Too many configuration writes: 20 a second, bursts of 40 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

Endpoints

POST/webhooks/endpoints

Create an endpoint

Creates a receiver for a tenant's messages and returns its signing_secret once. Every delivery is signed with Standard Webhooks headers (see the service description). At most 100 endpoints per tenant and 10,000 per team by default.

ParameterLocationDescription
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. A retry with the same key and the same request (compared semantically, after defaults) returns the first response with Idempotency-Replayed: true for 35 days; a different request with the key is 409 idempotency_conflict, and a retry while the first is running is 409 request_in_progress with Retry-After: 1. A failed validation doesn't use the key. A missing key is 422 invalid_field.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Request body
{
  "tenant_id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
  "url": "https://hooks.acme.example/motor?team=7",
  "name": "Billing receiver",
  "events": [
    "invoice.*"
  ],
  "filter": {
    "all": [
      {
        "path": "/amount",
        "op": "prefix",
        "value": "4"
      }
    ]
  },
  "headers": {
    "X-Acme-Tenant": "1042"
  },
  "authorization": "Bearer acme_live_9f8e7d6c"
}
FieldTypeDescription
tenant_idrequiredstringAn active tenant. Immutable.
urlrequiredstringHTTPS on port 443 to a public host name or IPv4 address, at most 2,048 bytes, without user information or a fragment. Private, loopback, Motor, and .local/.internal hosts are 422 unsafe_url. The query is kept exactly and treated as secret.
namestringWebhook by default.
descriptionstringAt most 4,096 bytes; empty by default.
eventsarray1 to 50 event type patterns: a name, prefix.*, or *. ["*"] by default. Patterns may name types you haven't created yet.
filternull (the default) matches every message of the event types.
transformnull (the default) sends the message's bytes unchanged.
statusstringenabled (the default) or paused. A paused endpoint queues deliveries while their deadlines run. A disabled endpoint needs /resume.
rate_per_secondinteger50 by default.
max_concurrencyinteger3 by default (or your team's cap, if lower), and at most 3: Motor runs at most 3 attempts of a team at once. 1 with a rate of 1 sends one at a time, though retries and replays can still reorder.
retry_delays_secondsarray0 to 8 delays of 1 to 86,400 seconds, adding up to at most 259,200. [15, 60, 300, 900, 3600, 14400, 43200, 86400] by default.
timeout_secondsinteger10 by default.
headersobjectCustom headers: at most 20 token names, values of at most 1,024 bytes without line breaks, 8 KiB in all. Motor's own and hop-by-hop headers (host, content-type, authorization, cookie, webhook-*, motor-*, x-forwarded-*, and others) aren't allowed. Values are write-only. {} by default.
authorizationstring or nullThe Authorization header to send, such as Bearer ...; write-only. null by default.
metadata
Responses

201The endpoint, with signing_secret. An idempotent replay shows the secret only to the key that created it, while that secret is current.

{
  "id": "wde_6mNbVc8xZaS2dFgH4jKlQw",
  "tenant_id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
  "name": "Billing receiver",
  "description": "",
  "url": "https://hooks.acme.example/motor?team=7",
  "events": [
    "invoice.*"
  ],
  "filter": {
    "all": [
      {
        "path": "/amount",
        "op": "prefix",
        "value": "4"
      }
    ]
  },
  "transform": null,
  "transform_version": "1",
  "status": "enabled",
  "disabled_reason": null,
  "rate_per_second": 50,
  "max_concurrency": 3,
  "retry_delays_seconds": [
    15,
    60,
    300,
    900,
    3600,
    14400,
    43200,
    86400
  ],
  "timeout_seconds": 10,
  "headers": [
    "x-acme-tenant"
  ],
  "has_authorization": true,
  "metadata": {},
  "secret_version": 1,
  "old_secret_expires_at": null,
  "health": {
    "circuit": "closed",
    "consecutive_failures": 0,
    "last_success_at": null,
    "last_failure_at": null,
    "next_probe_at": null
  },
  "queued_count": 0,
  "url_revision": "1",
  "version": "1",
  "created_at": "2026-10-09T18:22:01Z",
  "updated_at": "2026-10-09T18:22:01Z",
  "signing_secret": "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSwq8uEPmRUQ0Y="
}

400The body isn't valid JSON, isn't an object, or has a duplicate key (invalid_json).

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Duplicate key \"name\" at byte 31."
  }
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

409The team or the tenant is at its endpoint limit (quota_exceeded). Also idempotency_conflict for a different request with the same Idempotency-Key, and request_in_progress while it's running.

{
  "error": {
    "code": "quota_exceeded",
    "message": "Your team has reached its limit of 100 endpoints for this tenant. Delete some, or contact support@motor.dev to raise the limit."
  }
}

413The body is larger than 1 MiB (body_too_large).

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 1048576 bytes."
  }
}

415The body isn't application/json (unsupported_media_type).

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json."
  }
}

422A field is missing, invalid, or unknown, the tenant isn't active, max_concurrency is over your cap, or Idempotency-Key is missing (invalid_field); or the URL's host is private or Motor's (unsafe_url). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "unsafe_url",
    "message": "`url` can't be a private, loopback, link-local, or reserved address.",
    "field": "url"
  }
}

429Too many configuration writes: 20 a second, bursts of 40 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

503Motor couldn't complete the request, such as sealing a secret or closing a billing month (temporarily_unavailable). Retry.

{
  "error": {
    "code": "temporarily_unavailable",
    "message": "Motor couldn't protect a secret. Retry in a few seconds."
  }
}

GET/webhooks/endpoints

List endpoints

Lists endpoints that aren't deleted, oldest first.

ParameterLocationDescription
tenant_idqueryOnly this tenant's endpoints.
statusqueryOnly endpoints with this status.
limitqueryFrom 1 to 200; 50 by default.
cursorquerynext_cursor from the previous page. It's bound to your team, this list and its filters; another is 422 invalid_cursor. Results are oldest first; records changed between pages may move or disappear.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

200A page of endpoints.

{
  "data": [
    {
      "id": "wde_6mNbVc8xZaS2dFgH4jKlQw",
      "tenant_id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
      "name": "Billing receiver",
      "description": "",
      "url": "https://hooks.acme.example/motor?team=7",
      "events": [
        "invoice.*"
      ],
      "filter": {
        "all": [
          {
            "path": "/amount",
            "op": "prefix",
            "value": "4"
          }
        ]
      },
      "transform": null,
      "transform_version": "1",
      "status": "enabled",
      "disabled_reason": null,
      "rate_per_second": 50,
      "max_concurrency": 3,
      "retry_delays_seconds": [
        15,
        60,
        300,
        900,
        3600,
        14400,
        43200,
        86400
      ],
      "timeout_seconds": 10,
      "headers": [
        "x-acme-tenant"
      ],
      "has_authorization": true,
      "metadata": {},
      "secret_version": 1,
      "old_secret_expires_at": null,
      "health": {
        "circuit": "closed",
        "consecutive_failures": 0,
        "last_success_at": null,
        "last_failure_at": null,
        "next_probe_at": null
      },
      "queued_count": 0,
      "url_revision": "1",
      "version": "1",
      "created_at": "2026-10-09T18:22:01Z",
      "updated_at": "2026-10-09T18:22:01Z"
    }
  ],
  "next_cursor": null
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

422A filter is invalid or names another team's tenant (invalid_field), or the cursor isn't for this list (invalid_cursor). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`status` must be enabled, paused, or disabled."
  }
}

429Too many reads: 100 a second, bursts of 200 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

GET/webhooks/endpoints/{id}

Get an endpoint

Returns an endpoint with its URL, health, and ETag. The signing secret, header values, and authorization are never shown.

ParameterLocationDescription
idpathThe endpoint's ID, wde_.... Another team's ID is 404.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

200The endpoint.

{
  "id": "wde_6mNbVc8xZaS2dFgH4jKlQw",
  "tenant_id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
  "name": "Billing receiver",
  "description": "",
  "url": "https://hooks.acme.example/motor?team=7",
  "events": [
    "invoice.*"
  ],
  "filter": {
    "all": [
      {
        "path": "/amount",
        "op": "prefix",
        "value": "4"
      }
    ]
  },
  "transform": null,
  "transform_version": "1",
  "status": "enabled",
  "disabled_reason": null,
  "rate_per_second": 50,
  "max_concurrency": 3,
  "retry_delays_seconds": [
    15,
    60,
    300,
    900,
    3600,
    14400,
    43200,
    86400
  ],
  "timeout_seconds": 10,
  "headers": [
    "x-acme-tenant"
  ],
  "has_authorization": true,
  "metadata": {},
  "secret_version": 1,
  "old_secret_expires_at": null,
  "health": {
    "circuit": "closed",
    "consecutive_failures": 0,
    "last_success_at": null,
    "last_failure_at": null,
    "next_probe_at": null
  },
  "queued_count": 0,
  "url_revision": "1",
  "version": "1",
  "created_at": "2026-10-09T18:22:01Z",
  "updated_at": "2026-10-09T18:22:01Z"
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The endpoint doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The endpoint doesn't exist."
  }
}

422 Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`Motor-Mode` must be live or test."
  }
}

429Too many reads: 100 a second, bursts of 200 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

PATCH/webhooks/endpoints/{id}

Update an endpoint

Changes the configuration; the tenant can't change. Omitted fields keep their values, null clears filter, transform, and authorization, and arrays and objects replace in full. A URL with a new host must come with authorization (or null) and headers (or {}), so that credentials never move to another host by omission. A new URL resets health. Every pending delivery, retries and replays included, goes to the current URL with the current credentials at its next attempt; queued bytes never change. A disabled endpoint stays disabled until /resume.

ParameterLocationDescription
idpathThe endpoint's ID, wde_.... Another team's ID is 404.
If-MatchheaderThe ETag of the endpoint from your last read, in quotes, such as "wd:wde_6mNbVc8xZaS2dFgH4jKlQw:3". Missing is 428 precondition_required; stale is 412 revision_conflict (the response carries the current ETag); malformed is 422 invalid_field.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Request body
{
  "url": "https://hooks.acme.example/v2/motor",
  "status": "paused"
}
FieldTypeDescription
urlstringHTTPS on port 443 to a public host name or IPv4 address, at most 2,048 bytes, without user information or a fragment. Private, loopback, Motor, and .local/.internal hosts are 422 unsafe_url. The query is kept exactly and treated as secret.
namestringWebhook by default.
descriptionstringAt most 4,096 bytes; empty by default.
eventsarray1 to 50 event type patterns: a name, prefix.*, or *. ["*"] by default. Patterns may name types you haven't created yet.
filternull (the default) matches every message of the event types.
transformnull (the default) sends the message's bytes unchanged.
statusstringenabled (the default) or paused. A paused endpoint queues deliveries while their deadlines run. A disabled endpoint needs /resume.
rate_per_secondinteger50 by default.
max_concurrencyinteger3 by default (or your team's cap, if lower), and at most 3: Motor runs at most 3 attempts of a team at once. 1 with a rate of 1 sends one at a time, though retries and replays can still reorder.
retry_delays_secondsarray0 to 8 delays of 1 to 86,400 seconds, adding up to at most 259,200. [15, 60, 300, 900, 3600, 14400, 43200, 86400] by default.
timeout_secondsinteger10 by default.
headersobjectCustom headers: at most 20 token names, values of at most 1,024 bytes without line breaks, 8 KiB in all. Motor's own and hop-by-hop headers (host, content-type, authorization, cookie, webhook-*, motor-*, x-forwarded-*, and others) aren't allowed. Values are write-only. {} by default.
authorizationstring or nullThe Authorization header to send, such as Bearer ...; write-only. null by default.
metadata
Responses

200The endpoint.

{
  "id": "wde_6mNbVc8xZaS2dFgH4jKlQw",
  "tenant_id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
  "name": "Billing receiver",
  "description": "",
  "url": "https://hooks.acme.example/v2/motor",
  "events": [
    "invoice.*"
  ],
  "filter": {
    "all": [
      {
        "path": "/amount",
        "op": "prefix",
        "value": "4"
      }
    ]
  },
  "transform": null,
  "transform_version": "1",
  "status": "paused",
  "disabled_reason": null,
  "rate_per_second": 50,
  "max_concurrency": 3,
  "retry_delays_seconds": [
    15,
    60,
    300,
    900,
    3600,
    14400,
    43200,
    86400
  ],
  "timeout_seconds": 10,
  "headers": [
    "x-acme-tenant"
  ],
  "has_authorization": true,
  "metadata": {},
  "secret_version": 1,
  "old_secret_expires_at": null,
  "health": {
    "circuit": "closed",
    "consecutive_failures": 0,
    "last_success_at": null,
    "last_failure_at": null,
    "next_probe_at": null
  },
  "queued_count": 0,
  "url_revision": "2",
  "version": "4",
  "created_at": "2026-10-09T18:22:01Z",
  "updated_at": "2026-10-09T18:22:03Z"
}

400The body isn't valid JSON, isn't an object, or has a duplicate key (invalid_json).

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Duplicate key \"name\" at byte 31."
  }
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The endpoint doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The endpoint doesn't exist."
  }
}

409status: enabled for an endpoint Motor disabled (invalid_state): use /resume.

{
  "error": {
    "code": "invalid_state",
    "message": "Motor disabled this endpoint. Call POST /endpoints/wde_6mNbVc8xZaS2dFgH4jKlQw/resume to enable it."
  }
}

412If-Match is stale (revision_conflict). The response's ETag is current: read the resource again and retry.

{
  "error": {
    "code": "revision_conflict",
    "message": "The resource changed since you read it. Read it again and retry with its new ETag.",
    "resource_id": "wde_6mNbVc8xZaS2dFgH4jKlQw"
  }
}

413The body is larger than 1 MiB (body_too_large).

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 1048576 bytes."
  }
}

415The body isn't application/json (unsupported_media_type).

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json."
  }
}

422A field is invalid or unknown, a new host lacks authorization and headers, or If-Match is malformed (invalid_field); or the URL is unsafe (unsafe_url). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`url` changes the host, so send authorization (or null) and headers (or {}) too. Motor doesn't move credentials to a new host.",
    "field": "url"
  }
}

428If-Match is missing (precondition_required).

{
  "error": {
    "code": "precondition_required",
    "message": "Send the ETag of the resource in the If-Match header. Read the resource to get its current ETag."
  }
}

429Too many configuration writes: 20 a second, bursts of 40 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

503Motor couldn't complete the request, such as sealing a secret or closing a billing month (temporarily_unavailable). Retry.

{
  "error": {
    "code": "temporarily_unavailable",
    "message": "Motor couldn't protect a secret. Retry in a few seconds."
  }
}

DELETE/webhooks/endpoints/{id}

Delete an endpoint

Deletes the endpoint: it gets no new messages, nothing more is claimed, and its unclaimed deliveries are canceled. A request already in flight can't be recalled.

ParameterLocationDescription
idpathThe endpoint's ID, wde_.... Another team's ID is 404.
If-MatchheaderThe ETag of the endpoint from your last read, in quotes, such as "wd:wde_6mNbVc8xZaS2dFgH4jKlQw:3". Missing is 428 precondition_required; stale is 412 revision_conflict (the response carries the current ETag); malformed is 422 invalid_field.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

204Deleted.

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The endpoint doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The endpoint doesn't exist."
  }
}

412If-Match is stale (revision_conflict). The response's ETag is current: read the resource again and retry.

{
  "error": {
    "code": "revision_conflict",
    "message": "The resource changed since you read it. Read it again and retry with its new ETag.",
    "resource_id": "wde_6mNbVc8xZaS2dFgH4jKlQw"
  }
}

422If-Match is malformed (invalid_field). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`If-Match` must be one strong ETag in quotes."
  }
}

428If-Match is missing (precondition_required).

{
  "error": {
    "code": "precondition_required",
    "message": "Send the ETag of the resource in the If-Match header. Read the resource to get its current ETag."
  }
}

429Too many configuration writes: 20 a second, bursts of 40 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

POST/webhooks/endpoints/{id}/rotate_secret

Rotate the signing secret

Makes a new signing secret and returns it once. During overlap_seconds (86,400 by default), deliveries carry a v1 signature for both secrets; 0 retires every old secret at once. Another nonzero rotation during an overlap is 409.

ParameterLocationDescription
idpathThe endpoint's ID, wde_.... Another team's ID is 404.
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. A retry with the same key and the same request (compared semantically, after defaults) returns the first response with Idempotency-Replayed: true for 35 days; a different request with the key is 409 idempotency_conflict, and a retry while the first is running is 409 request_in_progress with Retry-After: 1. A failed validation doesn't use the key. A missing key is 422 invalid_field.
If-MatchheaderThe ETag of the endpoint from your last read, in quotes, such as "wd:wde_6mNbVc8xZaS2dFgH4jKlQw:3". Missing is 428 precondition_required; stale is 412 revision_conflict (the response carries the current ETag); malformed is 422 invalid_field.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Request body
{
  "overlap_seconds": 3600
}
FieldTypeDescription
overlap_secondsintegerHow long the previous secret keeps signing, 0 to 86,400 seconds.
Responses

200The endpoint, with the new signing_secret. An idempotent replay shows it only to the key that rotated it, while it's current.

{
  "id": "wde_6mNbVc8xZaS2dFgH4jKlQw",
  "tenant_id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
  "name": "Billing receiver",
  "description": "",
  "url": "https://hooks.acme.example/motor?team=7",
  "events": [
    "invoice.*"
  ],
  "filter": {
    "all": [
      {
        "path": "/amount",
        "op": "prefix",
        "value": "4"
      }
    ]
  },
  "transform": null,
  "transform_version": "1",
  "status": "enabled",
  "disabled_reason": null,
  "rate_per_second": 50,
  "max_concurrency": 3,
  "retry_delays_seconds": [
    15,
    60,
    300,
    900,
    3600,
    14400,
    43200,
    86400
  ],
  "timeout_seconds": 10,
  "headers": [
    "x-acme-tenant"
  ],
  "has_authorization": true,
  "metadata": {},
  "secret_version": 2,
  "old_secret_expires_at": "2026-10-10T18:22:01Z",
  "health": {
    "circuit": "closed",
    "consecutive_failures": 0,
    "last_success_at": null,
    "last_failure_at": null,
    "next_probe_at": null
  },
  "queued_count": 0,
  "url_revision": "1",
  "version": "3",
  "created_at": "2026-10-09T18:22:01Z",
  "updated_at": "2026-10-09T18:22:03Z",
  "signing_secret": "whsec_c2VjcmV0LWtleS1mb3ItZXhhbXBsZXMtb25seS0xMjM="
}

400The body isn't valid JSON, isn't an object, or has a duplicate key (invalid_json).

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Duplicate key \"name\" at byte 31."
  }
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The endpoint doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The endpoint doesn't exist."
  }
}

409The previous secret is still in its overlap and overlap_seconds isn't 0 (invalid_state). Also idempotency_conflict for a different request with the same Idempotency-Key, and request_in_progress while it's running.

{
  "error": {
    "code": "invalid_state",
    "message": "The previous signing key is still valid until 2026-10-10T18:22:01Z. Wait for the overlap to end, or rotate with overlap_seconds: 0 to retire every old key at once."
  }
}

412If-Match is stale (revision_conflict). The response's ETag is current: read the resource again and retry.

{
  "error": {
    "code": "revision_conflict",
    "message": "The resource changed since you read it. Read it again and retry with its new ETag.",
    "resource_id": "wde_6mNbVc8xZaS2dFgH4jKlQw"
  }
}

413The body is larger than 1 MiB (body_too_large).

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 1048576 bytes."
  }
}

415The body isn't application/json (unsupported_media_type).

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json."
  }
}

422overlap_seconds is invalid, a field is unknown, or a header is missing or malformed (invalid_field). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`overlap_seconds` must be from 0 to 86400."
  }
}

428If-Match is missing (precondition_required).

{
  "error": {
    "code": "precondition_required",
    "message": "Send the ETag of the resource in the If-Match header. Read the resource to get its current ETag."
  }
}

429Too many configuration writes: 20 a second, bursts of 40 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

503Motor couldn't complete the request, such as sealing a secret or closing a billing month (temporarily_unavailable). Retry.

{
  "error": {
    "code": "temporarily_unavailable",
    "message": "Motor couldn't protect a secret. Retry in a few seconds."
  }
}

POST/webhooks/endpoints/{id}/resume

Resume an endpoint

Checks the URL again, enables the endpoint, resets its circuit to half-open and lets one probe go. Use it for an endpoint that Motor disabled or whose circuit is open. Finished deliveries stay finished: replay them if you still need them.

ParameterLocationDescription
idpathThe endpoint's ID, wde_.... Another team's ID is 404.
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. A retry with the same key and the same request (compared semantically, after defaults) returns the first response with Idempotency-Replayed: true for 35 days; a different request with the key is 409 idempotency_conflict, and a retry while the first is running is 409 request_in_progress with Retry-After: 1. A failed validation doesn't use the key. A missing key is 422 invalid_field.
If-MatchheaderThe ETag of the endpoint from your last read, in quotes, such as "wd:wde_6mNbVc8xZaS2dFgH4jKlQw:3". Missing is 428 precondition_required; stale is 412 revision_conflict (the response carries the current ETag); malformed is 422 invalid_field.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Request body
{}
Responses

200The endpoint. An idempotent replay returns only {"id", "status"}.

{
  "id": "wde_6mNbVc8xZaS2dFgH4jKlQw",
  "tenant_id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
  "name": "Billing receiver",
  "description": "",
  "url": "https://hooks.acme.example/motor?team=7",
  "events": [
    "invoice.*"
  ],
  "filter": {
    "all": [
      {
        "path": "/amount",
        "op": "prefix",
        "value": "4"
      }
    ]
  },
  "transform": null,
  "transform_version": "1",
  "status": "enabled",
  "disabled_reason": null,
  "rate_per_second": 50,
  "max_concurrency": 3,
  "retry_delays_seconds": [
    15,
    60,
    300,
    900,
    3600,
    14400,
    43200,
    86400
  ],
  "timeout_seconds": 10,
  "headers": [
    "x-acme-tenant"
  ],
  "has_authorization": true,
  "metadata": {},
  "secret_version": 1,
  "old_secret_expires_at": null,
  "health": {
    "circuit": "half_open",
    "consecutive_failures": 0,
    "last_success_at": null,
    "last_failure_at": null,
    "next_probe_at": null
  },
  "queued_count": 0,
  "url_revision": "1",
  "version": "5",
  "created_at": "2026-10-09T18:22:01Z",
  "updated_at": "2026-10-09T18:22:03Z"
}

400The body isn't valid JSON, isn't an object, or has a duplicate key (invalid_json).

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Duplicate key \"name\" at byte 31."
  }
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The endpoint doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The endpoint doesn't exist."
  }
}

409Also idempotency_conflict for a different request with the same Idempotency-Key, and request_in_progress while it's running.

{
  "error": {
    "code": "idempotency_conflict",
    "message": "This Idempotency-Key was used for a different request. Use a new key for a new operation."
  }
}

412If-Match is stale (revision_conflict). The response's ETag is current: read the resource again and retry.

{
  "error": {
    "code": "revision_conflict",
    "message": "The resource changed since you read it. Read it again and retry with its new ETag.",
    "resource_id": "wde_6mNbVc8xZaS2dFgH4jKlQw"
  }
}

413The body is larger than 1 MiB (body_too_large).

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 1048576 bytes."
  }
}

415The body isn't application/json (unsupported_media_type).

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json."
  }
}

422The URL is no longer safe (unsafe_url), the body has a field, or a header is missing or malformed (invalid_field). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "unsafe_url",
    "message": "`url` can't be a private or Motor host.",
    "field": "url"
  }
}

428If-Match is missing (precondition_required).

{
  "error": {
    "code": "precondition_required",
    "message": "Send the ETag of the resource in the If-Match header. Read the resource to get its current ETag."
  }
}

429Too many configuration writes: 20 a second, bursts of 40 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

503Motor couldn't complete the request, such as sealing a secret or closing a billing month (temporarily_unavailable). Retry.

{
  "error": {
    "code": "temporarily_unavailable",
    "message": "Motor couldn't protect a secret. Retry in a few seconds."
  }
}

POST/webhooks/endpoints/{id}/test

Send a test message

Sends a motor.test message to this endpoint's real receiver only. It skips the endpoint's event types and filter, but uses its authentication, signing, retries, and billing: it costs ingestion ($0.00001 per 64 KiB, at least 1 unit) plus delivery ($0.00005 per 16 KiB, at least 1 unit). Paused and disabled endpoints are refused; a test never resumes them.

ParameterLocationDescription
idpathThe endpoint's ID, wde_.... Another team's ID is 404.
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. A retry with the same key and the same request (compared semantically, after defaults) returns the first response with Idempotency-Replayed: true for 35 days; a different request with the key is 409 idempotency_conflict, and a retry while the first is running is 409 request_in_progress with Retry-After: 1. A failed validation doesn't use the key. A missing key is 422 invalid_field.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Request body
{
  "payload": {
    "test": true
  }
}
FieldTypeDescription
payloadAny JSON to send, exactly as written. {"test":true} by default.
Responses

202The accepted test message.

{
  "id": "wdm_2xCvBn6mQwE9rTyUi4OpAs",
  "tenant_id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
  "event_type": "motor.test",
  "external_id": null,
  "origin": "test",
  "source_id": null,
  "status": "queued",
  "body_bytes": 11,
  "body_sha256": "7bc2e5c2c4b0f0b8d1b9a9e1c3d2f4a6b8c0e2f4a6b8c0d2e4f6a8b0c2d4e6f8",
  "content_type": "application/json",
  "deliver_at": "2026-10-09T18:22:01Z",
  "expires_at": "2026-10-12T18:22:01Z",
  "retained_until": "2026-11-08T18:22:01Z",
  "content_available": true,
  "delivery_counts": {
    "scheduled": 0,
    "queued": 1,
    "in_flight": 0,
    "retry_wait": 0,
    "succeeded": 0,
    "failed": 0,
    "expired": 0,
    "canceled": 0
  },
  "price": {
    "ingest_units": 1,
    "delivery_units": 1,
    "total_usd": "0.00006",
    "rate_version": "2026-10-08"
  },
  "redacted": false,
  "redaction_status": null,
  "redacted_at": null,
  "metadata": {},
  "created_at": "2026-10-09T18:22:01Z"
}

400The body isn't valid JSON, isn't an object, or has a duplicate key (invalid_json).

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Duplicate key \"name\" at byte 31."
  }
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

402No payment card is on the account (billing_required), or the charge would exceed the paying key's monthly spending limit (spending_limit).

{
  "error": {
    "code": "spending_limit",
    "message": "This work would exceed the paying API key's monthly spending limit. Raise the limit on the API keys page, or wait until the next billing month starts at 00:00 UTC."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The endpoint doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The endpoint doesn't exist."
  }
}

409The endpoint is paused or disabled (invalid_state). Also idempotency_conflict for a different request with the same Idempotency-Key, and request_in_progress while it's running.

{
  "error": {
    "code": "invalid_state",
    "message": "The endpoint is paused. Enable it before you send a test message."
  }
}

413The body is larger than 1 MiB (body_too_large).

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 1048576 bytes."
  }
}

415The body isn't application/json (unsupported_media_type).

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json."
  }
}

422The payload or a header is invalid, or a field is unknown (invalid_field). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`payload` isn't valid JSON."
  }
}

429Too many admissions: 100 a second by default, bursts of twice that (rate_limited), or the team's pending deliveries or stored content is at its limit (queue_full).

{
  "error": {
    "code": "queue_full",
    "message": "Your team has 100000 deliveries waiting, and its limit is 100000. Retry later."
  }
}

503Motor couldn't complete the request, such as sealing a secret or closing a billing month (temporarily_unavailable). Retry.

{
  "error": {
    "code": "temporarily_unavailable",
    "message": "Motor couldn't protect a secret. Retry in a few seconds."
  }
}

Messages

POST/webhooks/messages

Send a message

Accepts a message for one tenant and fans it out to the tenant's matching enabled and paused endpoints (at most 100, with at most 8 MiB of transformed bodies in all). Which endpoints receive it, and the bytes each one gets, are fixed now; endpoints created later receive nothing. payload is delivered as the exact bytes you sent, with insignificant whitespace removed: key order and number spellings stay. Admission is atomic: a transform failure or a limit rejects the whole message. Pricing: webhooks.ingest_64k is $0.00001 for each 64 KiB of the body, at least 1 unit, charged even with no matching endpoint; webhooks.delivery_16k is $0.00005 for each 16 KiB of each endpoint's body, at least 1 unit. Up to 9 attempts are included; retries are free. Nothing is refunded once accepted.

ParameterLocationDescription
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. A retry with the same key and the same request (compared semantically, after defaults) returns the first response with Idempotency-Replayed: true for 35 days; a different request with the key is 409 idempotency_conflict, and a retry while the first is running is 409 request_in_progress with Retry-After: 1. A failed validation doesn't use the key. A missing key is 422 invalid_field.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Request body
{
  "tenant_id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
  "event_type": "invoice.paid",
  "payload": {
    "invoice_id": "inv_42",
    "amount": "49.00"
  },
  "external_id": "invoice-paid-42",
  "metadata": {
    "order": "42"
  }
}
FieldTypeDescription
tenant_idrequiredstringAn active tenant.
event_typerequiredstringAn active event type of your catalog. Otherwise 422 invalid_event_type.
payloadAny JSON, null included. Exactly one of payload and body_base64. At most 1 MiB.
body_base64stringRaw bytes in standard padded Base64, at most 1 MiB decoded. Needs content_type.
content_typestringapplication/json (the default for payload, and the only type allowed with it), text/plain, application/octet-stream, or application/x-www-form-urlencoded. Textual types may add ; charset=utf-8.
endpoint_idsarray or nullRestricts delivery to these endpoints of the tenant; filters still apply. A disabled, deleted, or foreign endpoint rejects the request. null (the default) selects every matching endpoint.
external_idstringYour ID for the message. Not deduplicated.
deliver_atstring or nullWhen to deliver: now by default, at most 7 days ahead.
expires_atstring or nullNo attempt starts at or after it. After deliver_at and at most 72 hours later, which is the default.
metadata
Responses

202The accepted message with its price. Delivery happens afterwards; its failures appear on the deliveries.

{
  "id": "wdm_2xCvBn6mQwE9rTyUi4OpAs",
  "tenant_id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
  "event_type": "invoice.paid",
  "external_id": "invoice-paid-42",
  "origin": "api",
  "source_id": null,
  "status": "queued",
  "body_bytes": 41,
  "body_sha256": "3f0a2b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a",
  "content_type": "application/json",
  "deliver_at": "2026-10-09T18:22:01Z",
  "expires_at": "2026-10-12T18:22:01Z",
  "retained_until": "2026-11-08T18:22:01Z",
  "content_available": true,
  "delivery_counts": {
    "scheduled": 0,
    "queued": 2,
    "in_flight": 0,
    "retry_wait": 0,
    "succeeded": 0,
    "failed": 0,
    "expired": 0,
    "canceled": 0
  },
  "price": {
    "ingest_units": 1,
    "delivery_units": 2,
    "total_usd": "0.00011",
    "rate_version": "2026-10-08"
  },
  "redacted": false,
  "redaction_status": null,
  "redacted_at": null,
  "metadata": {
    "order": "42"
  },
  "created_at": "2026-10-09T18:22:01Z"
}

400The body isn't valid JSON, isn't an object, or has a duplicate key (invalid_json).

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Duplicate key \"name\" at byte 31."
  }
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

402No payment card is on the account (billing_required), or the charge would exceed the paying key's monthly spending limit (spending_limit).

{
  "error": {
    "code": "spending_limit",
    "message": "This work would exceed the paying API key's monthly spending limit. Raise the limit on the API keys page, or wait until the next billing month starts at 00:00 UTC."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

409Also idempotency_conflict for a different request with the same Idempotency-Key, and request_in_progress while it's running.

{
  "error": {
    "code": "idempotency_conflict",
    "message": "This Idempotency-Key was used for a different request. Use a new key for a new operation."
  }
}

413The body, the payload, or the decoded body_base64 is larger than 1 MiB (body_too_large).

{
  "error": {
    "code": "body_too_large",
    "message": "The payload must be at most 1048576 bytes."
  }
}

415The body isn't application/json (unsupported_media_type).

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json."
  }
}

422The event type isn't active (invalid_event_type); an endpoint's transform failed (transform_failed, with resource_id); or a field is invalid, the message matches more than 100 endpoints or 8 MiB, or Idempotency-Key is missing (invalid_field). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_event_type",
    "message": "The event type invoice.payed isn't an active type of your catalog. Create it with POST /webhooks/event_types first.",
    "field": "event_type"
  }
}

429Too many admissions: 100 a second by default, bursts of twice that (rate_limited), or the team's pending deliveries or stored content is at its limit (queue_full).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

503Motor couldn't store the message, or the tenant's configuration kept changing (temporarily_unavailable). Retry with the same key.

{
  "error": {
    "code": "temporarily_unavailable",
    "message": "Motor couldn't store the message. Retry in a few seconds."
  }
}

GET/webhooks/messages

List messages

Lists messages created in a window of at most 30 days, oldest first, without their bodies. The status filter applies within each page, so a page may have fewer items than limit while next_cursor is set.

ParameterLocationDescription
created_afterqueryInclusive RFC 3339 time. By default, 30 days before created_before. Lists messages created in a window of at most 30 days.
created_beforequeryExclusive RFC 3339 time, after created_after and at most 30 days later. By default, now.
tenant_idqueryOnly this tenant's messages.
event_typequeryOnly messages of this type.
source_idqueryOnly messages that this source captured.
statusqueryOnly messages with this status.
external_idqueryOnly messages with this external ID.
limitqueryFrom 1 to 200; 50 by default.
cursorquerynext_cursor from the previous page. It's bound to your team, this list and its filters; another is 422 invalid_cursor. Results are oldest first; records changed between pages may move or disappear.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

200A page of messages.

{
  "data": [
    {
      "id": "wdm_2xCvBn6mQwE9rTyUi4OpAs",
      "tenant_id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
      "event_type": "invoice.paid",
      "external_id": "invoice-paid-42",
      "origin": "api",
      "source_id": null,
      "status": "queued",
      "body_bytes": 41,
      "body_sha256": "3f0a2b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a",
      "content_type": "application/json",
      "deliver_at": "2026-10-09T18:22:01Z",
      "expires_at": "2026-10-12T18:22:01Z",
      "retained_until": "2026-11-08T18:22:01Z",
      "content_available": true,
      "delivery_counts": {
        "scheduled": 0,
        "queued": 2,
        "in_flight": 0,
        "retry_wait": 0,
        "succeeded": 0,
        "failed": 0,
        "expired": 0,
        "canceled": 0
      },
      "price": {
        "ingest_units": 1,
        "delivery_units": 2,
        "total_usd": "0.00011",
        "rate_version": "2026-10-08"
      },
      "redacted": false,
      "redaction_status": null,
      "redacted_at": null,
      "metadata": {
        "order": "42"
      },
      "created_at": "2026-10-09T18:22:01Z"
    }
  ],
  "next_cursor": "eyJ2IjoxLCJzIjoiOGYzYSIsImEiOiIxNzkxNTY5NzIxMDAwMDAwIDQyIn0.Zm9vYmFyYmF6"
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

422A filter or time is invalid, the window is longer than 30 days (invalid_field), or the cursor isn't for this list (invalid_cursor). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`created_before` must be at most 30 days after created_after."
  }
}

429Too many reads: 100 a second, bursts of 200 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

GET/webhooks/messages/{id}

Get a message

Returns a message's metadata, status, counts, and price.

ParameterLocationDescription
idpathThe message's ID, wdm_.... Another team's ID is 404.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

200The message.

{
  "id": "wdm_2xCvBn6mQwE9rTyUi4OpAs",
  "tenant_id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
  "event_type": "invoice.paid",
  "external_id": "invoice-paid-42",
  "origin": "api",
  "source_id": null,
  "status": "queued",
  "body_bytes": 41,
  "body_sha256": "3f0a2b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a",
  "content_type": "application/json",
  "deliver_at": "2026-10-09T18:22:01Z",
  "expires_at": "2026-10-12T18:22:01Z",
  "retained_until": "2026-11-08T18:22:01Z",
  "content_available": true,
  "delivery_counts": {
    "scheduled": 0,
    "queued": 2,
    "in_flight": 0,
    "retry_wait": 0,
    "succeeded": 0,
    "failed": 0,
    "expired": 0,
    "canceled": 0
  },
  "price": {
    "ingest_units": 1,
    "delivery_units": 2,
    "total_usd": "0.00011",
    "rate_version": "2026-10-08"
  },
  "redacted": false,
  "redaction_status": null,
  "redacted_at": null,
  "metadata": {
    "order": "42"
  },
  "created_at": "2026-10-09T18:22:01Z"
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The message doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The message doesn't exist."
  }
}

422 Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`Motor-Mode` must be live or test."
  }
}

429Too many reads: 100 a second, bursts of 200 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

GET/webhooks/messages/{id}/content

Get a message's body

Returns the original accepted bytes with their content type, until the message is redacted or passes retained_until.

ParameterLocationDescription
idpathThe message's ID, wdm_.... Another team's ID is 404.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

200The body, with the message's Content-Type.

{
  "invoice_id": "inv_42",
  "amount": "49.00"
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The message doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The message doesn't exist."
  }
}

410The body was redacted or passed its retention (payload_expired). The metadata stays.

{
  "error": {
    "code": "payload_expired",
    "message": "The message's content passed its retention and is gone. Its metadata stays."
  }
}

422 Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`Motor-Mode` must be live or test."
  }
}

429Too many reads: 100 a second, bursts of 200 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

503Motor couldn't read the body (temporarily_unavailable). Retry.

{
  "error": {
    "code": "temporarily_unavailable",
    "message": "Motor couldn't read the content. Retry in a few seconds."
  }
}

POST/webhooks/messages/{id}/cancel

Cancel a message

Cancels the message's original deliveries that no attempt has claimed. Attempts in flight finish, but their retries are canceled. Replays aren't affected. Repeating it has no new effect. Nothing is refunded.

ParameterLocationDescription
idpathThe message's ID, wdm_.... Another team's ID is 404.
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. A retry with the same key and the same request (compared semantically, after defaults) returns the first response with Idempotency-Replayed: true for 35 days; a different request with the key is 409 idempotency_conflict, and a retry while the first is running is 409 request_in_progress with Retry-After: 1. A failed validation doesn't use the key. A missing key is 422 invalid_field.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Request body
{}
Responses

200What the cancellation did.

{
  "message_id": "wdm_2xCvBn6mQwE9rTyUi4OpAs",
  "canceled_count": 1,
  "in_flight_count": 1,
  "already_terminal_count": 0
}

400The body isn't valid JSON, isn't an object, or has a duplicate key (invalid_json).

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Duplicate key \"name\" at byte 31."
  }
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The message doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The message doesn't exist."
  }
}

409Also idempotency_conflict for a different request with the same Idempotency-Key, and request_in_progress while it's running.

{
  "error": {
    "code": "idempotency_conflict",
    "message": "This Idempotency-Key was used for a different request. Use a new key for a new operation."
  }
}

413The body is larger than 1 MiB (body_too_large).

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 1048576 bytes."
  }
}

415The body isn't application/json (unsupported_media_type).

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json."
  }
}

422The body has a field, or Idempotency-Key is missing (invalid_field). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`Idempotency-Key` is required for this request."
  }
}

429Too many configuration writes: 20 a second, bursts of 40 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

POST/webhooks/messages/{id}/redact

Redact a message

Erases the message's body and every transformed copy irreversibly, and cancels all its pending deliveries, replays included. Reads and replays of the content stop at once; the stored bytes go within 24 hours, and the message's redaction_status becomes complete. Metadata and charges stay.

ParameterLocationDescription
idpathThe message's ID, wdm_.... Another team's ID is 404.
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. A retry with the same key and the same request (compared semantically, after defaults) returns the first response with Idempotency-Replayed: true for 35 days; a different request with the key is 409 idempotency_conflict, and a retry while the first is running is 409 request_in_progress with Retry-After: 1. A failed validation doesn't use the key. A missing key is 422 invalid_field.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Request body
{}
Responses

202Redaction started.

{
  "message_id": "wdm_2xCvBn6mQwE9rTyUi4OpAs",
  "status": "redacting",
  "redacted_at": "2026-10-09T18:22:03Z"
}

400The body isn't valid JSON, isn't an object, or has a duplicate key (invalid_json).

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Duplicate key \"name\" at byte 31."
  }
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The message doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The message doesn't exist."
  }
}

409Also idempotency_conflict for a different request with the same Idempotency-Key, and request_in_progress while it's running.

{
  "error": {
    "code": "idempotency_conflict",
    "message": "This Idempotency-Key was used for a different request. Use a new key for a new operation."
  }
}

413The body is larger than 1 MiB (body_too_large).

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 1048576 bytes."
  }
}

415The body isn't application/json (unsupported_media_type).

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json."
  }
}

422The body has a field, or Idempotency-Key is missing (invalid_field). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`Idempotency-Key` is required for this request."
  }
}

429Too many configuration writes: 20 a second, bursts of 40 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

POST/webhooks/previews

Preview an endpoint

Shows whether an endpoint's current filter matches a body and what its transform makes of it. Nothing is stored, sent, or charged, and no Idempotency-Key is needed. It doesn't check event types or the endpoint's status.

ParameterLocationDescription
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Request body
{
  "endpoint_id": "wde_6mNbVc8xZaS2dFgH4jKlQw",
  "payload": {
    "invoice_id": "inv_42",
    "amount": "49.00",
    "internal": true
  }
}
FieldTypeDescription
endpoint_idrequiredstringAn endpoint of your team that isn't deleted.
payloadAny JSON. Exactly one of payload and body_base64.
body_base64stringRaw bytes in standard padded Base64. Needs content_type.
content_typestringAs for POST /messages.
Responses

200The preview.

{
  "matches": true,
  "body_base64": "eyJhbW91bnQiOiI0OS4wMCIsImludm9pY2VfaWQiOiJpbnZfNDIifQ==",
  "content_type": "application/json",
  "body_bytes": 40,
  "delivery_units": 1,
  "transform_version": "2",
  "errors": []
}

400The body isn't valid JSON, isn't an object, or has a duplicate key (invalid_json).

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Duplicate key \"name\" at byte 31."
  }
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

413The body is larger than 1 MiB (body_too_large).

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 1048576 bytes."
  }
}

415The body isn't application/json (unsupported_media_type).

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json."
  }
}

422endpoint_id isn't an endpoint of your team, or the payload is invalid (invalid_field). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`endpoint_id` must be the ID of an endpoint of your team."
  }
}

429Too many configuration writes: 20 a second, bursts of 40 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

Deliveries

GET/webhooks/deliveries

List deliveries

Lists deliveries created in a window of at most 30 days, oldest first. With message_id, the window is ignored. status=failed and status=expired list the dead letters.

ParameterLocationDescription
created_afterqueryInclusive RFC 3339 time. By default, 30 days before created_before. Lists deliveries created in a window of at most 30 days.
created_beforequeryExclusive RFC 3339 time, after created_after and at most 30 days later. By default, now.
tenant_idqueryOnly this tenant's deliveries.
endpoint_idqueryOnly this endpoint's deliveries.
message_idqueryOnly this message's deliveries, every generation.
statusqueryOnly deliveries with this status.
limitqueryFrom 1 to 200; 50 by default.
cursorquerynext_cursor from the previous page. It's bound to your team, this list and its filters; another is 422 invalid_cursor. Results are oldest first; records changed between pages may move or disappear.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

200A page of deliveries.

{
  "data": [
    {
      "id": "wdd_5gHjKl8ZxCv3BnMq7WeRtY",
      "message_id": "wdm_2xCvBn6mQwE9rTyUi4OpAs",
      "tenant_id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
      "endpoint_id": "wde_6mNbVc8xZaS2dFgH4jKlQw",
      "generation": 0,
      "replay_of": null,
      "replay_id": null,
      "transform_version": "1",
      "configuration": "original",
      "status": "failed",
      "blocked_reason": null,
      "attempt_count": 9,
      "next_attempt_at": null,
      "expires_at": "2026-10-12T18:22:01Z",
      "completed_at": "2026-10-12T11:40:09Z",
      "reason": "http_500",
      "body_bytes": 41,
      "body_sha256": "3f0a2b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a",
      "content_type": "application/json",
      "rate_version": "2026-10-08",
      "delivery_units": 1,
      "price_usd": "0.00005",
      "cancel_requested": false,
      "created_at": "2026-10-09T18:22:01Z",
      "updated_at": "2026-10-12T11:40:09Z"
    }
  ],
  "next_cursor": null
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

422A filter or time is invalid or names another team's resource, the window is longer than 30 days (invalid_field), or the cursor isn't for this list (invalid_cursor). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`status` must be scheduled, queued, in_flight, retry_wait, succeeded, failed, expired, or canceled."
  }
}

429Too many reads: 100 a second, bursts of 200 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

GET/webhooks/deliveries/{id}

Get a delivery

Returns a delivery.

ParameterLocationDescription
idpathThe delivery's ID, wdd_.... Another team's ID is 404.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

200The delivery.

{
  "id": "wdd_5gHjKl8ZxCv3BnMq7WeRtY",
  "message_id": "wdm_2xCvBn6mQwE9rTyUi4OpAs",
  "tenant_id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
  "endpoint_id": "wde_6mNbVc8xZaS2dFgH4jKlQw",
  "generation": 0,
  "replay_of": null,
  "replay_id": null,
  "transform_version": "1",
  "configuration": "original",
  "status": "retry_wait",
  "blocked_reason": null,
  "attempt_count": 2,
  "next_attempt_at": "2026-10-09T18:28:16Z",
  "expires_at": "2026-10-12T18:22:01Z",
  "completed_at": null,
  "reason": null,
  "body_bytes": 41,
  "body_sha256": "3f0a2b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a",
  "content_type": "application/json",
  "rate_version": "2026-10-08",
  "delivery_units": 1,
  "price_usd": "0.00005",
  "cancel_requested": false,
  "created_at": "2026-10-09T18:22:01Z",
  "updated_at": "2026-10-09T18:23:16Z"
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The delivery doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The delivery doesn't exist."
  }
}

422 Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`Motor-Mode` must be live or test."
  }
}

429Too many reads: 100 a second, bursts of 200 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

GET/webhooks/deliveries/{id}/content

Get a delivery's body

Returns the exact outbound bytes of the delivery, with its content type: the message's bytes, or its transformed bytes.

ParameterLocationDescription
idpathThe delivery's ID, wdd_.... Another team's ID is 404.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

200The body, with the delivery's Content-Type.

{
  "invoice_id": "inv_42",
  "amount": "49.00"
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The delivery doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The delivery doesn't exist."
  }
}

410The message was redacted or its body passed its retention (payload_expired).

{
  "error": {
    "code": "payload_expired",
    "message": "The message was redacted. Its content is gone."
  }
}

422 Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`Motor-Mode` must be live or test."
  }
}

429Too many reads: 100 a second, bursts of 200 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

503Motor couldn't read the body (temporarily_unavailable). Retry.

{
  "error": {
    "code": "temporarily_unavailable",
    "message": "Motor couldn't read the content. Retry in a few seconds."
  }
}

GET/webhooks/deliveries/{id}/attempts

List a delivery's attempts

Lists the delivery's attempts, at most 9, in order. There is one page: next_cursor is always null.

ParameterLocationDescription
idpathThe delivery's ID, wdd_.... Another team's ID is 404.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

200The attempts.

{
  "data": [
    {
      "id": "wda_1qAzWs4XeDc7RfVtGb9YhN",
      "delivery_id": "wdd_5gHjKl8ZxCv3BnMq7WeRtY",
      "number": 2,
      "claim_generation": 2,
      "started_at": "2026-10-09T18:23:16Z",
      "finished_at": "2026-10-09T18:23:16Z",
      "duration_ms": 412,
      "http_status": 503,
      "result": "retryable",
      "error_code": null,
      "response_headers": {
        "content-type": "text/plain",
        "retry-after": "60"
      },
      "response_body_base64": null,
      "response_truncated": false,
      "signature_timestamp": 1791569996,
      "destination_host": "hooks.acme.example",
      "endpoint_version": "1",
      "url_revision": "1",
      "probe": false
    }
  ],
  "next_cursor": null
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The delivery doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The delivery doesn't exist."
  }
}

422 Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`Motor-Mode` must be live or test."
  }
}

429Too many reads: 100 a second, bursts of 200 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

GET/webhooks/attempts/{id}

Get an attempt

Returns an attempt with its timing and the stored part of the response.

ParameterLocationDescription
idpathThe attempt's ID, wda_.... Another team's ID is 404.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

200The attempt.

{
  "id": "wda_1qAzWs4XeDc7RfVtGb9YhN",
  "delivery_id": "wdd_5gHjKl8ZxCv3BnMq7WeRtY",
  "number": 2,
  "claim_generation": 2,
  "started_at": "2026-10-09T18:23:16Z",
  "finished_at": "2026-10-09T18:23:16Z",
  "duration_ms": 412,
  "http_status": 503,
  "result": "retryable",
  "error_code": null,
  "response_headers": {
    "content-type": "text/plain",
    "retry-after": "60"
  },
  "response_body_base64": null,
  "response_truncated": false,
  "signature_timestamp": 1791569996,
  "destination_host": "hooks.acme.example",
  "endpoint_version": "1",
  "url_revision": "1",
  "probe": false
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The attempt doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The attempt doesn't exist."
  }
}

422 Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`Motor-Mode` must be live or test."
  }
}

429Too many reads: 100 a second, bursts of 200 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

POST/webhooks/deliveries/{id}/cancel

Cancel a delivery

Stops further attempts. A waiting delivery becomes canceled; one in flight finishes, and in_flight is true because only its retries are stopped. Canceling a canceled delivery changes nothing. Nothing is refunded.

ParameterLocationDescription
idpathThe delivery's ID, wdd_.... Another team's ID is 404.
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. A retry with the same key and the same request (compared semantically, after defaults) returns the first response with Idempotency-Replayed: true for 35 days; a different request with the key is 409 idempotency_conflict, and a retry while the first is running is 409 request_in_progress with Retry-After: 1. A failed validation doesn't use the key. A missing key is 422 invalid_field.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Request body
{}
Responses

200The delivery, with in_flight.

{
  "id": "wdd_5gHjKl8ZxCv3BnMq7WeRtY",
  "message_id": "wdm_2xCvBn6mQwE9rTyUi4OpAs",
  "tenant_id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
  "endpoint_id": "wde_6mNbVc8xZaS2dFgH4jKlQw",
  "generation": 0,
  "replay_of": null,
  "replay_id": null,
  "transform_version": "1",
  "configuration": "original",
  "status": "canceled",
  "blocked_reason": null,
  "attempt_count": 2,
  "next_attempt_at": null,
  "expires_at": "2026-10-12T18:22:01Z",
  "completed_at": "2026-10-09T18:24:30Z",
  "reason": "canceled",
  "body_bytes": 41,
  "body_sha256": "3f0a2b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a",
  "content_type": "application/json",
  "rate_version": "2026-10-08",
  "delivery_units": 1,
  "price_usd": "0.00005",
  "cancel_requested": true,
  "created_at": "2026-10-09T18:22:01Z",
  "updated_at": "2026-10-09T18:24:30Z",
  "in_flight": false
}

400The body isn't valid JSON, isn't an object, or has a duplicate key (invalid_json).

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Duplicate key \"name\" at byte 31."
  }
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The delivery doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The delivery doesn't exist."
  }
}

409The delivery already succeeded, failed, or expired (invalid_state). Also idempotency_conflict for a different request with the same Idempotency-Key, and request_in_progress while it's running.

{
  "error": {
    "code": "invalid_state",
    "message": "The delivery is already succeeded. Only waiting deliveries can be canceled."
  }
}

413The body is larger than 1 MiB (body_too_large).

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 1048576 bytes."
  }
}

415The body isn't application/json (unsupported_media_type).

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json."
  }
}

422The body has a field, or Idempotency-Key is missing (invalid_field). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`Idempotency-Key` is required for this request."
  }
}

429Too many configuration writes: 20 a second, bursts of 40 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

POST/webhooks/deliveries/{id}/replay

Replay a delivery

Creates a new generation of a finished delivery, with the same message ID (webhook-id) and a new delivery ID, sent to the endpoint's current URL with its current credentials and a new 72-hour deadline. original resends the frozen bytes; current applies the endpoint's current transform to the message and skips its filter. The endpoint must be enabled or paused, and the body retained and not redacted. Pricing: a replay is a new paid delivery generation, $0.00005 for each 16 KiB of its body, at least 1 unit; no new ingestion. Its retries are free.

ParameterLocationDescription
idpathThe delivery's ID, wdd_.... Another team's ID is 404.
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. A retry with the same key and the same request (compared semantically, after defaults) returns the first response with Idempotency-Replayed: true for 35 days; a different request with the key is 409 idempotency_conflict, and a retry while the first is running is 409 request_in_progress with Retry-After: 1. A failed validation doesn't use the key. A missing key is 422 invalid_field.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Request body
{
  "configuration": "original"
}
FieldTypeDescription
configurationstringoriginal (the default) or current.
Responses

202The new delivery.

{
  "id": "wdd_7uIoPa2SdF5gHjKl9ZxCvB",
  "message_id": "wdm_2xCvBn6mQwE9rTyUi4OpAs",
  "tenant_id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
  "endpoint_id": "wde_6mNbVc8xZaS2dFgH4jKlQw",
  "generation": 1,
  "replay_of": "wdd_5gHjKl8ZxCv3BnMq7WeRtY",
  "replay_id": null,
  "transform_version": "1",
  "configuration": "original",
  "status": "queued",
  "blocked_reason": null,
  "attempt_count": 0,
  "next_attempt_at": "2026-10-13T09:00:00Z",
  "expires_at": "2026-10-16T09:00:00Z",
  "completed_at": null,
  "reason": null,
  "body_bytes": 41,
  "body_sha256": "3f0a2b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a",
  "content_type": "application/json",
  "rate_version": "2026-10-08",
  "delivery_units": 1,
  "price_usd": "0.00005",
  "cancel_requested": false,
  "created_at": "2026-10-13T09:00:00Z",
  "updated_at": "2026-10-13T09:00:00Z"
}

400The body isn't valid JSON, isn't an object, or has a duplicate key (invalid_json).

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Duplicate key \"name\" at byte 31."
  }
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

402No payment card is on the account (billing_required), or the charge would exceed the paying key's monthly spending limit (spending_limit).

{
  "error": {
    "code": "spending_limit",
    "message": "This work would exceed the paying API key's monthly spending limit. Raise the limit on the API keys page, or wait until the next billing month starts at 00:00 UTC."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The delivery doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The delivery doesn't exist."
  }
}

409The delivery hasn't finished, or its tenant is deleted or its endpoint disabled or deleted (invalid_state). Also idempotency_conflict for a different request with the same Idempotency-Key, and request_in_progress while it's running.

{
  "error": {
    "code": "invalid_state",
    "message": "The delivery is retry_wait. Only a finished delivery can be replayed."
  }
}

410The message was redacted or its body passed its retention (payload_expired).

{
  "error": {
    "code": "payload_expired",
    "message": "The message's content passed its retention."
  }
}

413The body is larger than 1 MiB (body_too_large).

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 1048576 bytes."
  }
}

415The body isn't application/json (unsupported_media_type).

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json."
  }
}

422The current transform failed (transform_failed), or a field is invalid (invalid_field). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "transform_failed",
    "message": "The endpoint's current transform failed: /customer/id is required."
  }
}

429Too many admissions (rate_limited), or the team's pending deliveries or stored content is at its limit (queue_full).

{
  "error": {
    "code": "queue_full",
    "message": "Your team has too many deliveries waiting. Retry later."
  }
}

503Motor couldn't complete the request, such as sealing a secret or closing a billing month (temporarily_unavailable). Retry.

{
  "error": {
    "code": "temporarily_unavailable",
    "message": "Motor couldn't protect a secret. Retry in a few seconds."
  }
}

POST/webhooks/deliveries/{id}/retry_now

Retry a delivery now

Makes a delivery that waits to retry due now, within its existing attempts. A receiver's Retry-After for the same URL still holds, and pacing, concurrency, and an open circuit still apply. It creates no generation and no charge, and resets neither attempts nor the deadline. A finished delivery needs a paid replay instead.

ParameterLocationDescription
idpathThe delivery's ID, wdd_.... Another team's ID is 404.
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. A retry with the same key and the same request (compared semantically, after defaults) returns the first response with Idempotency-Replayed: true for 35 days; a different request with the key is 409 idempotency_conflict, and a retry while the first is running is 409 request_in_progress with Retry-After: 1. A failed validation doesn't use the key. A missing key is 422 invalid_field.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Request body
{}
Responses

200The delivery, with its effective next_attempt_at and blocked_reason.

{
  "id": "wdd_5gHjKl8ZxCv3BnMq7WeRtY",
  "message_id": "wdm_2xCvBn6mQwE9rTyUi4OpAs",
  "tenant_id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
  "endpoint_id": "wde_6mNbVc8xZaS2dFgH4jKlQw",
  "generation": 0,
  "replay_of": null,
  "replay_id": null,
  "transform_version": "1",
  "configuration": "original",
  "status": "retry_wait",
  "blocked_reason": null,
  "attempt_count": 2,
  "next_attempt_at": "2026-10-09T18:24:30Z",
  "expires_at": "2026-10-12T18:22:01Z",
  "completed_at": null,
  "reason": null,
  "body_bytes": 41,
  "body_sha256": "3f0a2b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a",
  "content_type": "application/json",
  "rate_version": "2026-10-08",
  "delivery_units": 1,
  "price_usd": "0.00005",
  "cancel_requested": false,
  "created_at": "2026-10-09T18:22:01Z",
  "updated_at": "2026-10-09T18:24:30Z"
}

400The body isn't valid JSON, isn't an object, or has a duplicate key (invalid_json).

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Duplicate key \"name\" at byte 31."
  }
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The delivery doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The delivery doesn't exist."
  }
}

409The delivery isn't retry_wait (delivery_not_retryable), or something blocks it (retry_blocked, with reason): a deleted tenant, a paused or disabled endpoint, an unavailable payload, a canceled message, no attempts left, expiry, or a suspended account. Also idempotency_conflict for a different request with the same Idempotency-Key, and request_in_progress while it's running.

{
  "error": {
    "code": "retry_blocked",
    "message": "Motor can't retry this delivery now: endpoint_paused.",
    "reason": "endpoint_paused"
  }
}

413The body is larger than 1 MiB (body_too_large).

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 1048576 bytes."
  }
}

415The body isn't application/json (unsupported_media_type).

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json."
  }
}

422The body has a field, or Idempotency-Key is missing (invalid_field). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`Idempotency-Key` is required for this request."
  }
}

429Too many configuration writes: 20 a second, bursts of 40 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

Replays

POST/webhooks/replays

Replay deliveries in bulk

Selects, for one endpoint, the newest generation of each message created in a window of at most 30 days whose status is in statuses, and replays them in batches as new paid generations (original bytes or the current transform captured now). Each batch is charged when it's created ($0.00005 per 16 KiB unit, at least 1 per delivery); the job stops as failed if the next batch would exceed your spending limit (spending_limit) or max_cost_usd (cost_ceiling_exceeded), or after 24 hours. An empty selection completes at once and costs nothing. At most 10 active replays by default.

ParameterLocationDescription
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. A retry with the same key and the same request (compared semantically, after defaults) returns the first response with Idempotency-Replayed: true for 35 days; a different request with the key is 409 idempotency_conflict, and a retry while the first is running is 409 request_in_progress with Retry-After: 1. A failed validation doesn't use the key. A missing key is 422 invalid_field.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Request body
{
  "endpoint_id": "wde_6mNbVc8xZaS2dFgH4jKlQw",
  "created_after": "2026-10-01T00:00:00Z",
  "created_before": "2026-10-09T00:00:00Z",
  "max_cost_usd": "5.00"
}
FieldTypeDescription
endpoint_idrequiredstringAn endpoint of your team that isn't deleted or disabled.
created_afterrequiredstringInclusive; by message creation time.
created_beforerequiredstringExclusive; at most 30 days after created_after.
statusesarray["failed", "expired"] by default.
configurationstringoriginal (the default) or current.
max_deliveriesinteger1,000 by default. A larger selection is refused.
max_cost_usdstring or nullA ceiling on the job's total charge, as a decimal string of dollars. null (the default) for none.
Responses

202The replay, with selected_count.

{
  "id": "wdr_3eDcRf6VtGb8YhNuJm2IkO",
  "endpoint_id": "wde_6mNbVc8xZaS2dFgH4jKlQw",
  "created_after": "2026-10-01T00:00:00Z",
  "created_before": "2026-10-09T00:00:00Z",
  "statuses": [
    "failed",
    "expired"
  ],
  "configuration": "original",
  "transform_version": "1",
  "max_deliveries": 1000,
  "max_cost_usd": "5.00",
  "status": "queued",
  "reason": null,
  "selected_count": 312,
  "created_count": 0,
  "skipped_count": 0,
  "pending_count": 312,
  "charged_usd": "0.00",
  "rate_version": "2026-10-08",
  "deadline": "2026-10-10T18:22:01Z",
  "created_at": "2026-10-09T18:22:01Z",
  "updated_at": "2026-10-09T18:22:01Z",
  "completed_at": null
}

400The body isn't valid JSON, isn't an object, or has a duplicate key (invalid_json).

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Duplicate key \"name\" at byte 31."
  }
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

402No payment card is on the account (billing_required).

{
  "error": {
    "code": "billing_required",
    "message": "Add a payment card on the billing page before sending webhooks."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

409The endpoint is disabled (invalid_state), or the team has its maximum of active replays (quota_exceeded). Also idempotency_conflict for a different request with the same Idempotency-Key, and request_in_progress while it's running.

{
  "error": {
    "code": "quota_exceeded",
    "message": "Your team has reached its limit of 10 active replays. Delete some, or contact support@motor.dev to raise the limit."
  }
}

413The body is larger than 1 MiB (body_too_large).

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 1048576 bytes."
  }
}

415The body isn't application/json (unsupported_media_type).

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json."
  }
}

422A field is missing or invalid, the window is longer than 30 days, more deliveries match than max_deliveries, or Idempotency-Key is missing (invalid_field). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "The window has 1412 matching deliveries, more than max_deliveries (1000). Narrow the window or raise max_deliveries, up to 10000.",
    "field": "max_deliveries"
  }
}

429Too many configuration writes: 20 a second, bursts of 40 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

GET/webhooks/replays

List replays

Lists replays submitted in a window of at most 30 days, oldest first.

ParameterLocationDescription
created_afterqueryInclusive RFC 3339 time. By default, 30 days before created_before. Lists replays created in a window of at most 30 days.
created_beforequeryExclusive RFC 3339 time, after created_after and at most 30 days later. By default, now.
limitqueryFrom 1 to 200; 50 by default.
cursorquerynext_cursor from the previous page. It's bound to your team, this list and its filters; another is 422 invalid_cursor. Results are oldest first; records changed between pages may move or disappear.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

200A page of replays.

{
  "data": [
    {
      "id": "wdr_3eDcRf6VtGb8YhNuJm2IkO",
      "endpoint_id": "wde_6mNbVc8xZaS2dFgH4jKlQw",
      "created_after": "2026-10-01T00:00:00Z",
      "created_before": "2026-10-09T00:00:00Z",
      "statuses": [
        "failed",
        "expired"
      ],
      "configuration": "original",
      "transform_version": "1",
      "max_deliveries": 1000,
      "max_cost_usd": "5.00",
      "status": "queued",
      "reason": null,
      "selected_count": 312,
      "created_count": 0,
      "skipped_count": 0,
      "pending_count": 312,
      "charged_usd": "0.00",
      "rate_version": "2026-10-08",
      "deadline": "2026-10-10T18:22:01Z",
      "created_at": "2026-10-09T18:22:01Z",
      "updated_at": "2026-10-09T18:22:01Z",
      "completed_at": null
    }
  ],
  "next_cursor": null
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

422A time is invalid, the window is longer than 30 days (invalid_field), or the cursor isn't for this list (invalid_cursor). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`created_after` must be before created_before."
  }
}

429Too many reads: 100 a second, bursts of 200 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

GET/webhooks/replays/{id}

Get a replay

Returns a replay with its counts and charge so far.

ParameterLocationDescription
idpathThe replay's ID, wdr_.... Another team's ID is 404.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

200The replay.

{
  "id": "wdr_3eDcRf6VtGb8YhNuJm2IkO",
  "endpoint_id": "wde_6mNbVc8xZaS2dFgH4jKlQw",
  "created_after": "2026-10-01T00:00:00Z",
  "created_before": "2026-10-09T00:00:00Z",
  "statuses": [
    "failed",
    "expired"
  ],
  "configuration": "original",
  "transform_version": "1",
  "max_deliveries": 1000,
  "max_cost_usd": "5.00",
  "status": "queued",
  "reason": null,
  "selected_count": 312,
  "created_count": 0,
  "skipped_count": 0,
  "pending_count": 312,
  "charged_usd": "0.00",
  "rate_version": "2026-10-08",
  "deadline": "2026-10-10T18:22:01Z",
  "created_at": "2026-10-09T18:22:01Z",
  "updated_at": "2026-10-09T18:22:01Z",
  "completed_at": null
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The replay doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The replay doesn't exist."
  }
}

422 Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`Motor-Mode` must be live or test."
  }
}

429Too many reads: 100 a second, bursts of 200 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

GET/webhooks/replays/{id}/items

List a replay's items

Lists the result for each selected delivery, in selection order. Items not reached yet are pending, or skipped with the stop reason once the replay stopped.

ParameterLocationDescription
idpathThe replay's ID, wdr_.... Another team's ID is 404.
limitqueryFrom 1 to 200; 50 by default.
cursorquerynext_cursor from the previous page. It's bound to your team, this list and its filters; another is 422 invalid_cursor. Results are oldest first; records changed between pages may move or disappear.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

200A page of items.

{
  "data": [
    {
      "number": 0,
      "original_delivery_id": "wdd_5gHjKl8ZxCv3BnMq7WeRtY",
      "delivery_id": "wdd_7uIoPa2SdF5gHjKl9ZxCvB",
      "status": "created",
      "reason": null,
      "price_usd": "0.00005"
    }
  ],
  "next_cursor": "eyJ2IjoxLCJzIjoiOGYzYSIsImEiOiIxNzkxNTY5NzIxMDAwMDAwIDQyIn0.Zm9vYmFyYmF6"
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The replay doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The replay doesn't exist."
  }
}

422limit is invalid (invalid_field), or the cursor isn't for this list (invalid_cursor). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_cursor",
    "message": "The cursor isn't valid for this list. Use next_cursor from the previous response.",
    "field": "cursor"
  }
}

429Too many reads: 100 a second, bursts of 200 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

POST/webhooks/replays/{id}/cancel

Cancel a replay

Stops creating deliveries. Deliveries already created stay, and so do their charges; the remaining items become skipped. Canceling a finished replay changes nothing.

ParameterLocationDescription
idpathThe replay's ID, wdr_.... Another team's ID is 404.
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. A retry with the same key and the same request (compared semantically, after defaults) returns the first response with Idempotency-Replayed: true for 35 days; a different request with the key is 409 idempotency_conflict, and a retry while the first is running is 409 request_in_progress with Retry-After: 1. A failed validation doesn't use the key. A missing key is 422 invalid_field.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Request body
{}
Responses

200The replay.

{
  "id": "wdr_3eDcRf6VtGb8YhNuJm2IkO",
  "endpoint_id": "wde_6mNbVc8xZaS2dFgH4jKlQw",
  "created_after": "2026-10-01T00:00:00Z",
  "created_before": "2026-10-09T00:00:00Z",
  "statuses": [
    "failed",
    "expired"
  ],
  "configuration": "original",
  "transform_version": "1",
  "max_deliveries": 1000,
  "max_cost_usd": "5.00",
  "status": "canceled",
  "reason": "canceled",
  "selected_count": 312,
  "created_count": 200,
  "skipped_count": 112,
  "pending_count": 0,
  "charged_usd": "0.01",
  "rate_version": "2026-10-08",
  "deadline": "2026-10-10T18:22:01Z",
  "created_at": "2026-10-09T18:22:01Z",
  "updated_at": "2026-10-09T18:30:00Z",
  "completed_at": "2026-10-09T18:30:00Z"
}

400The body isn't valid JSON, isn't an object, or has a duplicate key (invalid_json).

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Duplicate key \"name\" at byte 31."
  }
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The replay doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The replay doesn't exist."
  }
}

409Also idempotency_conflict for a different request with the same Idempotency-Key, and request_in_progress while it's running.

{
  "error": {
    "code": "idempotency_conflict",
    "message": "This Idempotency-Key was used for a different request. Use a new key for a new operation."
  }
}

413The body is larger than 1 MiB (body_too_large).

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 1048576 bytes."
  }
}

415The body isn't application/json (unsupported_media_type).

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json."
  }
}

422The body has a field, or Idempotency-Key is missing (invalid_field). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`Idempotency-Key` is required for this request."
  }
}

429Too many configuration writes: 20 a second, bursts of 40 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

Sources

POST/webhooks/sources

Create a source

Creates a verified receive URL for another provider's webhooks, and returns receive_url once. Each verified request becomes a message of the source's tenant, priced like POST /messages and paid by billing_key_id. Stripe signs test and live events with different secrets: create one source for each. At most 1,000 sources by default.

ParameterLocationDescription
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. A retry with the same key and the same request (compared semantically, after defaults) returns the first response with Idempotency-Replayed: true for 35 days; a different request with the key is 409 idempotency_conflict, and a retry while the first is running is 409 request_in_progress with Retry-After: 1. A failed validation doesn't use the key. A missing key is 422 invalid_field.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Request body
{
  "tenant_id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
  "name": "Stripe live",
  "verification": {
    "kind": "stripe",
    "secret": "whsec_test_51NcQz7T"
  },
  "event_type": "stripe.event",
  "event_type_path": "/type",
  "event_type_prefix": "stripe."
}
FieldTypeDescription
tenant_idrequiredstringAn active tenant. Immutable.
namerequiredstring
verificationrequiredobjectOnly kind and secret for every kind but hmac.
event_typerequiredstringAn active event type: the type of every message, or the fallback check when event_type_path is set.
statusstringenabled (the default) or paused.
event_type_pathstring or nullA JSON Pointer, such as /type, whose string (after event_type_prefix) must be an active type; or $github_event / $shopify_topic. null by default.
event_type_prefixstringText before the mapped type, such as stripe.; at most 64 bytes.
dedup_headerstring or nullhmac and bearer only: a header whose value deduplicates requests for 35 days. Not an authentication, signature, Host, or Content-Length header.
metadata
billing_key_idstringAn active secret key of your team that may use webhooks; the creating key by default.
Responses

201The source, with receive_url. An idempotent replay shows it only to the key that created it, while that token is current.

{
  "id": "wds_8kJhGf3dSa7QwErTy5UiOp",
  "tenant_id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
  "name": "Stripe live",
  "verification": {
    "kind": "stripe",
    "secret_hint": "Qz7T",
    "secret_version": 1,
    "old_secret_expires_at": null
  },
  "receive_path": "/webhooks/receive/…x9Qz",
  "token_version": 1,
  "status": "enabled",
  "event_type": "stripe.event",
  "event_type_path": "/type",
  "event_type_prefix": "stripe.",
  "dedup_header": null,
  "billing_key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
  "metadata": {},
  "counters": {
    "accepted": 0,
    "duplicates": 0,
    "rejected": 0
  },
  "version": "1",
  "created_at": "2026-10-09T18:22:01Z",
  "updated_at": "2026-10-09T18:22:01Z",
  "receive_url": "https://api.motor.dev/webhooks/receive/Jx2kq8V0rW5nTq3mZbLs0aYc1dHf7gPe4uKiO6x9Qz"
}

400The body isn't valid JSON, isn't an object, or has a duplicate key (invalid_json).

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Duplicate key \"name\" at byte 31."
  }
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

409The team is at its source limit (quota_exceeded). Also idempotency_conflict for a different request with the same Idempotency-Key, and request_in_progress while it's running.

{
  "error": {
    "code": "quota_exceeded",
    "message": "Your team has reached its limit of 1000 sources. Delete some, or contact support@motor.dev to raise the limit."
  }
}

413The body is larger than 1 MiB (body_too_large).

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 1048576 bytes."
  }
}

415The body isn't application/json (unsupported_media_type).

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json."
  }
}

422A field is missing, invalid, or unknown, the event type isn't active, the secret doesn't fit the kind, or Idempotency-Key is missing (invalid_field). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`verification.secret` must be from 16 to 256 UTF-8 bytes."
  }
}

429Too many configuration writes: 20 a second, bursts of 40 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

503Motor couldn't complete the request, such as sealing a secret or closing a billing month (temporarily_unavailable). Retry.

{
  "error": {
    "code": "temporarily_unavailable",
    "message": "Motor couldn't protect a secret. Retry in a few seconds."
  }
}

GET/webhooks/sources

List sources

Lists sources that aren't deleted, oldest first.

ParameterLocationDescription
tenant_idqueryOnly this tenant's sources.
statusqueryOnly sources with this status.
limitqueryFrom 1 to 200; 50 by default.
cursorquerynext_cursor from the previous page. It's bound to your team, this list and its filters; another is 422 invalid_cursor. Results are oldest first; records changed between pages may move or disappear.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

200A page of sources.

{
  "data": [
    {
      "id": "wds_2wSxEdC4rFvTgB6yHnUjM8",
      "tenant_id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
      "name": "Shopify-style HMAC",
      "verification": {
        "kind": "hmac",
        "algorithm": "sha256",
        "header": "x-acme-signature",
        "encoding": "hex",
        "prefix": "sha256=",
        "signed_content": [
          {
            "header": "x-acme-timestamp"
          },
          {
            "literal": "."
          },
          "body"
        ],
        "timestamp_header": "x-acme-timestamp",
        "tolerance_seconds": 300,
        "secret_hint": "",
        "secret_version": 1,
        "old_secret_expires_at": null
      },
      "receive_path": "/webhooks/receive/…x9Qz",
      "token_version": 1,
      "status": "enabled",
      "event_type": "acme.order",
      "event_type_path": null,
      "event_type_prefix": "",
      "dedup_header": "x-acme-delivery",
      "billing_key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
      "metadata": {},
      "counters": {
        "accepted": 0,
        "duplicates": 0,
        "rejected": 0
      },
      "version": "1",
      "created_at": "2026-10-09T18:22:01Z",
      "updated_at": "2026-10-09T18:22:01Z"
    }
  ],
  "next_cursor": null
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

422A filter is invalid (invalid_field), or the cursor isn't for this list (invalid_cursor). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`status` must be enabled or paused."
  }
}

429Too many reads: 100 a second, bursts of 200 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

GET/webhooks/sources/{id}

Get a source

Returns a source with its ETag. Its secret and receive URL are never shown.

ParameterLocationDescription
idpathThe source's ID, wds_.... Another team's ID is 404.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

200The source.

{
  "id": "wds_8kJhGf3dSa7QwErTy5UiOp",
  "tenant_id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
  "name": "Stripe live",
  "verification": {
    "kind": "stripe",
    "secret_hint": "Qz7T",
    "secret_version": 1,
    "old_secret_expires_at": null
  },
  "receive_path": "/webhooks/receive/…x9Qz",
  "token_version": 1,
  "status": "enabled",
  "event_type": "stripe.event",
  "event_type_path": "/type",
  "event_type_prefix": "stripe.",
  "dedup_header": null,
  "billing_key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
  "metadata": {},
  "counters": {
    "accepted": 0,
    "duplicates": 0,
    "rejected": 0
  },
  "version": "1",
  "created_at": "2026-10-09T18:22:01Z",
  "updated_at": "2026-10-09T18:22:01Z"
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The source doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The source doesn't exist."
  }
}

422 Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`Motor-Mode` must be live or test."
  }
}

429Too many reads: 100 a second, bursts of 200 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

PATCH/webhooks/sources/{id}

Update a source

Changes the name, status, event type mapping, deduplication header, billing key, or metadata. The tenant and the verifier can't change: rotate the secret with rotate_secret, or create a new source.

ParameterLocationDescription
idpathThe source's ID, wds_.... Another team's ID is 404.
If-MatchheaderThe ETag of the source from your last read, in quotes, such as "wd:wde_6mNbVc8xZaS2dFgH4jKlQw:3". Missing is 428 precondition_required; stale is 412 revision_conflict (the response carries the current ETag); malformed is 422 invalid_field.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Request body
{
  "status": "paused"
}
FieldTypeDescription
namestring
statusstringenabled or paused.
event_typestringAn active event type.
event_type_pathstring or null
event_type_prefixstring
dedup_headerstring or null
metadata
billing_key_idstring
Responses

200The source.

{
  "id": "wds_8kJhGf3dSa7QwErTy5UiOp",
  "tenant_id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
  "name": "Stripe live",
  "verification": {
    "kind": "stripe",
    "secret_hint": "Qz7T",
    "secret_version": 1,
    "old_secret_expires_at": null
  },
  "receive_path": "/webhooks/receive/…x9Qz",
  "token_version": 1,
  "status": "paused",
  "event_type": "stripe.event",
  "event_type_path": "/type",
  "event_type_prefix": "stripe.",
  "dedup_header": null,
  "billing_key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
  "metadata": {},
  "counters": {
    "accepted": 0,
    "duplicates": 0,
    "rejected": 0
  },
  "version": "2",
  "created_at": "2026-10-09T18:22:01Z",
  "updated_at": "2026-10-09T18:22:03Z"
}

400The body isn't valid JSON, isn't an object, or has a duplicate key (invalid_json).

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Duplicate key \"name\" at byte 31."
  }
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The source doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The source doesn't exist."
  }
}

412If-Match is stale (revision_conflict). The response's ETag is current: read the resource again and retry.

{
  "error": {
    "code": "revision_conflict",
    "message": "The resource changed since you read it. Read it again and retry with its new ETag.",
    "resource_id": "wde_6mNbVc8xZaS2dFgH4jKlQw"
  }
}

413The body is larger than 1 MiB (body_too_large).

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 1048576 bytes."
  }
}

415The body isn't application/json (unsupported_media_type).

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json."
  }
}

422A field is invalid or unknown, or If-Match is malformed (invalid_field). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`billing_key_id` must be the ID of an active API key of your team that may use webhooks."
  }
}

428If-Match is missing (precondition_required).

{
  "error": {
    "code": "precondition_required",
    "message": "Send the ETag of the resource in the If-Match header. Read the resource to get its current ETag."
  }
}

429Too many configuration writes: 20 a second, bursts of 40 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

DELETE/webhooks/sources/{id}

Delete a source

Deletes the source: its receive URLs and secrets stop working at once. Accepted messages stay with their tenant and follow normal retention.

ParameterLocationDescription
idpathThe source's ID, wds_.... Another team's ID is 404.
If-MatchheaderThe ETag of the source from your last read, in quotes, such as "wd:wde_6mNbVc8xZaS2dFgH4jKlQw:3". Missing is 428 precondition_required; stale is 412 revision_conflict (the response carries the current ETag); malformed is 422 invalid_field.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

204Deleted.

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The source doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The source doesn't exist."
  }
}

412If-Match is stale (revision_conflict). The response's ETag is current: read the resource again and retry.

{
  "error": {
    "code": "revision_conflict",
    "message": "The resource changed since you read it. Read it again and retry with its new ETag.",
    "resource_id": "wde_6mNbVc8xZaS2dFgH4jKlQw"
  }
}

422If-Match is malformed (invalid_field). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`If-Match` must be one strong ETag in quotes."
  }
}

428If-Match is missing (precondition_required).

{
  "error": {
    "code": "precondition_required",
    "message": "Send the ETag of the resource in the If-Match header. Read the resource to get its current ETag."
  }
}

429Too many configuration writes: 20 a second, bursts of 40 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

POST/webhooks/sources/{id}/rotate_token

Rotate the receive URL

Makes a new receive URL and returns it once. The old URL keeps working for overlap_seconds (86,400 by default); 0 retires it at once. Another nonzero rotation during an overlap is 409.

ParameterLocationDescription
idpathThe source's ID, wds_.... Another team's ID is 404.
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. A retry with the same key and the same request (compared semantically, after defaults) returns the first response with Idempotency-Replayed: true for 35 days; a different request with the key is 409 idempotency_conflict, and a retry while the first is running is 409 request_in_progress with Retry-After: 1. A failed validation doesn't use the key. A missing key is 422 invalid_field.
If-MatchheaderThe ETag of the source from your last read, in quotes, such as "wd:wde_6mNbVc8xZaS2dFgH4jKlQw:3". Missing is 428 precondition_required; stale is 412 revision_conflict (the response carries the current ETag); malformed is 422 invalid_field.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Request body
{
  "overlap_seconds": 86400
}
FieldTypeDescription
overlap_secondsinteger
Responses

200The source, with the new receive_url. An idempotent replay shows it only to the key that rotated it, while it's current.

{
  "id": "wds_8kJhGf3dSa7QwErTy5UiOp",
  "tenant_id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
  "name": "Stripe live",
  "verification": {
    "kind": "stripe",
    "secret_hint": "Qz7T",
    "secret_version": 1,
    "old_secret_expires_at": null
  },
  "receive_path": "/webhooks/receive/…Lm3P",
  "token_version": 2,
  "status": "enabled",
  "event_type": "stripe.event",
  "event_type_path": "/type",
  "event_type_prefix": "stripe.",
  "dedup_header": null,
  "billing_key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
  "metadata": {},
  "counters": {
    "accepted": 0,
    "duplicates": 0,
    "rejected": 0
  },
  "version": "2",
  "created_at": "2026-10-09T18:22:01Z",
  "updated_at": "2026-10-09T18:22:03Z",
  "receive_url": "https://api.motor.dev/webhooks/receive/aB4cD6eF8gH0iJ2kL4mN6oP8qR0sT2uV4wX6yZ8Lm3P"
}

400The body isn't valid JSON, isn't an object, or has a duplicate key (invalid_json).

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Duplicate key \"name\" at byte 31."
  }
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The source doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The source doesn't exist."
  }
}

409The previous URL is still in its overlap and overlap_seconds isn't 0 (invalid_state). Also idempotency_conflict for a different request with the same Idempotency-Key, and request_in_progress while it's running.

{
  "error": {
    "code": "invalid_state",
    "message": "The previous receive URL is still in its overlap. Wait for it to end, or rotate with overlap_seconds: 0."
  }
}

412If-Match is stale (revision_conflict). The response's ETag is current: read the resource again and retry.

{
  "error": {
    "code": "revision_conflict",
    "message": "The resource changed since you read it. Read it again and retry with its new ETag.",
    "resource_id": "wde_6mNbVc8xZaS2dFgH4jKlQw"
  }
}

413The body is larger than 1 MiB (body_too_large).

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 1048576 bytes."
  }
}

415The body isn't application/json (unsupported_media_type).

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json."
  }
}

422overlap_seconds is invalid, a field is unknown, or a header is missing or malformed (invalid_field). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`overlap_seconds` must be from 0 to 86400."
  }
}

428If-Match is missing (precondition_required).

{
  "error": {
    "code": "precondition_required",
    "message": "Send the ETag of the resource in the If-Match header. Read the resource to get its current ETag."
  }
}

429Too many configuration writes: 20 a second, bursts of 40 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

503Motor couldn't complete the request, such as sealing a secret or closing a billing month (temporarily_unavailable). Retry.

{
  "error": {
    "code": "temporarily_unavailable",
    "message": "Motor couldn't protect a secret. Retry in a few seconds."
  }
}

POST/webhooks/sources/{id}/rotate_secret

Rotate the verification secret

Replaces the secret with one of the same kind (secret, or secret_base64 for an hmac source). The old one also verifies for overlap_seconds (86,400 by default); 0 retires it at once. Motor never shows a secret back.

ParameterLocationDescription
idpathThe source's ID, wds_.... Another team's ID is 404.
Idempotency-Keyheader1 to 128 printable ASCII characters that you choose for this operation. A retry with the same key and the same request (compared semantically, after defaults) returns the first response with Idempotency-Replayed: true for 35 days; a different request with the key is 409 idempotency_conflict, and a retry while the first is running is 409 request_in_progress with Retry-After: 1. A failed validation doesn't use the key. A missing key is 422 invalid_field.
If-MatchheaderThe ETag of the source from your last read, in quotes, such as "wd:wde_6mNbVc8xZaS2dFgH4jKlQw:3". Missing is 428 precondition_required; stale is 412 revision_conflict (the response carries the current ETag); malformed is 422 invalid_field.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Request body
{
  "secret": "whsec_live_7cTqWm2R2d9",
  "overlap_seconds": 86400
}
FieldTypeDescription
secretstringThe new secret, with the same rules as at creation.
secret_base64stringhmac only, instead of secret.
overlap_secondsinteger
Responses

200The source.

{
  "id": "wds_8kJhGf3dSa7QwErTy5UiOp",
  "tenant_id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
  "name": "Stripe live",
  "verification": {
    "kind": "stripe",
    "secret_hint": "R2d9",
    "secret_version": 2,
    "old_secret_expires_at": "2026-10-10T18:22:03Z"
  },
  "receive_path": "/webhooks/receive/…x9Qz",
  "token_version": 1,
  "status": "enabled",
  "event_type": "stripe.event",
  "event_type_path": "/type",
  "event_type_prefix": "stripe.",
  "dedup_header": null,
  "billing_key_id": "key_BWBeN28Vb7cMEx7Ym8AUzs",
  "metadata": {},
  "counters": {
    "accepted": 0,
    "duplicates": 0,
    "rejected": 0
  },
  "version": "2",
  "created_at": "2026-10-09T18:22:01Z",
  "updated_at": "2026-10-09T18:22:03Z"
}

400The body isn't valid JSON, isn't an object, or has a duplicate key (invalid_json).

{
  "error": {
    "code": "invalid_json",
    "message": "The request body isn't valid JSON. Duplicate key \"name\" at byte 31."
  }
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The source doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The source doesn't exist."
  }
}

409The previous secret is still in its overlap and overlap_seconds isn't 0 (invalid_state). Also idempotency_conflict for a different request with the same Idempotency-Key, and request_in_progress while it's running.

{
  "error": {
    "code": "invalid_state",
    "message": "The previous secret is still in its overlap. Wait for it to end, or rotate with overlap_seconds: 0."
  }
}

412If-Match is stale (revision_conflict). The response's ETag is current: read the resource again and retry.

{
  "error": {
    "code": "revision_conflict",
    "message": "The resource changed since you read it. Read it again and retry with its new ETag.",
    "resource_id": "wde_6mNbVc8xZaS2dFgH4jKlQw"
  }
}

413The body is larger than 1 MiB (body_too_large).

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 1048576 bytes."
  }
}

415The body isn't application/json (unsupported_media_type).

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as application/json."
  }
}

422The secret doesn't fit the source's kind, a field is invalid or unknown, or a header is missing or malformed (invalid_field). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`secret` must be from 16 to 256 UTF-8 bytes."
  }
}

428If-Match is missing (precondition_required).

{
  "error": {
    "code": "precondition_required",
    "message": "Send the ETag of the resource in the If-Match header. Read the resource to get its current ETag."
  }
}

429Too many configuration writes: 20 a second, bursts of 40 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

503Motor couldn't complete the request, such as sealing a secret or closing a billing month (temporarily_unavailable). Retry.

{
  "error": {
    "code": "temporarily_unavailable",
    "message": "Motor couldn't protect a secret. Retry in a few seconds."
  }
}

Receive

POST/webhooks/receive/{token}

Receive a provider's webhook

The public URL that you give a provider. It needs no Motor key: the token selects the source, and the provider's signature (or bearer token) authenticates the request, checked against the exact raw bytes before anything else. A verified, new request becomes a message of the source's tenant, charged to the source's billing key like POST /messages ($0.00001 per 64 KiB ingested, at least 1 unit, even with no matching endpoint, plus $0.00005 per 16 KiB per endpoint delivery), and Motor answers only after it's stored. A duplicate (the same provider ID and bytes, or the same body within 24 hours for providers without signed IDs) gets 200 without a new message or charge. Failed requests are neither kept nor charged. Acknowledgments: 202 {"message_id", "duplicate": false} for a new request and 200 {"message_id", "duplicate": true} for a duplicate; Slack gets 200 with an empty body, or 200 {"challenge"} for a signed url_verification (admitted as a paid ingestion-only motor.source_verification message); Twilio gets 200 application/xml <Response/>. Motor never passes a receiver's response back. Requests are limited to 50 a second per source, bursts of 100, and 20 failed verifications a second per IP address.

This endpoint authenticates with the secret in its URL. Keep the URL private.

ParameterLocationDescription
tokenpathThe source's secret receive token, from receive_url. An unknown, retired, or deleted token is 404.
webhook-signatureheaderstandard_webhooks: with webhook-id and webhook-timestamp (within 300 seconds).
Stripe-Signatureheaderstripe: t=...,v1=..., within 300 seconds.
X-Hub-Signature-256headergithub: with X-GitHub-Delivery and X-GitHub-Event.
X-Shopify-Hmac-SHA256headershopify: with X-Shopify-Webhook-Id.
X-Slack-Signatureheaderslack: with X-Slack-Request-Timestamp, within 300 seconds.
X-Twilio-Signatureheadertwilio: over the URL as called and the form fields, or a bodySHA256 query parameter for JSON.
Authorizationheaderbearer: Bearer <secret>.
Request body
{
  "id": "evt_1Q2w3E4r",
  "type": "invoice.paid",
  "livemode": true,
  "data": {
    "object": {
      "id": "in_1Q2w"
    }
  }
}
Responses

200A duplicate (duplicate: true, no new charge); or any accepted Slack request (an empty body, or {"challenge"} for URL verification); or any accepted Twilio request (<Response/>).

{
  "message_id": "wdm_2xCvBn6mQwE9rTyUi4OpAs",
  "duplicate": true
}

202Accepted, stored, fanned out, and charged. Delivery happens afterwards.

{
  "message_id": "wdm_2xCvBn6mQwE9rTyUi4OpAs",
  "duplicate": false
}

400The JSON body is malformed (invalid_json), a form body is malformed (invalid_form), or the request has two Content-Type headers (invalid_request). Only after verification, except the header check.

{
  "error": {
    "code": "invalid_json",
    "message": "The body isn't valid JSON."
  }
}

401The signature, timestamp, or bearer secret is missing, duplicated, malformed, stale, or wrong (invalid_signature).

{
  "error": {
    "code": "invalid_signature",
    "message": "The request has no Stripe-Signature header."
  }
}

402The account has no payment card or the source's billing key is revoked (billing_required), or the charge would exceed the billing key's spending limit (spending_limit). The provider should retry; Motor records a webhooks.budget.blocked event.

{
  "error": {
    "code": "spending_limit",
    "message": "This work would exceed the paying API key's monthly spending limit. Raise the limit on the API keys page, or wait until the next billing month starts at 00:00 UTC."
  }
}

404No source has this token, or its tenant is deleted (not_found).

{
  "error": {
    "code": "not_found",
    "message": "No source has this receive URL."
  }
}

409A request with the same provider ID and different bytes was already accepted (invalid_state).

{
  "error": {
    "code": "invalid_state",
    "message": "A request with this event ID and different bytes was already accepted."
  }
}

413The body is larger than 1 MiB (body_too_large).

{
  "error": {
    "code": "body_too_large",
    "message": "The request body must be at most 1048576 bytes."
  }
}

415The body is compressed, or its type isn't JSON, form, text, or octet-stream (Twilio: form or JSON only) (unsupported_media_type).

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Sources don't accept compressed bodies. Send them without Content-Encoding."
  }
}

422The event type that the request maps to isn't active (invalid_event_type), or a provider field is missing, such as Stripe's id, type, and livemode or GitHub's delivery ID (invalid_field). Register the type, then let the provider retry.

{
  "error": {
    "code": "invalid_event_type",
    "message": "The event type stripe.charge.refunded isn't an active type of your catalog. Create it with POST /webhooks/event_types first.",
    "field": "event_type"
  }
}

429Too many requests for the source, too many failed verifications from the address, or too many admissions for the team (rate_limited); or the team's pending deliveries or stored content is at its limit (queue_full).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

503The source is paused, the account can't accept webhooks now, or Motor couldn't store the request (temporarily_unavailable). The provider should retry.

{
  "error": {
    "code": "temporarily_unavailable",
    "message": "The source is paused. Retry later."
  }
}

Analytics and events

GET/webhooks/analytics

Get analytics

Returns hourly or daily counts of admissions, deliveries, attempts, and outcomes in a window of at most 30 days, and the current queue. Counts are by the time of each transition, eventually consistent within about a minute, and never a billing record. Without filters they cover the whole team; endpoint_id, source_id, and tenant_id narrow them (in that order of precedence).

ParameterLocationDescription
created_afterqueryRequired. Inclusive RFC 3339 time.
created_beforequeryRequired. Exclusive RFC 3339 time, at most 30 days after created_after.
bucketqueryhour (default) or day, aligned to UTC.
tenant_idqueryOnly this tenant's admissions and attempts.
endpoint_idqueryOnly this endpoint's deliveries and attempts.
source_idqueryOnly this source's captures, duplicates, and rejections.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

200The counts.

{
  "bucket": "hour",
  "created_after": "2026-10-09T00:00:00Z",
  "created_before": "2026-10-09T03:00:00Z",
  "data": [
    {
      "start": "2026-10-09T00:00:00Z",
      "admitted_messages": 1200,
      "ingest_bytes": 1310720,
      "deliveries": 2400,
      "attempts": 2461,
      "succeeded": 2380,
      "failed": 3,
      "expired": 0,
      "canceled": 2,
      "duplicates": 14,
      "rejected_auth": 1,
      "throttled": 0,
      "queue_depth": 0,
      "oldest_due_age_seconds": 0
    },
    {
      "start": "2026-10-09T02:00:00Z",
      "admitted_messages": 860,
      "ingest_bytes": 901120,
      "deliveries": 1720,
      "attempts": 1733,
      "succeeded": 1712,
      "failed": 0,
      "expired": 0,
      "canceled": 0,
      "duplicates": 6,
      "rejected_auth": 0,
      "throttled": 0,
      "queue_depth": 0,
      "oldest_due_age_seconds": 0
    }
  ],
  "queue_depth": 37,
  "oldest_due_age_seconds": 4
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

422A time is missing or invalid, the window is longer than 30 days, bucket is invalid, or tenant_id, endpoint_id, or source_id doesn't name a resource of your team (invalid_field). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`created_after` and created_before are required, at most 30 days apart."
  }
}

429Too many reads: 100 a second, bursts of 200 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

GET/webhooks/events

List events

Lists operational events of the last 30 days, oldest first: failed and expired deliveries, disabled and recovered endpoints, rejected source requests, completed and failed replays, and captures blocked by spending. Events hold IDs, reasons, counts, and times, never payloads or secrets.

ParameterLocationDescription
typequeryOnly events of this type.
resource_idqueryOnly events about this resource, including as the first or last of a coalesced event.
tenant_idqueryOnly this tenant's events.
limitqueryFrom 1 to 200; 50 by default.
cursorquerynext_cursor from the previous page. It's bound to your team, this list and its filters; another is 422 invalid_cursor. Results are oldest first; records changed between pages may move or disappear.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

200A page of events.

{
  "data": [
    {
      "id": "wdv_9oLpKi4JuHy7GtFrDe2SwQ",
      "type": "webhooks.endpoint.disabled",
      "created_at": "2026-10-09T18:22:01Z",
      "mode": "live",
      "resource_id": "wde_6mNbVc8xZaS2dFgH4jKlQw",
      "tenant_id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
      "resource_version": "4",
      "data": {
        "endpoint_id": "wde_6mNbVc8xZaS2dFgH4jKlQw",
        "reason": "endpoint_gone"
      },
      "coalesced": false,
      "count": 1,
      "first_id": "wde_6mNbVc8xZaS2dFgH4jKlQw",
      "last_id": "wde_6mNbVc8xZaS2dFgH4jKlQw"
    }
  ],
  "next_cursor": null
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

422type or limit is invalid (invalid_field), or the cursor isn't for this list (invalid_cursor). Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`type` must be one of webhooks.delivery.failed, ..."
  }
}

429Too many reads: 100 a second, bursts of 200 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}

GET/webhooks/events/{id}

Get an event

Returns an event of the last 30 days.

ParameterLocationDescription
idpathThe event's ID, wdv_.... Another team's ID is 404.
Motor-ModeheaderWebhooks are live-only. Omit this header or send live. test gives 422 unsupported_mode and any other value 422 invalid_field, before anything else happens. Test against a real receiver with POST /endpoints/{id}/test, or use a separate tenant whose endpoints point at your staging receivers.
Responses

200The event.

{
  "id": "wdv_9oLpKi4JuHy7GtFrDe2SwQ",
  "type": "webhooks.endpoint.disabled",
  "created_at": "2026-10-09T18:22:01Z",
  "mode": "live",
  "resource_id": "wde_6mNbVc8xZaS2dFgH4jKlQw",
  "tenant_id": "wdt_4fRm8NqZ2kLpVx7cJs9hYb",
  "resource_version": "4",
  "data": {
    "endpoint_id": "wde_6mNbVc8xZaS2dFgH4jKlQw",
    "reason": "endpoint_gone"
  },
  "coalesced": false,
  "count": 1,
  "first_id": "wde_6mNbVc8xZaS2dFgH4jKlQw",
  "last_id": "wde_6mNbVc8xZaS2dFgH4jKlQw"
}

401The key is missing, invalid, revoked, publishable, or not allowed to use webhooks (unauthorized).

{
  "error": {
    "code": "unauthorized",
    "message": "This API key is invalid or has been revoked."
  }
}

403The team's account, or its use of webhooks, is suspended (account_suspended).

{
  "error": {
    "code": "account_suspended",
    "message": "This team's Motor account is suspended. Contact support@motor.dev for help."
  }
}

404The event doesn't exist, or belongs to another team (not_found).

{
  "error": {
    "code": "not_found",
    "message": "The event doesn't exist."
  }
}

422 Also unsupported_mode / invalid_field for Motor-Mode.

{
  "error": {
    "code": "invalid_field",
    "message": "`Motor-Mode` must be live or test."
  }
}

429Too many reads: 100 a second, bursts of 200 (rate_limited).

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after 1 seconds."
  }
}