WeBot API docs

API quickstart

Use the public catalog to choose a service, create an API key, submit an account-owned job, attach files through presign, and poll status with idempotent authenticated calls.

1. Read the catalog

The catalog route is public and returns service family IDs, service option IDs, intake_requirements, quality_signals, and customer-safe job statuses. Agent callers should ask for missing intake requirements before creating work.

curl https://api.webot.agency/api/v1/catalog

2. Check intent before spending quota

Use the public /api/v1/catalog/intent-check route before job creation when an agent is still gathering context. It does not create a job, consume quota, attach files, or grant access.

curl https://api.webot.agency/api/v1/catalog/intent-check \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "service_family": "research_insights",
    "service_type": "comparison_briefs",
    "customer_title": "CRM comparison brief",
    "customer_instructions": "Question to answer: which CRM fits a two-person studio. Decision it supports: choosing a vendor next month. Source preferences: official pricing pages. Depth and deadline: a quick brief by Friday."
  }'

{
  "intent_check": {
    "ready": true,
    "missing_intake_requirements": [],
    "quality_signals": ["Claim/source separation", "Citation coverage", "Comparable criteria", "Decision-ready summary"]
  }
}

3. Set credentials

Use placeholder tokens in examples. Real API key plaintext is shown once at create time; never paste real keys into source files, logs, tickets, or shared screenshots.

export WEBOT_API_KEY="wbot_example_prefix_example_token"

4. Check account state

curl https://api.webot.agency/api/v1/me \
  -H "Authorization: Bearer $WEBOT_API_KEY"

curl https://api.webot.agency/api/v1/entitlements \
  -H "Authorization: Bearer $WEBOT_API_KEY"

curl "https://api.webot.agency/api/v1/usage?period_start=2026-06-01T00:00:00.000Z" \
  -H "Authorization: Bearer $WEBOT_API_KEY"

5. Preflight account readiness

After the public intent check, use the authenticated preflight route to verify active access, remaining quota, active-job capacity, and rate-limit policy before sending a create request. This route does not create a job, write usage, consume rate counters, or attach files.

curl https://api.webot.agency/api/v1/jobs/preflight \
  -X POST \
  -H "Authorization: Bearer $WEBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "service_family": "research_insights",
    "service_type": "comparison_briefs",
    "customer_title": "CRM comparison brief",
    "customer_instructions": "Question to answer: which CRM fits a two-person studio. Decision it supports: choosing a vendor next month. Source preferences: official pricing pages. Depth and deadline: a quick brief by Friday."
  }'

{
  "preflight": {
    "ready_to_create": true,
    "blocking_reasons": [],
    "usage": { "used": 0, "quota": 500, "remaining": 500, "unit": "job" },
    "concurrency": { "active_jobs": 0, "limit": 5, "remaining": 5, "allowed": true },
    "rate_limit_policy": {
      "route": "/api/v1/jobs",
      "account": { "limit": 180, "window_seconds": 60 },
      "checked_without_consuming": true
    }
  }
}

6. Create a job

Mutating requests should include Idempotency-Key. The server owns account ID, quota checks, status, usage writes, and job IDs. Put the answered catalog intake requirements into customer_instructions so fulfillment has the customer context it needs.

curl https://api.webot.agency/api/v1/jobs \
  -X POST \
  -H "Authorization: Bearer $WEBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: job-create-2026-06-28-001" \
  -d '{
    "service_family": "voice_audio",
    "service_type": "text_to_speech",
    "customer_title": "Welcome message voice-over",
    "customer_instructions": "Script: Nice to finally meet you. We are glad you are here. Voice and tone: warm and natural. Output format: MP3. Usage context: plays on the welcome page."
  }'

{
  "job": {
    "id": "job_example",
    "status": "queued",
    "links": {
      "self": "/api/v1/jobs/job_example",
      "events": "/api/v1/jobs/job_example/events",
      "approve": "/api/v1/jobs/job_example/approve",
      "cancel": "/api/v1/jobs/job_example/cancel",
      "revision": "/api/v1/jobs/job_example/revision"
    }
  },
  "next_action": "Check job status or add source files if needed."
}

7. Attach a file

Presign returns a short-lived signed upload contract and a customer-safe file record. Use the returnedupload.url and upload.headers exactly as provided, then call complete after the upload succeeds. The upload URL is not a durable public URL.

curl https://api.webot.agency/api/v1/files/presign \
  -X POST \
  -H "Authorization: Bearer $WEBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "purpose": "job_source",
    "content_type": "application/pdf",
    "size_bytes": 1048576,
    "file_name": "source-notes.pdf",
    "job_id": "job_example",
    "retention_days": 7
  }'

{
  "file": {
    "id": "file_example",
    "job_id": "job_example",
    "purpose": "job_source",
    "file_name": "source-notes.pdf",
    "content_type": "application/pdf",
    "size_bytes": 1048576,
    "status": "pending_upload"
  },
  "upload": {
    "method": "PUT",
    "url": "SIGNED_UPLOAD_URL_FROM_RESPONSE",
    "expires_at": "2026-06-28T12:15:00.000Z",
    "headers": {
      "content-type": "application/pdf",
      "x-webot-max-size": "1048576"
    }
  }
}

