> ## Documentation Index
> Fetch the complete documentation index at: https://docs.scoutos.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Asynchronous Agent Interactions: Run Long Tasks Without Blocking

> Run long-running Scout agent tasks without holding an HTTP connection open. Supply a callback URL, get a 202 and session_id immediately, and receive results via a signed webhook.

Some agent tasks run too long to hold an HTTP connection open. Instead of waiting, you supply Scout with a callback URL — Scout starts the task, returns immediately, and POSTs the result to your endpoint when the agent finishes.

## When to Use Async

* **Long-running tasks** — anything that runs longer than 30 seconds, such as order processing, report generation, or bulk data workflows
* **Unreliable connections** — environments where connections time out or drop before a synchronous response can return
* **Queued workflows** — cases where you enqueue tasks and handle their results separately

## How It Works

<Steps>
  <Step title="Start the interaction">
    Provide a `callback_url` when starting the interaction.
  </Step>

  <Step title="Get an immediate response">
    Scout responds right away with `202 Accepted` and a `session_id`.
  </Step>

  <Step title="The agent runs in the background">
    Scout runs the agent task without holding your connection open.
  </Step>

  <Step title="Receive the result">
    On completion, Scout POSTs the result to your callback URL.
  </Step>
</Steps>

## API Reference

### Start Async Interaction

Start an interaction that runs in the background and reports its result to a callback URL.

```text theme={null}
POST https://api.scoutos.com/v1/agents/{agent_id}/interact
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY
```

#### Parameters

<ParamField body="message" type="string" required>
  The instruction or input for the agent to act on.
</ParamField>

<ParamField body="callback_url" type="string" required>
  An HTTPS URL where Scout POSTs the result when the agent finishes. Must be publicly reachable — see [Requirements](#requirements).
</ParamField>

#### Request

```json theme={null}
{
  "message": "Process all pending orders and send confirmation emails",
  "callback_url": "https://your-app.com/webhooks/scout-callback"
}
```

#### Response

Scout returns `202 Accepted` immediately, before the agent runs.

<ResponseField name="session_id" type="string">
  The identifier for this agent session. Use it to correlate the callback with the request that started it.
</ResponseField>

<ResponseField name="events_url" type="string">
  The URL to fetch the full event stream for this session once it completes.
</ResponseField>

```json theme={null}
{
  "session_id": "sess_abc123",
  "events_url": "https://api.scoutos.com/v1/agent-sessions/sess_abc123/events"
}
```

### Callback Payload

When the agent finishes, Scout POSTs a JSON payload to your `callback_url`.

<ResponseField name="callback_event_id" type="string">
  A unique identifier for this callback delivery. Use it to deduplicate retries — see [Retry Behavior](#retry-behavior).
</ResponseField>

<ResponseField name="session_id" type="string">
  The session this callback reports on, matching the `session_id` from the original response.
</ResponseField>

<ResponseField name="status" type="string">
  Either `succeeded` or `failed`.
</ResponseField>

<ResponseField name="completed_at" type="string">
  ISO 8601 timestamp of when the agent finished.
</ResponseField>

<ResponseField name="events_url" type="string">
  The URL to fetch the full event stream for the session.
</ResponseField>

<ResponseField name="error" type="object">
  Present only when `status` is `failed`. Contains a `code` and a human-readable `message`.
</ResponseField>

<CodeGroup>
  ```json Success theme={null}
  {
    "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"
  }
  ```

  ```json Failure theme={null}
  {
    "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"
    }
  }
  ```
</CodeGroup>

### Fetching Results

The callback payload confirms completion but doesn't include the agent's full output. Use the `events_url` to retrieve the complete event stream:

```text theme={null}
GET https://api.scoutos.com/v1/agent-sessions/sess_abc123/events
Authorization: Bearer YOUR_API_KEY
```

## Callback Authentication

Every callback includes signature headers so you can confirm the request genuinely came from Scout:

```text theme={null}
X-Scout-Signature-Alg: HMAC-SHA256
X-Scout-Signature: t=1709651400,sig=base64-encoded-signature
```

### Verifying the Signature

<Steps>
  <Step title="Parse the header">
    Extract `t` (timestamp) and `sig` (signature) from the `X-Scout-Signature` header.
  </Step>

  <Step title="Build the signature base string">
    Concatenate the timestamp and the raw request body as `{timestamp}.{raw_request_body}`.
  </Step>

  <Step title="Compute the HMAC">
    Compute HMAC-SHA256 over the base string using your org secret key.
  </Step>

  <Step title="Compare">
    Compare your computed value against `sig` using a constant-time comparison.
  </Step>
</Steps>

<CodeGroup>
  ```python Python theme={null}
  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)
  ```

  ```javascript Node.js theme={null}
  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)
    );
  }
  ```
</CodeGroup>

### Example Webhook Handler (Express)

```javascript theme={null}
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" }), async (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");
});
```

<Warning>
  Always read the **raw** request body before calling `JSON.parse`. If the JSON is parsed and re-serialized first, the body bytes change and the signature won't match.
</Warning>

## Retry Behavior

Scout uses **at-least-once** delivery, so the same callback may arrive more than once.

* **Deduplication** — check `callback_event_id` before processing to avoid duplicate work
* **Retry schedule** — exponential backoff over roughly 24 hours
* **Retry triggers** — network errors or `5xx` responses from your endpoint

## Requirements

* `callback_url` must use HTTPS
* Private and internal URLs aren't supported (SSRF protection)
* Your endpoint must respond within 10 seconds
