Documentation

BotEsq API Documentation

Integrate licensed legal services into your AI agents using the Model Context Protocol (MCP). BotEsq provides a secure, compliant way for AI systems to access real legal expertise.

What is BotEsq?

BotEsq is trust infrastructure for AI agents. It provides two complementary products: secure agent-to-agent transactions (Resolve) and licensed legal services (Legal). Both integrate seamlessly with the Model Context Protocol (MCP).

🤝BotEsq Resolve

Free transaction infrastructure for agent-to-agent commerce. Escrow, trust scores, and automated dispute resolution.

Free for most use cases

⚖️BotEsq Legal

Professional legal services for AI agents. Legal Q&A, document review, and attorney consultations.

Pay-per-use credits

When to Use Each

  • Agent-to-agent transactions? Use Resolve for escrow, trust scores, and dispute handling
  • Legal questions or document review? Use Legal for AI-powered answers with attorney verification
  • Dispute needs human review? Resolve escalates to Legal automatically — pay only when needed

How It Works

  1. Get an API Key - Sign up for an operator account and generate API keys
  2. Start a Session - Use the start_session tool to authenticate
  3. Use Legal Tools - Call any of our 16 MCP tools for legal services
  4. Pay with Credits - Credits are deducted automatically based on usage

Pricing

BotEsq uses a credit-based system. Service costs vary based on complexity, urgency, and scope. When you submit a request, the system returns the exact credit cost before processing.

How It Works

  1. Submit a request — Call the appropriate MCP tool with your request details
  2. Receive a quote — The response includes the credit cost for that specific request
  3. Confirm or cancel — Credits are only deducted when you proceed with the service

Free tools: Session management, information retrieval, and status checks are always free. Paid services (legal Q&A, consultations, document review) return pricing in the response.

Webhooks

Receive real-time notifications when async operations complete. Configure your webhook URL in the operator portal under Settings → Webhooks.

Security Requirement

Webhook URLs must use HTTPS to protect sensitive legal data in transit. HTTP is only permitted for local development (localhost, 127.0.0.1).

Webhook Events

EventDescription
consultation.completedAttorney has submitted a response to your consultation
document.analysis_completedDocument analysis has finished

Payload Format

All webhooks are sent as HTTP POST requests with a JSON body:

{
  "event": "consultation.completed",
  "timestamp": "2026-02-04T12:34:56.789Z",
  "data": {
    "consultation_id": "CONS-ABC12345",
    "matter_id": "MTR-XYZ98765",
    "status": "completed",
    "question": "What are the requirements for...",
    "response": "Based on applicable law...",
    "attorney_reviewed": true,
    "completed_at": "2026-02-04T12:34:56.789Z"
  }
}

Verifying Signatures

All webhooks include a signature for verification. Check these headers:

  • X-BotEsq-Signature - HMAC-SHA256 signature
  • X-BotEsq-Timestamp - Unix timestamp when sent

Verify the signature like this:

// Node.js / TypeScript
import crypto from 'crypto'

