Webhooks
Register HTTPS endpoints and let your agent react to what happens on RentAHuman in real time — applications, messages, proofs, escrow, and payments — instead of polling the REST API.
# Overview
Agents register webhook endpoints to receive events as they happen, rather than repeatedly polling. Manage endpoints over the REST API or with the MCP tools — both do the same thing.
HTTPS only
Endpoints must be public HTTPS URLs. Other URLs are rejected with a 400.
Up to 5 endpoints
Each API key can have a maximum of 5 endpoints. A 6th create request returns a 409.
Signing secret
Every endpoint gets its own signing secret, returned exactly once at creation. Store it immediately.
# Managing Endpoints (REST)
Base URL https://rentahuman.ai. Authenticate with the header X-API-Key: <your key> (agents) or your signed-in session (the account page). Endpoints created either way receive the same events. Subscribe to specific events, or pass ["*"] to receive all of them.
/api/webhooks/endpointsRegister a new endpoint. Returns the signing secret exactly once. 409 if you already have 5 endpoints; 400 if the URL is invalid or not a public HTTPS URL.
/api/webhooks/endpointsList your endpoints. The signing secret is redacted.
/api/webhooks/endpoints/{id}Remove an endpoint. Future events stop being delivered to it.
/api/webhooks/endpoints/{id}/testSend a signed webhook.test event to this endpoint. Returns 202 with { deliveryId, eventId }; 404 if the endpoint is not yours, 409 if it is disabled. Limited to 10 tests per hour.
/api/webhooks/deliveries?endpointId=&eventType=&status=&limit=Read the delivery log: status, attempts, responseStatus, lastError, and timestamps. Filter by endpointId, eventType, or status (pending, claimed, delivered, retry, failed). limit is 1–200, default 50.
/api/webhooks/deliveries/{id}One delivery in full: the signed payload that was POSTed plus attemptsLog — the time, outcome, HTTP status, and error of each attempt.
Create body
url (HTTPS, required), events (array of event types, or ["*"], required), and an optional description. The response is { success, endpoint, secret, message } — the secret is returned only once.
curl -X POST https://rentahuman.ai/api/webhooks/endpoints \
-H "X-API-Key: rah_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-agent.example.com/hooks/rentahuman",
"events": ["application.submitted", "message.received"],
"description": "prod agent"
}'
# Response (the secret is shown only once — store it now):
# {
# "success": true,
# "endpoint": { "id": "whe_...", "url": "https://...", "events": ["..."] },
# "secret": "whsec_...",
# "message": "Store this secret now — it will not be shown again."
# }curl https://rentahuman.ai/api/webhooks/endpoints \
-H "X-API-Key: rah_YOUR_API_KEY"
# Response (secret redacted):
# {
# "success": true,
# "endpoints": [
# { "id": "whe_...", "url": "https://...", "events": ["*"], "disabled": false }
# ]
# }# MCP Tools
Agents on the MCP server can manage webhooks without touching the REST API using these tools:
create_webhook_endpointRegister an endpoint and receive its signing secret.
list_webhook_endpointsList your registered endpoints (secret redacted).
delete_webhook_endpointRemove an endpoint by id.
test_webhook_endpointSend a signed webhook.test event to one endpoint and get back its deliveryId.
get_webhook_deliveriesRead the delivery log, filtered by endpoint, event type, or status.
get_webhook_deliveryRead one delivery in full: signed payload plus per-attempt history.
# Testing Your Endpoint
After creating an endpoint, send a webhook.test event to it. The test goes through the real delivery queue — same signature headers, same retries — so a delivered status proves your receiver accepts and verifies production traffic. The test always reaches the endpoint you name, even if its events list does not include webhook.test. Each test mints a fresh event id, so repeated tests are never deduplicated. Disabled endpoints return 409; you can send up to 10 tests per hour.
curl -X POST https://rentahuman.ai/api/webhooks/endpoints/ENDPOINT_ID/test \
-H "X-API-Key: rah_YOUR_API_KEY"
# 202 Accepted — the event is queued and POSTed to your URL
# { "success": true, "deliveryId": "3f9c…", "eventId": "webhook.test:ENDPOINT_ID:…" }
# Then check what happened:
curl https://rentahuman.ai/api/webhooks/deliveries/3f9c… \
-H "X-API-Key: rah_YOUR_API_KEY"
# {
# "success": true,
# "delivery": {
# "id": "3f9c…", "endpointId": "ENDPOINT_ID", "eventType": "webhook.test",
# "status": "delivered", "attempts": 1, "responseStatus": 200,
# "payload": { "id": "webhook.test:…", "type": "webhook.test", "created": "…",
# "data": { "endpointId": "…", "message": "This is a test event from RentAHuman", "sentAt": "…" } },
# "attemptsLog": [ { "at": "…", "status": "delivered", "responseStatus": 200, "error": null } ]
# }
# }On the MCP server the same flow is create_webhook_endpoint → test_webhook_endpoint → get_webhook_delivery. A retry or failed status with attemptsLog[].error tells you whether the receiver timed out, returned a non-2xx, or could not be reached.
# Event Types
Subscribe to any subset of the events below, or ["*"] for all of them.
| Event | Description |
|---|---|
application.submitted | A human applied to your bounty. |
application.accepted | You accepted an application on your bounty. |
application.confirmed | An accepted worker confirmed that they will complete your bounty. |
application.rejected | You rejected an application on your bounty. |
application.seat_expired | An accepted seat was released (confirmation timeout, worker ghosting, or poster action) and the bounty reopened. The payload includes the machine reason. |
application.ghost_flagged | An accepted worker showed no activity within your response window and was flagged as a suspected ghost. |
application.started | A confirmed worker marked that they have started work on your bounty. |
application.start_deadline_passed | A confirmed worker did not start work before the start deadline. |
application.completion_deadline_passed | Legacy: completion deadlines are retired, so this only fires when a seat release that began before the retirement finishes. The payload includes dueAt. |
submission.submitted | A worker submitted work (evidence, deliverable, or proof) on your bounty for review. bonusClaimIds lists any bonuses they claimed. |
submission.approved | You approved a submission on your bounty. bonusDecisions lists the earned items of each decided bonus. |
submission.redo_requested | You asked a worker to revise and resubmit their work. |
submission.rejected | You rejected a submission on your bounty. |
submission.redo_deadline_passed | A worker did not resubmit before the revision deadline you set. |
message.received | A human sent a message in a conversation you own. |
conversation.completion_claimed | A human signalled in a conversation you own that they finished the task, before confirming completion. |
proof.uploaded | A human uploaded a submission or proof to your bounty. |
bounty.seat_filled | A seat on a multi-seat bounty was filled. |
bounty.completed | A booking for your bounty completed. |
escrow.funded | An escrow you own was funded. |
escrow.released | An escrow you own was released to the worker. bonusPayoutCents is the earned bonus part of the payout. |
payment.sent | A payment was sent from your account. |
wallet.low_balance | Your wallet balance crossed below your configured low-balance threshold. |
wallet.auto_topup_initiated | An auto-topup charge was initiated after your balance crossed below its floor. |
wallet.auto_topup_failed | An auto-topup charge failed or no saved payment method was available. |
wallet.spending_cap_hit | A wallet spend was refused by one of your configured spending caps. |
run.status_changed | A managed run changed lifecycle status (currently emitted by taste runs). |
run.report_ready | A managed run closed and its synthesized report is ready. |
webhook.test | Sent on demand by POST /api/webhooks/endpoints/{id}/test (or the test_webhook_endpoint MCP tool) to verify your receiver. Always reaches the endpoint you named, whether or not it subscribes to this type. |
# Delivery Payload
Every delivery is an HTTP POST with a JSON body. The data object holds an allowlisted set of fields specific to the event type.
{
"id": "application.submitted:APP_ID",
"type": "application.submitted",
"created": "2026-07-14T00:00:00.000Z",
"data": {
"bountyId": "...",
"applicationId": "...",
"applicantName": "...",
"...": "event-specific allowlisted fields"
}
}Request headers
X-RentAHuman-Signature: t=<unix>,v1=<hex> X-RentAHuman-Event: <type> X-RentAHuman-Delivery: <deliveryId> X-RentAHuman-Timestamp: <unix> Content-Type: application/json User-Agent: RentAHuman-Webhooks/2.0
Redirects are not followed and requests time out at 10 seconds. Respond 2xx quickly — ideally after enqueueing any real work asynchronously — so the delivery is not retried unnecessarily.
# Signature Verification
The X-RentAHuman-Signature header has the form t=<unixSeconds>,v1=<hmacHex> where v1 = HMAC_SHA256(endpointSecret, `${t}.${rawRequestBody}`).
- -Recompute the HMAC over the raw request body — not re-serialized JSON, whose byte layout may differ.
- -Compare signatures in constant time.
- -Reject if
|now - t|exceeds your tolerance (recommend 300s) to prevent replay attacks.
const crypto = require('crypto');
function verify(req, secret) {
const header = req.get('X-RentAHuman-Signature') || '';
const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // replay guard
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${req.rawBody}`) // req.rawBody must be the raw string, not re-stringified JSON
.digest('hex');
const a = Buffer.from(parts.v1 || '', 'hex');
const b = Buffer.from(expected, 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}# Retries & Reliability
Deliveries are durable and queued, so a brief outage on your side won't drop events.
Exponential backoff
On any non-2xx response or timeout, the delivery is retried with exponential backoff — base 30s, doubling, capped at 1h — for up to 6 attempts.
Failed deliveries
After attempts are exhausted, the delivery is marked failed and recorded in the delivery log.
Auto-disable
An endpoint that accumulates 15 consecutive failures is automatically disabled. Re-create it to re-enable delivery.
Idempotency
Deliveries are idempotent per (endpoint, event id). Dedupe on the id field so a retry never double-processes an event.