Your agent kicks off a job that takes 8 minutes. The caller has two bad options: hold the HTTP connection open for 8 minutes (and watch it die to a proxy timeout at minute 6), or pollDocumentation Index
Fetch the complete documentation index at: https://docs.getbindu.com/llms.txt
Use this file to discover all available pages before exploring further.
tasks/get every few seconds (and discover that 95% of those calls return “still working”).
Webhooks fix it. The caller registers a URL once, then goes about their day. When the task transitions — working, input-required, completed, failed, and so on — Bindu POSTs a signed JSON event to that URL. One notification per real event. Zero wasted requests.
This follows the A2A Protocol push notification spec, so any A2A-compliant client can subscribe without custom code.
Use webhooks when a task may outlive a normal request timeout — minutes, hours, or days.
For sub-second responses, just keep the connection open and skip this entirely.
Push vs polling
Polling (tasks/get)
Client decides cadence. Wastes requests. Latency = poll interval. Works
everywhere, no inbound port required on the client.
Push (this doc)
Server decides cadence. One event per real state change. Sub-second latency.
Client must expose an HTTPS endpoint Bindu can reach.
| Property | Polling | Push |
|---|---|---|
| Requests per task | Many (one per poll) | One per state change |
| Worst-case latency | Poll interval | Network RTT |
| Client must accept inbound HTTP | No | Yes |
| Survives server restart | Trivially | Only if long_running=true |
| Auth direction | Client authenticates server | Server authenticates to client (Bearer token) |
How it works
Per-task or global
Register a webhook per task at
message/send, or set one global_webhook_url
on the manifest that catches everything else.Persistent
When the caller sets
long_running=true, the webhook config is written to
storage and reloaded on startup, so notifications survive restarts.SSRF-hardened
Bindu resolves the webhook hostname once, refuses private / loopback /
metadata IPs, and connects directly to the resolved address — no DNS-rebinding
window between validation and delivery.
Subscribe
Send
push_notification_config inline with message/send, or call
tasks/pushNotificationConfig/set later. Add long_running: true if you
need the subscription to survive a Bindu restart.Execute
ManifestWorker runs the task. Every state transition and every artifact
flush hits PushNotificationManager.Quick start
1. Declare the capability
WEBHOOK_URL and WEBHOOK_TOKEN environment variables auto-populate
global_webhook_url / global_webhook_token when push_notifications is
enabled, so you can keep secrets out of code.2. Send a task with a webhook
3. Receive events
If
capabilities.push_notifications is missing or False, every push RPC
returns JSON-RPC error -32005 (PushNotificationNotSupportedError) and no
events fire. Enable the capability first.Events Bindu actually emits
Bindu does not emit asubmitted event. submitted is the initial database state set when a task is accepted; the first webhook you ever receive is working. Every event is a JSON-RPC-free POST body — no envelope, just the event object.
Common envelope
Every event carries the same top-level fields:event_id— unique per emission; use it to deduplicate on the receiver.sequence— monotonically increasing per task, starting at 1. Use to detect out-of-order delivery.timestamp— ISO 8601, UTC, microsecond precision.
status-update — working
status-update — working
Emitted when the worker picks the task up and transitions out of
submitted.status-update — input-required / auth-required
status-update — input-required / auth-required
Emitted when the agent needs more from the user before it can continue.
The agent’s prompt is embedded inside Same shape applies for
status.message as an A2A Message
so operator-facing clients (like the Bindu inbox) can show it directly.auth-required and any future intermediate state
that passes a status_message through.artifact-update
artifact-update
Emitted for each artifact the task produces, fired after the task has been
persisted to storage (outbox pattern — the DB write commits before the
notification leaves, so the webhook never references state that isn’t yet
durable).JSON encoding uses
json.dumps(..., default=str), so UUIDs and datetimes
inside the artifact serialize as strings — your receiver should treat
artifact_id as a string, not a UUID type.status-update — completed / failed / canceled / rejected
status-update — completed / failed / canceled / rejected
Terminal events. Order is: all
final: true tells the receiver no more events are coming
for this task_id.artifact-update events first (for completed), then the
terminal status-update.States that can appear in a
status-update: working, input-required,
auth-required, completed, failed, canceled, rejected. Bindu also
supports extended states (payment-required, negotiation-bid-submitted,
etc.) — these emit the same envelope when the worker transitions to them.Headers sent on every POST
Registration paths
Inline (recommended)
Send
push_notification_config in the message/send configuration.
The subscription exists before the task starts, so no working event
can race past you.RPC after the fact
tasks/pushNotificationConfig/set — useful for late-binding a webhook
to an existing task, or rotating the URL/token mid-flight.Inline registration
Inline registration
Separate RPC registration
Separate RPC registration
TaskNotFound — Bindu does not leak that the task
exists.Persistence and fallback
Long-running tasks
When the caller setslong_running: true, PushNotificationManager calls
storage.save_webhook_config(task_id, config). On boot, initialize() calls
storage.load_all_webhook_configs() and reinstalls every subscription before
the worker pool starts accepting tasks.
Webhook precedence
get_effective_webhook_config(task_id) resolves in this order:
- Task-specific config registered for
task_id - Manifest
global_webhook_url(if set) None— no delivery, event is dropped silently
API reference
RPC methods
All four methods enforce caller ownership. If
caller_did does not match
the task owner stored at submission, the response is the same as a missing
task — Bindu refuses to leak existence.PushNotificationConfig
Delivery, retries, and failure handling
NotificationService lives in bindu/utils/notifications.py and handles every
outbound POST. It is not Kafka, not SNS, not a queue — it is a direct HTTP call
wrapped in the unified retry decorator.
| Knob | Value |
|---|---|
| Transport | HTTP/HTTPS POST |
| Timeout | 5 s connect + read |
| Max attempts | 3 |
| Backoff | Exponential, 0.5s → 5s |
| Retried on | 5xx, 429, connection errors, timeouts |
| Dropped on | 4xx (except 429) — logged and abandoned |
| After exhaustion | Logged, no DLQ, event is lost |
Why 4xx is dropped
A 4xx means “your request is broken” — replaying it will fail the same way. Bindu logs atWARNING and moves on rather than burning retry budget. 429
is the exception: it means “slow down,” so it gets the full retry treatment.
SSRF protection (server side, automatic)
Before every POST,validate_config does this:
- Parse the URL; require
httporhttpsscheme and a non-empty netloc. - Resolve the hostname via
socket.getaddrinfoonce. - Reject loopback / private / link-local / metadata addresses.
- Pass the resolved IP through to the connection layer. The HTTP client connects directly to that IP and uses the original hostname only for the TLS SNI / cert verification.
Receiver patterns
Verify the token (constant-time)
Dedupe by event_id, order by sequence
Node / Express receiver
Smoke-test with curl
Disabling
Droppush_notifications from capabilities (or set it to False) and all
four RPCs return JSON-RPC error -32005 (PushNotificationNotSupportedError).
No events fire. Callers should fall back to tasks/get.
To disable just the global fallback while leaving per-task webhooks intact,
unset global_webhook_url (and unset WEBHOOK_URL in the environment).
Troubleshooting
Webhook never receives anything
Webhook never receives anything
Confirm the capability flag is on:Confirm the subscription is registered:If
get returns Push notification configuration not found for task., the
inline push_notification_config in message/send was missing or the task
completed and the manager has dropped the entry.Webhooks vanish after a restart
Webhooks vanish after a restart
long_running: true is required for persistence. Without it the
subscription is in-memory only.Got a 401 from my own webhook
Got a 401 from my own webhook
Bindu sends exactly
Authorization: Bearer <token> — token verbatim, no
extra whitespace. The token you registered must match byte-for-byte.
Compare with hmac.compare_digest, not ==.Bindu refuses my webhook URL
Bindu refuses my webhook URL
validate_config rejects:- Non-
http/httpsschemes - Missing hostname
- Hostnames that resolve to loopback, private, link-local, or cloud-metadata IPs
127.0.0.1 or 10.x.x.x.Duplicate events
Duplicate events
Bindu emits one event per state change, but a 5xx-then-success retry can
cause the same
event_id to appear twice on the wire. Dedupe on
event_id and you are safe.Out-of-order events
Out-of-order events
artifact-update events fire before the terminal completed event by
design. Within a single TCP-bound task_id, follow sequence to detect
reordering caused by retries.Related
Retry policy
How the 3-attempt exponential backoff is wired and how to tune it.
Storage
Where
long_running=true actually persists webhook configs.Scheduler
The path between
message/send and the worker that fires events.A2A spec
The interop contract this implementation follows.