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": "growth_market",
    "service_type": "seo",
    "customer_title": "Local SEO plan",
    "customer_instructions": "Ideal customer: dental offices. Offer maturity: new service. Market geography: Coconut Creek. Conversion goal: book consultation calls."
  }'

{
  "intent_check": {
    "ready": true,
    "missing_intake_requirements": [],
    "quality_signals": ["Audience fit", "Search-intent match", "Offer clarity", "Actionable next step"]
  }
}

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": "growth_market",
    "service_type": "seo",
    "customer_title": "Local SEO plan",
    "customer_instructions": "Ideal customer: dental offices. Offer maturity: new service. Market geography: Coconut Creek. Conversion goal: book consultation calls."
  }'

{
  "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": "create_polish",
    "service_type": "tone_rewrites_copy_polish",
    "customer_title": "Polish launch email",
    "customer_instructions": "Audience: small business owners. Tone: clear and friendly. Brand constraint: no hype. Delivery format: short launch email. Tighten this draft for a useful product announcement."
  }'

{
  "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();
  }
}