Skip to content
← prismbi.ai

Webhook agents ​

Audience: Tenant admin and integration engineers
Goal: Register a webhook receiver, bootstrap an Edge Agent, and implement a compatible trigger endpoint in your stack.

Webhook delivery is event-driven: PrismBI cloud POSTs a signed JSON payload to your HTTPS URL when a task is queued. Your receiver verifies the signature, returns HTTP 2xx immediately (prefer 202 Accepted), then runs a one-shot Edge Agent to execute the task and exit.

Portal setup ​

Prerequisite: set Edge agent delivery to Webhook on Agents (/agents). See Agent configuration.

1. Save a webhook receiver ​

  1. Open Agents (/agents).
  2. Under Webhook receivers, enter your Webhook URL (the full HTTPS path PrismBI cloud will POST to, for example https://hooks.example.com/prismbi/trigger).
  3. Optionally set a custom auth header if an API gateway or function host requires an extra key (for example x-api-key).
  4. Click Save webhook receiver.

Webhook receivers
Figure: Save a receiver URL, open its detail page from the grid, then generate tokens and run Test connection.

2. Generate token pair ​

  1. Open the receiver row to open its detail page (/agents/webhooks/:id).
  2. Click Generate token pair.
  3. Copy both secrets immediately — they are shown once:
    • vbw_ — webhook signing secret (verify triggers came from PrismBI cloud).
    • vbr_ — one-time bootstrap token (first agent registration only).

The bootstrap token is valid for 2 hours. Copy tokens to your receiver environment, then complete step 3 before expiry.

Store on your receiver host (names are conventions; map to your config system):

Portal tokenSuggested env varPurpose
vbw_…PRISM_WEBHOOK_SECRETHMAC verification of inbound POSTs
vbr_…PRISM_AGENT_TOKENFirst-run bootstrap only (until identity is persisted)

Restart your receiver process after updating secrets.

3. Test connection (bootstrap) ​

Click Test connection. The portal sends a WEBHOOK_BOOTSTRAP trigger — not a real query.

Your receiver should:

  1. Verify the signature.
  2. Return 202 immediately.
  3. Start a one-shot Edge Agent that registers with PrismBI cloud (no query is run).

Wait until the detail page shows the linked agent as registered, then run Explore queries.

Receiver responsibilities ​

Your webhook endpoint is a thin trigger layer. It does not need to implement SQL or planning logic — the Edge Agent does that after bootstrap.

ResponsibilityOwner
Sign and send triggersPrismBI cloud
Verify X-Prism-Signature, optional gateway authYour receiver
Fast HTTP acknowledgmentYour receiver
Run task / bootstrapEdge Agent (PRISM_RUN_MODE=once)
Persist vba_ identity across runsYour receiver (shared volume, secret store, or agent state path)

HTTP contract ​

Request ​

ItemValue
MethodPOST
URLExact Webhook URL saved in the portal
Content-Typeapplication/json
BodyUTF-8 JSON (see Payload)

Required headers (from PrismBI cloud):

HeaderDescription
X-Prism-TimestampUnix epoch milliseconds as a string (for example 1720000000123)
X-Prism-SignatureLowercase hex HMAC-SHA256 of timestamp + "." + rawBody

Optional header (portal configuration):

If you configured Auth header name and Auth header value on the receiver, PrismBI cloud sends that header on every dispatch (for example x-api-key: <your-gateway-key>). Your receiver must require it if your infrastructure expects it.

Signature verification ​

PrismBI cloud signs with your vbw_ secret:

message = <X-Prism-Timestamp> + "." + <raw request body as UTF-8>
signature = HMAC_SHA256(key=vbw_secret, message=message) as lowercase hex

Validation checklist:

  1. Reject missing X-Prism-Timestamp or X-Prism-Signature.
  2. Reject timestamps older than 5 minutes (300 seconds) — compare clock skew in absolute value.
  3. Compute expected signature over the raw body bytes (before JSON re-serialization).
  4. Compare with hmac.compare_digest / crypto.timingSafeEqual (constant-time).
  5. Return 401 if verification fails.

Payload ​

JSON object sent by PrismBI cloud:

FieldTypeAlways presentDescription
taskIdstring (UUID)YesTask identifier
tenantIdstring (UUID)YesTenant that queued the work
taskTypestringYesTask kind (see below)
coreUrlstringYesPrismBI cloud API URL the agent should call (coreUrl in payloads; may use host.docker.internal in local dev)
expiresAtstring (ISO-8601)YesTask expiry instant
agentTokenstringPlatform agent onlyBearer token for platform general-purpose agent; omitted for tenant webhook agents

Common taskType values:

taskTypeSpawns agent work?Purpose
WEBHOOK_BOOTSTRAPYesPortal Test connection — register agent, no query
WEBHOOK_PINGNoLegacy connectivity check (signature only)
QUERY_PLANYesExplore / Analyze query execution
TEST_CONNECTIONYesDatasource connection test
SCHEMA_DISCOVERYYesSchema snapshot job

For tenant webhook agents, do not expect agentToken in the payload. The Edge Agent uses PRISM_AGENT_TOKEN (vbr_ on first run) or persisted state from a prior bootstrap.

Response ​

StatusMeaning
2xx (prefer 202)Trigger accepted; PrismBI cloud records success
401Signature or auth header failed
4xx / 5xxDispatch failed — portal test shows error; tasks may retry via lease maintenance

Return JSON quickly. Do not hold the HTTP request open until the Edge Agent finishes the query — run the agent asynchronously (background thread, queue, or separate container invocation).

Example success body (your format may vary; status code matters most):

json
{
  "status": "success",
  "data": {
    "accepted": true,
    "taskId": "11111111-1111-1111-1111-111111111111"
  }
}

Reference implementations ​

Adapt routing, logging, and idempotency to your platform (API Gateway + Lambda, Cloud Run, Kubernetes Job, Azure Function, etc.).

python
import hashlib
import hmac
import time
from fastapi import FastAPI, HTTPException, Request, Response

WEBHOOK_SECRET = "vbw_..."  # from portal
MAX_SIGNATURE_AGE_SECONDS = 300

app = FastAPI()

def verify_signature(secret: str, timestamp: str, body: bytes, signature: str) -> bool:
    if not secret or not timestamp or not signature:
        return False
    try:
        age = abs(time.time() - int(timestamp) / 1000)
    except ValueError:
        return False
    if age > MAX_SIGNATURE_AGE_SECONDS:
        return False
    expected = hmac.new(
        secret.encode("utf-8"),
        f"{timestamp}.{body.decode('utf-8')}".encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

@app.post("/webhook/trigger")
async def trigger(request: Request, response: Response):
    body = await request.body()
    timestamp = request.headers.get("X-Prism-Timestamp", "")
    signature = request.headers.get("X-Prism-Signature", "")
    if not verify_signature(WEBHOOK_SECRET, timestamp, body, signature):
        raise HTTPException(status_code=401, detail="Invalid webhook signature")

    payload = await request.json()
    task_type = payload.get("taskType")

    if task_type == "WEBHOOK_PING":
        response.status_code = 202
        return {"status": "success", "data": {"accepted": True}}

    # WEBHOOK_BOOTSTRAP and real tasks: enqueue one-shot agent run (async).
    start_agent_async(payload)
    response.status_code = 202
    return {"status": "success", "data": {"accepted": True, "taskId": payload.get("taskId")}}
javascript
import crypto from 'node:crypto';
import express from 'express';

const WEBHOOK_SECRET = process.env.PRISM_WEBHOOK_SECRET;
const MAX_AGE_MS = 5 * 60 * 1000;
const app = express();

app.post(
  '/webhook/trigger',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const timestamp = req.header('X-Prism-Timestamp') ?? '';
    const signature = req.header('X-Prism-Signature') ?? '';
    const body = req.body.toString('utf8');

    if (!verifySignature(WEBHOOK_SECRET, timestamp, body, signature)) {
      return res.status(401).json({ status: 'error', message: 'Invalid webhook signature' });
    }

    const payload = JSON.parse(body);
    if (payload.taskType !== 'WEBHOOK_PING') {
      enqueueAgentRun(payload); // non-blocking
    }
    return res.status(202).json({ status: 'success', data: { accepted: true } });
  },
);

function verifySignature(secret, timestamp, body, signature) {
  if (!secret || !timestamp || !signature) return false;
  const age = Math.abs(Date.now() - Number(timestamp));
  if (Number.isNaN(age) || age > MAX_AGE_MS) return false;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${body}`)
    .digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
go
func verifySignature(secret, timestamp string, body []byte, signature string) bool {
    if secret == "" || timestamp == "" || signature == "" {
        return false
    }
    ms, err := strconv.ParseInt(timestamp, 10, 64)
    if err != nil {
        return false
    }
    if abs(time.Now().UnixMilli()-ms) > 300_000 {
        return false
    }
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(timestamp + "."))
    mac.Write(body)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(signature))
}

Starting the Edge Agent from your receiver ​

After verification, start a one-shot agent with:

VariableValue
PRISM_CORE_URLcoreUrl from the payload
PRISM_RUN_MODEonce
PRISM_AGENT_TOKENvbr_… on first bootstrap; omit after identity is persisted
PRISM_WEBHOOK_SECRETvbw_… (if the agent image embeds a receiver — optional for external receivers)

Persist agent identity between invocations (for example a Docker volume mounted at the agent state path) so later triggers reuse vba_ without sending vbr_ again.

Example local Docker invocation (adjust image and network for production):

bash
docker run --rm \
  -e PRISM_CORE_URL=https://api.prismbi.ai \
  -e PRISM_RUN_MODE=once \
  -e PRISM_AGENT_TOKEN="vbr_<from-portal>" \
  -v voicebi-edge-state:/var/lib/voicebi-edge \
  ghcr.io/voicebi/prismbi:0.1.0

Local development reference ​

PrismBI ships a docker-webhook simulator for local testing (not for production). It implements the same signature check and spawns one-shot agent containers. See the docker-webhook repository README in the VoiceBI workspace.

For local development, the portal may accept http://localhost or http://127.0.0.1 webhook URLs. Spawned agents may receive coreUrl with host.docker.internal so containers can reach PrismBI cloud on the host.

Troubleshooting ​

IssueWhat to try
Test connection fails (401)Confirm PRISM_WEBHOOK_SECRET matches portal vbw_; check clock skew
Bootstrap token expiredRegenerate token pair within 2 hours; update receiver env
Explore: no active webhook agentComplete Test connection until receiver shows bootstrapped
Agent container cannot reach PrismBI cloudRewrite localhost to host-reachable URL inside container network
Task stuck after 202Check agent logs; verify PRISM_RUN_MODE=once and persisted state volume

Conversational analytics for governed enterprise data.