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.