Asynchronous Agent Interactions
Some agent tasks take longer than a single HTTP connection can reliably hold open. Instead of keeping a connection alive for minutes while your agent works, you give Scout a callback URL. Scout fires off the task, returns immediately, and POSTs the result to your endpoint when it’s done.
When to Use Async
- Tasks that run longer than 30 seconds (order processing, report generation, bulk data workflows)
- Environments where connections drop or time out unpredictably
- Any workflow where you need to queue tasks and handle results separately
How It Works
- You provide a
callback_urlwhen starting the interaction - Scout responds immediately with
202 Acceptedand asession_id - The agent runs in the background
- When complete, Scout POSTs to your callback URL with the result
API Reference
Start Async Interaction
Request
POST /v1/agents/{agent_id}/interact
Content-Type: application/json
{
"message": "Process all pending orders and send confirmation emails",
"callback_url": "https://your-app.com/webhooks/scout-callback"
}Response (202 Accepted)
{
"session_id": "sess_abc123",
"events_url": "https://api.scoutos.com/v1/agent-sessions/sess_abc123/events"
}Callback Payload
When the agent completes, Scout POSTs to your callback_url:
Success
{
"callback_event_id": "2b2f5b7d-7a5d-4f9b-9f6d-8ed0d2c7a1d2",
"session_id": "sess_abc123",
"status": "succeeded",
"completed_at": "2026-03-05T14:30:00Z",
"events_url": "https://api.scoutos.com/v1/agent-sessions/sess_abc123/events"
}Failure
{
"callback_event_id": "2b2f5b7d-7a5d-4f9b-9f6d-8ed0d2c7a1d2",
"session_id": "sess_abc123",
"status": "failed",
"completed_at": "2026-03-05T14:30:00Z",
"events_url": "https://api.scoutos.com/v1/agent-sessions/sess_abc123/events",
"error": {
"code": "EXECUTION_ERROR",
"message": "Agent exceeded maximum step count"
}
}Fetching Results
Use the events_url to retrieve the full event stream:
GET https://api.scoutos.com/v1/agent-sessions/sess_abc123/events
Authorization: Bearer your-api-keyCallback Authentication
Every callback includes signature headers so you can confirm it came from Scout:
X-Scout-Signature-Alg: HMAC-SHA256
X-Scout-Signature: t=1709651400,sig=base64-encoded-signatureVerifying the Signature
- Extract
t(timestamp) andsig(signature) from the header - Build the signature base string:
{timestamp}.{raw_request_body} - Compute HMAC-SHA256 using your org secret key
- Compare your result to
sigusing a constant-time comparison
Here’s how to do it in Python and Node.js:
Python
import hmac
import hashlib
import base64
def verify_scout_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
# Parse the header: t=1709651400,sig=base64...
parts = dict(item.split("=", 1) for item in signature_header.split(","))
timestamp = parts.get("t", "")
provided_sig = parts.get("sig", "")
# Build the signed string
signed_string = f"{timestamp}.{raw_body.decode('utf-8')}"
# Compute HMAC-SHA256
expected = hmac.new(
secret.encode("utf-8"),
signed_string.encode("utf-8"),
hashlib.sha256
).digest()
expected_b64 = base64.b64encode(expected).decode("utf-8")
# Constant-time comparison prevents timing attacks
return hmac.compare_digest(expected_b64, provided_sig)Node.js
const crypto = require("crypto");
function verifyScoutSignature(rawBody, signatureHeader, secret) {
// Parse the header: t=1709651400,sig=base64...
const parts = Object.fromEntries(
signatureHeader.split(",").map((p) => p.split("=", 2))
);
const timestamp = parts.t ?? "";
const providedSig = parts.sig ?? "";
// Build the signed string
const signedString = `${timestamp}.${rawBody}`;
// Compute HMAC-SHA256
const expected = crypto
.createHmac("sha256", secret)
.update(signedString)
.digest("base64");
// Constant-time comparison prevents timing attacks
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(providedSig)
);
}Example Webhook Handler (Express)
A minimal endpoint that verifies the signature and handles the callback:
const express = require("express");
const crypto = require("crypto");
const app = express();
// Use raw body for signature verification
app.post("/webhooks/scout-callback", express.raw({ type: "application/json" }), (req, res) => {
const sigHeader = req.headers["x-scout-signature"];
if (!verifyScoutSignature(req.body, sigHeader, process.env.SCOUT_SECRET)) {
return res.status(401).send("Invalid signature");
}
const payload = JSON.parse(req.body);
// Deduplicate using callback_event_id
if (await alreadyProcessed(payload.callback_event_id)) {
return res.status(200).send("Already handled");
}
if (payload.status === "succeeded") {
// Fetch the full event stream if you need step-by-step details
await handleSuccess(payload.session_id, payload.events_url);
} else {
await handleFailure(payload.session_id, payload.error);
}
// Respond within 10 seconds or Scout will retry
res.status(200).send("OK");
});Important: Always parse the raw request body before calling
JSON.parse. If your framework parses JSON first, the body bytes change and the signature won’t match.
Retry Behavior
Scout uses at-least-once delivery, meaning the same callback can arrive more than once.
- Deduplication: Check
callback_event_idbefore processing to avoid duplicate work - Retry schedule: Exponential backoff over ~24 hours
- Retry triggers: Network errors or 5xx responses from your endpoint
Requirements
callback_urlmust be HTTPS- Private/internal URLs aren’t supported (SSRF protection)
- Your endpoint must respond within 10 seconds