curl "$SIGNED_UPLOAD_URL_FROM_RESPONSE" \
  -X PUT \
  -H "content-type: application/pdf" \
  -H "x-webot-max-size: 1048576" \
  --data-binary @source-notes.pdf

curl https://api.webot.agency/api/v1/files/file_example/complete \
  -X POST \
  -H "Authorization: Bearer $WEBOT_API_KEY"

8. Poll status and events

Poll the job resource for current status and the events resource for customer-safe history. Stop polling once the job reaches completed, rejected, or canceled.

List jobs

curl "https://api.webot.agency/api/v1/jobs?status=queued,running&limit=25" \
  -H "Authorization: Bearer $WEBOT_API_KEY"

Read job

curl https://api.webot.agency/api/v1/jobs/job_example \
  -H "Authorization: Bearer $WEBOT_API_KEY"

Read events

curl https://api.webot.agency/api/v1/jobs/job_example/events \
  -H "Authorization: Bearer $WEBOT_API_KEY"

9. Approve, cancel, or revise

Approve delivery

curl https://api.webot.agency/api/v1/jobs/job_example/approve \
  -X POST \
  -H "Authorization: Bearer $WEBOT_API_KEY" \
  -H "Idempotency-Key: job-approve-2026-06-28-001"

Cancel job

curl https://api.webot.agency/api/v1/jobs/job_example/cancel \
  -X POST \
  -H "Authorization: Bearer $WEBOT_API_KEY" \
  -H "Idempotency-Key: job-cancel-2026-06-28-001"

Request revision

curl https://api.webot.agency/api/v1/jobs/job_example/revision \
  -X POST \
  -H "Authorization: Bearer $WEBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: job-revision-2026-06-28-001" \
  -d '{ "customer_instructions": "Make the tone more direct and shorten the opening." }'

TypeScript client shape

type WebotClientOptions = {
  apiKey: string;
  baseUrl?: string;
};

class WebotClient {
  private readonly baseUrl: string;
  private readonly apiKey: string;

  constructor(options: WebotClientOptions) {
    this.baseUrl = options.baseUrl ?? "https://api.webot.agency";
    this.apiKey = options.apiKey;
  }

  async createFilePresign(body: {
    purpose: "job_source" | "job_reference" | "job_deliverable";
    content_type: string;
    size_bytes: number;
    file_name: string;
    job_id?: string;
  }) {
    const response = await fetch(this.baseUrl + "/api/v1/files/presign", {
      method: "POST",
      headers: {
        Authorization: "Bearer " + this.apiKey,
        "Content-Type": "application/json"
      },
      body: JSON.stringify(body)
    });

    if (!response.ok) {
      throw await response.json();
    }

    return response.json();
  }

  async completeFileUpload(fileId: string) {
    const response = await fetch(this.baseUrl + "/api/v1/files/" + encodeURIComponent(fileId) + "/complete", {
      method: "POST",
      headers: {
        Authorization: "Bearer " + this.apiKey
      }
    });

    if (!response.ok) {
      throw await response.json();
    }

    return response.json();
  }

  async getJob(id: string) {
    const response = await fetch(this.baseUrl + "/api/v1/jobs/" + encodeURIComponent(id), {
      headers: {
        Authorization: "Bearer " + this.apiKey
      }
    });

    if (!response.ok) {
      throw await response.json();
    }

    return response.json();
  }

  async listJobs(params: { status?: string; service_family?: string; cursor?: string; limit?: number } = {}) {
    const search = new URLSearchParams();

    for (const [key, value] of Object.entries(params)) {
      if (value !== undefined) {
        search.set(key, String(value));
      }
    }

    const response = await fetch(this.baseUrl + "/api/v1/jobs?" + search.toString(), {
      headers: {
        Authorization: "Bearer " + this.apiKey
      }
    });

    if (!response.ok) {
      throw await response.json();
    }

    return response.json();
  }

  async preflightJob(body: {
    service_family: string;
    service_type: string;
    customer_title: string;
    customer_instructions: string;
  }) {
    const response = await fetch(this.baseUrl + "/api/v1/jobs/preflight", {
      method: "POST",
      headers: {
        Authorization: "Bearer " + this.apiKey,
        "Content-Type": "application/json"
      },
      body: JSON.stringify(body)
    });

    if (!response.ok) {
      throw await response.json();
    }

    return response.json();
  }

  async createJob(body: {
    service_family: string;
    service_type: string;
    customer_title: string;
    customer_instructions: string;
  }) {
    const response = await fetch(this.baseUrl + "/api/v1/jobs", {
      method: "POST",
      headers: {
        Authorization: "Bearer " + this.apiKey,
        "Content-Type": "application/json",
        "Idempotency-Key": crypto.randomUUID()
      },
      body: JSON.stringify(body)
    });

    if (!response.ok) {
      throw await response.json();
    }

    return response.json();
  }
}