Inbound Endpoints
Receive webhooks from external services, store every payload, and forward them to your app with retries and serialized delivery.
How It Works
Create an endpoint and you get a unique URL. Point external services (Stripe, GitHub, etc.) at that URL. When a webhook arrives, Runlater stores the raw payload and forwards it to your app — with automatic retries if your server is down.
- Create an endpoint with a name and forward URLs
- Copy the inbound URL and paste it into Stripe, GitHub, or any service
- Runlater receives the webhook, stores the full payload, and forwards it to your app
- If forwarding fails, we retry with exponential backoff (configurable, 0-10 attempts)
- Choose serial (queued) or parallel delivery per endpoint
Quick Start
1. Create an endpoint
curl -X POST https://runlater.eu/api/v1/endpoints \ -H "Authorization: Bearer pk_xxx.sk_xxx" \ -H "Content-Type: application/json" \ -d '{ "name": "Stripe webhooks", "forward_urls": ["https://myapp.com/webhooks/stripe"], "retry_attempts": 5, "use_lane": true }'
The response includes an
inbound_url
— this is the URL you give to Stripe (or any external service).
2. Configure the external service
Copy the inbound URL and paste it into the webhook settings of the external service. The URL looks like:
https://runlater.eu/in/ep_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
3. Webhooks start flowing
Every request to the inbound URL is stored and forwarded to your forward URLs. The original HTTP method, headers, and body are preserved.
Delivery Modes
Serial delivery (queue enabled — default)
When use_lane
is enabled (the default),
events are forwarded one at a time — only one event per endpoint runs at any moment.
This prevents race conditions for services like Stripe, where overlapping requests could cause double-billing.
Under the hood, each endpoint uses a lane based on the endpoint name to guarantee serialized execution.
Parallel delivery (queue disabled)
Set use_lane
to false
when event ordering doesn't matter. Events are forwarded concurrently for faster throughput.
Ideal for stateless webhooks like analytics events or notifications.
Retry Configuration
Each endpoint has a configurable number of retry attempts (0-10, default: 5). When forwarding fails, Runlater retries with exponential backoff. Set to 0 to disable retries entirely.
Connection Pausing
Pause webhook delivery when your target server is down for maintenance or a deploy. Events are still received and logged, but executions are held until you resume.
- Paused — events accepted (HTTP 200), executions queued but not forwarded
- Resumed — workers wake up and process the backlog automatically
- Disabled — events rejected (HTTP 503). Different from pausing.
Via API
# Pause delivery curl -X POST https://runlater.eu/api/v1/endpoints/:id/pause \ -H "Authorization: Bearer pk_xxx.sk_xxx" # Resume delivery curl -X POST https://runlater.eu/api/v1/endpoints/:id/resume \ -H "Authorization: Bearer pk_xxx.sk_xxx"
Deduplication
Many webhook providers (Stripe, GitHub, Shopify) deliver events at least once — meaning your endpoint may receive the same event multiple times.
Use the Lua transform() function to deduplicate by returning a
debounce_key and
debounce window.
Within the window, only one forwarding execution is created per key.
-- Deduplicate Stripe events by event ID function transform(event) local data = json_decode(event.body) return { debounce_key = data.id, debounce = "30s" } end
The first event with a given key schedules an execution at
now + debounce.
If the same key arrives again within the window, it replaces the pending execution but keeps the original scheduled time — so the latest payload is always forwarded, but only once.
data.id —
GitHub: event.headers["x-github-delivery"] —
Shopify: event.headers["x-shopify-webhook-id"]
Debounce requires a Lua script (Pro only). See the Lua debounce reference for the full details.
Custom Forwarding
Override the HTTP method, headers, or body for all forwarded requests — no code required. Set these on any endpoint to apply static defaults to every event before it’s forwarded.
| Field | Effect |
|---|---|
forward_method |
Replaces the original HTTP method (e.g. always forward as POST) |
forward_headers |
Merged with the original headers — custom headers win on conflict |
forward_body |
Replaces the original request body entirely |
Leave a field empty to forward the original value. Custom Forwarding is available on all tiers.
forward_method
to POST
and every event is forwarded as a POST request — no Lua needed.
Processing Order
When an event arrives, Lua verification and filtering run on the original
request.
Custom Forwarding and Lua transform()
only affect what gets forwarded:
receive → verify(original) → filter(original) → custom forwarding → transform() → route() → forward
-
verify()andfilter()always see the original headers, body, and method — so signature checks work correctly. - Custom Forwarding applies static defaults (method, headers, body) for the forwarded request.
-
Lua
transform()runs last. Any values it returns override the Custom Forwarding values. Fields it doesn’t return keep the Custom Forwarding value.
transform()
takes priority when it returns values.
Lua Scripting Pro
Write Lua scripts to verify signatures, filter unwanted events, transform payloads, and route to specific URLs. See the full Lua Scripting documentation for the complete reference, examples, and error handling details.
Event Replay
Every inbound event is stored with the full request payload. You can replay any event from the dashboard or via the API — useful for reprocessing after a bug fix.
curl -X POST https://runlater.eu/api/v1/endpoints/:id/events/:event_id/replay \ -H "Authorization: Bearer pk_xxx.sk_xxx"
Inbound URL
The inbound URL accepts any HTTP method (GET, POST, PUT, PATCH, DELETE). No authentication is needed — the slug in the URL is the authentication, just like monitor ping tokens.
Responses
| Status | Body | Meaning |
|---|---|---|
200 |
{"id": "...", "status": "received"}
|
Event stored and queued for forwarding |
200 |
{"id": "...", "status": "queued"}
|
Event stored but delivery is paused — will be forwarded on resume |
403 |
{"id": "...", "status": "rejected"}
|
Event rejected by Lua script (default for verify; script can override status code) |
404 |
{"error": "Not found"}
|
Invalid slug |
503 |
{"error": "Endpoint disabled"}
|
Endpoint is disabled (providers will retry) |
Endpoints API
Manage endpoints programmatically via the REST API. All endpoints require an API key passed as a Bearer token. See the API Reference for authentication details.
List Endpoints
curl https://runlater.eu/api/v1/endpoints \ -H "Authorization: Bearer pk_xxx.sk_xxx"
Create Endpoint
| Field | Type | Description |
|---|---|---|
name |
string | Display name (required) |
forward_urls |
array | List of URLs to forward events to (required, at least one) |
retry_attempts |
integer | Number of retry attempts, 0-10 (default: 5) |
use_lane |
boolean | Enable serialized delivery (one at a time). Set to false for parallel. (default: true) |
enabled |
boolean | Whether the endpoint accepts events (default: true) |
forward_method |
string |
Override HTTP method for forwarded requests (e.g. "POST")
|
forward_headers |
object | Headers merged with originals (custom headers override on conflict) |
forward_body |
string | Replace original body for forwarded requests |
script |
string | Lua script for webhook processing (Pro only) |
secrets |
object |
Encrypted key-value pairs accessible in
verify()
(Pro only, write-only)
|
curl -X POST https://runlater.eu/api/v1/endpoints \ -H "Authorization: Bearer pk_xxx.sk_xxx" \ -H "Content-Type: application/json" \ -d '{ "name": "GitHub webhooks", "forward_urls": ["https://myapp.com/webhooks/github"], "retry_attempts": 3, "use_lane": false }'
Get Endpoint
curl https://runlater.eu/api/v1/endpoints/:id \ -H "Authorization: Bearer pk_xxx.sk_xxx"
Update Endpoint
curl -X PUT https://runlater.eu/api/v1/endpoints/:id \ -H "Authorization: Bearer pk_xxx.sk_xxx" \ -H "Content-Type: application/json" \ -d '{ "name": "Updated Name", "forward_urls": ["https://myapp.com/webhooks/new-url"] }'
Delete Endpoint
curl -X DELETE https://runlater.eu/api/v1/endpoints/:id \ -H "Authorization: Bearer pk_xxx.sk_xxx"
List Events
curl https://runlater.eu/api/v1/endpoints/:id/events \ -H "Authorization: Bearer pk_xxx.sk_xxx"
Replay Event
curl -X POST https://runlater.eu/api/v1/endpoints/:id/events/:event_id/replay \ -H "Authorization: Bearer pk_xxx.sk_xxx"
Pause Endpoint
curl -X POST https://runlater.eu/api/v1/endpoints/:id/pause \ -H "Authorization: Bearer pk_xxx.sk_xxx"
Resume Endpoint
curl -X POST https://runlater.eu/api/v1/endpoints/:id/resume \ -H "Authorization: Bearer pk_xxx.sk_xxx"
For the full API specification including request/response schemas, visit the interactive API docs.
Use Cases
Stripe webhooks
Never miss a payment event. Runlater buffers Stripe webhooks and forwards them to your app, even during deployments or downtime.
GitHub / GitLab events
Receive push, PR, and issue events reliably. Replay events to reprocess after a bug fix.
Third-party integrations
Any service that sends webhooks — Shopify, Twilio, SendGrid, Slack — can be received and forwarded with full history and retry support.
Tier Limits
| Free | Pro | |
|---|---|---|
| Endpoints | 3 | Unlimited |
| Event history | 30 days | 30 days |
| Lua scripting | — | Verify, filter, transform, route |