function verifySignature(
  payload: string,
  signature: string,
  timestamp: string,
  secret: string
): boolean {
  // Reject old timestamps (> 5 minutes)
  const age = Date.now() / 1000 - parseInt(timestamp)
  if (age > 300) return false

  // Verify signature
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${payload}`)
    .digest('hex')

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  )
}

Example Handler (Node.js)

// Express.js webhook handler
import express from 'express'
import crypto from 'crypto'

const app = express()
const WEBHOOK_SECRET = process.env.BOTESQ_WEBHOOK_SECRET

// Store pending consultations (use Redis/database in production)
const pendingConsultations = new Map()

app.post('/webhooks/botesq', express.json(), (req, res) => {
  const signature = req.headers['x-botesq-signature']
  const timestamp = req.headers['x-botesq-timestamp']
  const payload = JSON.stringify(req.body)

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

  const { event, data } = req.body

  switch (event) {
    case 'consultation.completed':
      // Store the response for the agent to retrieve
      pendingConsultations.set(data.consultation_id, {
        response: data.response,
        completedAt: data.completed_at,
        attorneyReviewed: data.attorney_reviewed
      })
      console.log(`Consultation ${data.consultation_id} completed`)
      break

    case 'document.analysis_completed':
      console.log(`Document ${data.document_id} analysis ready`)
      break
  }

  res.json({ received: true })
})

// Endpoint for your agent to check consultation status
app.get('/consultations/:id/result', (req, res) => {
  const result = pendingConsultations.get(req.params.id)
  if (result) {
    res.json({ status: 'completed', ...result })
  } else {
    res.json({ status: 'pending' })
  }
})

Example Handler (Python)

# FastAPI webhook handler
import hmac
import hashlib
import time
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()
WEBHOOK_SECRET = os.environ["BOTESQ_WEBHOOK_SECRET"]

# Store results (use Redis/database in production)
consultation_results = {}

def verify_signature(payload: str, signature: str, timestamp: str) -> bool:
    # Reject old timestamps (> 5 minutes)
    if time.time() - int(timestamp) > 300:
        return False

    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        f"{timestamp}.{payload}".encode(),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected)

@app.post("/webhooks/botesq")
async def handle_webhook(request: Request):
    signature = request.headers.get("x-botesq-signature")
    timestamp = request.headers.get("x-botesq-timestamp")
    payload = await request.body()

    if not verify_signature(payload.decode(), signature, timestamp):
        raise HTTPException(status_code=401, detail="Invalid signature")

    data = await request.json()
    event = data["event"]

    if event == "consultation.completed":
        consultation_id = data["data"]["consultation_id"]
        consultation_results[consultation_id] = {
            "response": data["data"]["response"],
            "completed_at": data["data"]["completed_at"],
            "attorney_reviewed": data["data"]["attorney_reviewed"]
        }
        print(f"Consultation {consultation_id} completed")

    return {"received": True}

# Your agent polls this endpoint or you push to it
@app.get("/consultations/{consultation_id}/result")
async def get_result(consultation_id: str):
    if consultation_id in consultation_results:
        return {"status": "completed", **consultation_results[consultation_id]}
    return {"status": "pending"}

Agent Integration Pattern

Here's how your AI agent can use webhooks for async consultations:

# Python agent example using BotEsq MCP
import asyncio
import httpx

class LegalAgent:
    def __init__(self, mcp_client, webhook_service_url):
        self.mcp = mcp_client
        self.webhook_url = webhook_service_url

    async def request_legal_consultation(self, matter_id: str, question: str):
        # 1. Submit consultation request via MCP
        result = await self.mcp.call_tool("request_consultation", {
            "matter_id": matter_id,
            "question": question,
            "priority": "standard"
        })

        consultation_id = result["consultation_id"]
        print(f"Consultation submitted: {consultation_id}")
        print("Waiting for attorney response...")

        # 2. Poll your webhook service for the result
        # (The webhook will populate this when BotEsq sends notification)
        async with httpx.AsyncClient() as client:
            while True:
                response = await client.get(
                    f"{self.webhook_url}/consultations/{consultation_id}/result"
                )
                data = response.json()

                if data["status"] == "completed":
                    return {
                        "consultation_id": consultation_id,
                        "response": data["response"],
                        "attorney_reviewed": data["attorney_reviewed"]
                    }

                # Wait before polling again
                await asyncio.sleep(30)

# Usage
agent = LegalAgent(mcp_client, "https://your-service.com")
result = await agent.request_legal_consultation(
    matter_id="MTR-ABC123",
    question="What are the legal requirements for..."
)
print(f"Attorney response: {result['response']}")

Polling Without Webhooks

If you prefer not to set up webhooks, you can poll BotEsq directly using the get_consultation_result MCP tool. The consultation_id returned when you submit a request is used to retrieve results.

How It Works

  1. Call request_consultation → receive a consultation_id
  2. Store the consultation_id in your agent's state
  3. Periodically call get_consultation_result with that ID
  4. When status is completed, the response is included

Polling Example (Python)

import asyncio

class LegalAgent:
    def __init__(self, mcp_client):
        self.mcp = mcp_client
        self.session_token = None

    async def start(self, api_key: str):
        """Initialize session with BotEsq"""
        result = await self.mcp.call_tool("start_session", {
            "api_key": api_key
        })
        self.session_token = result["session_token"]

    async def request_consultation(self, matter_id: str, question: str):
        """Submit a consultation and poll for the result"""

        # 1. Submit the consultation request
        result = await self.mcp.call_tool("request_consultation", {
            "session_token": self.session_token,
            "matter_id": matter_id,
            "question": question,
            "priority": "standard"
        })

        consultation_id = result["consultation_id"]
        print(f"Consultation submitted: {consultation_id}")
        print(f"Estimated wait: {result.get('estimated_wait_minutes', 'unknown')} minutes")

        # 2. Poll for the result using the consultation_id
        while True:
            status = await self.mcp.call_tool("get_consultation_result", {
                "session_token": self.session_token,
                "consultation_id": consultation_id
            })

            if status["status"] == "completed":
                return {
                    "consultation_id": consultation_id,
                    "response": status["response"],
                    "attorney_reviewed": status["attorney_reviewed"],
                    "citations": status.get("citations", [])
                }

            print(f"Status: {status['status']}, waiting...")
            await asyncio.sleep(60)  # Poll every 60 seconds

# Usage
async def main():
    agent = LegalAgent(mcp_client)
    await agent.start("be_your_api_key_here")

    result = await agent.request_consultation(
        matter_id="MTR-ABC123",
        question="What are the legal requirements for forming an LLC in Delaware?"
    )

    print(f"Attorney response: {result['response']}")
    print(f"Reviewed by attorney: {result['attorney_reviewed']}")

asyncio.run(main())

Polling Example (TypeScript)

class LegalAgent {
  private mcp: MCPClient
  private sessionToken: string | null = null

  constructor(mcpClient: MCPClient) {
    this.mcp = mcpClient
  }

  async start(apiKey: string): Promise<void> {
    const result = await this.mcp.callTool('start_session', { api_key: apiKey })
    this.sessionToken = result.session_token
  }

  async requestConsultation(matterId: string, question: string): Promise<{
    consultationId: string
    response: string
    attorneyReviewed: boolean
  }> {
    // 1. Submit consultation
    const submitResult = await this.mcp.callTool('request_consultation', {
      session_token: this.sessionToken,
      matter_id: matterId,
      question,
      priority: 'standard'
    })

    const consultationId = submitResult.consultation_id
    console.log(`Consultation submitted: ${consultationId}`)

    // 2. Poll until complete
    while (true) {
      const status = await this.mcp.callTool('get_consultation_result', {
        session_token: this.sessionToken,
        consultation_id: consultationId
      })

      if (status.status === 'completed') {
        return {
          consultationId,
          response: status.response,
          attorneyReviewed: status.attorney_reviewed
        }
      }

      console.log(`Status: ${status.status}, waiting...`)
      await new Promise(resolve => setTimeout(resolve, 60000))
    }
  }
}

// Usage
const agent = new LegalAgent(mcpClient)
await agent.start('be_your_api_key_here')

const result = await agent.requestConsultation(
  'MTR-ABC123',
  'What are the legal requirements for forming an LLC in Delaware?'
)
console.log(`Response: ${result.response}`)

Polling Best Practices

  • Poll no more frequently than once per minute to avoid rate limits
  • Use the estimated_wait_minutes field to optimize polling frequency
  • Implement exponential backoff for long-running consultations
  • Store consultation IDs persistently if your agent may restart
  • Consider webhooks for production systems to reduce latency and API calls