# X Authority Builder

> REST API and automation guide for X Authority Builder — an OfficeX app that bulk-generates expert Twitter/X posts using Gemini AI and schedules them with volunteer-based publishing. Supports both AI generation and manual mode (bring your own tweets for scheduling only). Use when: (1) Integrating with the X Authority Builder API, (2) Creating jobs to generate AI tweets, (3) Managing results, approvals, and scheduling, (4) Building automation around tweet generation and calendar publishing, (5) Understanding the credit/reservation billing model, (6) Working with volunteer magic links and proof submission, (7) Querying the calendar or scheduled posts, (8) Bringing your own pre-written tweets for scheduling and dispatch. Triggers: x authority builder, tweet generation, ai tweets, x scheduling, twitter content, authority building, bulk tweets, volunteer publishing, schedule tweets, content calendar, tweet approval, manual tweets, bring your own content.

- Skill: `officexapp/x-authority-builder` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add officexapp/x-authority-builder`
- Raw SKILL.md: https://api.skillmd.com/api/skills/officexapp/x-authority-builder/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: officexapp (https://skillmd.com/u/officexapp)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/officexapp/x-authority-builder

---


# X Authority Builder — API Reference

Grow high-authority X accounts reliably & consistently. Generate expert-level tweets with
Gemini AI, approve/edit them, schedule to a calendar, and dispatch volunteer magic links for
hands-free publishing. Or bring your own tweets and use the scheduling, calendar, webhooks,
and volunteer dispatch as a composable service.

## Quick Start

'''typescript
const API = "https://x-authority-builder-api.cloud.zoomgtm.com";
const TOKEN = btoa('{install_id}:{install_secret}'); // from OfficeX install webhook

const headers = {
"Content-Type": "application/json",
Authorization: 'Bearer {TOKEN}',
};

// 1. Create a job (generates 10 tweets by default)
const { data: job } = await fetch('{API}/jobs', {
method: "POST",
headers,
body: JSON.stringify({
content_prompt: "Expert insights on B2B SaaS growth strategies",
output_quantity: 10,
publish_mode: "MANUAL_REVIEW",
}),
}).then((r) => r.json());

// 2. Poll until complete (~30-90s)
let status = "PENDING";
while (status !== "COMPLETED" && status !== "FAILED") {
await new Promise((r) => setTimeout(r, 5000));
const { data } = await fetch('{API}/jobs/{job.job_id}', { headers }).then(
(r) => r.json()
);
status = data.status;
}

// 3. List generated tweets
const { data: results } = await fetch(
'{API}/jobs/{job.job_id}/results',
{ headers }
).then((r) => r.json());

// 4. Approve a tweet
await fetch('{API}/results/{results.items[0].result_id}?job_id={job.job_id}', {
method: "PATCH",
headers,
body: JSON.stringify({ approved: true }),
});

// 5. Auto-schedule (AI picks optimal time)
const { data: scheduled } = await fetch(
'{API}/results/{results.items[0].result_id}/auto-schedule?job_id={job.job_id}',
{ method: "POST", headers }
).then((r) => r.json());
'''

---

## Authentication

All authenticated endpoints require a Bearer token derived from OfficeX install credentials.

'''
Authorization: Bearer <base64(install_id:install_secret)>
'''

The 'install_id' and 'install_secret' are provided when a user installs the app via OfficeX.
The install webhook ('POST /webhooks/officex') returns these in 'agent_context.auth_token'.

---

## TypeScript Types

'''typescript
// ── Enums ──────────────────────────────────────────────────────

type JobStatus = "PENDING" | "PROCESSING" | "COMPLETED" | "FAILED";
type PublishMode = "MANUAL_REVIEW" | "AI_REVIEW" | "AUTO_APPROVED";
type GenerationMode = "AI" | "MANUAL";

// ── Core Entities ──────────────────────────────────────────────

interface Job {
job_id: string;
user_id: string;
status: JobStatus;
created_at: string;
updated_at: string;
title?: string;
content_prompt: string;
output_quantity: number;
instruction_prompt?: string;
generation_mode: GenerationMode;
publish_mode: PublishMode;
publish_prompt?: string;
schedule_prompt?: string;
on_job_finish_webhook?: string;
on_job_finish_email?: string;
on_schedule_webhook_default?: string;
on_schedule_email_default?: string;
on_proof_webhook_default?: string;
on_proof_email_default?: string;
destination_url?: string;
tracer?: string;
inbox_tracer?: string;
results_created: number;
results_approved: number;
credits_spent: number;
credits_reserved?: number;
reservation_id?: string;
notes?: string;
bookmarked?: boolean;
}

interface Result {
result_id: string;
job_id: string;
user_id: string;
tweet_text: string; // ≤280 chars
match_score: number; // 0–100
ai_analysis: string;
approval_decision?: string;
approved: boolean;
rejected: boolean;
user_notes?: string;
scheduled_datetime?: string;
scheduled: boolean;
on_schedule_email?: string;
on_schedule_webhook?: string;
on_proof_webhook?: string;
on_proof_email?: string;
destination_url?: string;
tracer?: string;
inbox_tracer?: string;
bookmarked?: boolean;
}

interface Scheduled {
scheduled_id: string;
result_id: string;
job_id: string;
user_id: string;
tweet_text: string;
status?: 'pending' | 'completed';
scheduled_datetime: string;
volunteer_magic_link?: string;
on_schedule_email?: string;
on_schedule_webhook?: string;
on_proof_webhook?: string;
on_proof_email?: string;
destination_url?: string;
instruction_prompt?: string;
fired: boolean;
fired_at?: string;
proof_url?: string;
proof_timestamp?: string;
reported_by?: string;
tracer?: string;
inbox_tracer?: string;
}

interface User {
user_id: string;
email: string;
created_at: string;
internal_credits: number;
}

// ── Request Bodies ─────────────────────────────────────────────

interface CreateJobRequest {
content_prompt: string; // REQUIRED
output_quantity?: number; // 1–100, default 10 (ignored in MANUAL mode)
generation_mode?: GenerationMode; // default "AI" — set "MANUAL" to skip AI generation
instruction_prompt?: string; // extra context for AI
publish_mode?: PublishMode; // default "MANUAL_REVIEW"
publish_prompt?: string; // AI review criteria (AI_REVIEW mode)
schedule_prompt?: string; // hints for auto-schedule AI
on_job_finish_webhook?: string; // POST on completion
on_job_finish_email?: string; // email on completion
on_schedule_webhook_default?: string; // default for each result
on_schedule_email_default?: string; // default for each result
on_proof_webhook_default?: string; // default for each result
on_proof_email_default?: string; // default for each result
destination_url?: string; // X profile or thread URL
tracer?: string; // your correlation ID
inbox_tracer?: string; // batch grouping key
notes?: string;
bookmarked?: boolean;
}

interface CreateResultRequest {
tweet_text: string; // REQUIRED — the tweet content
approved?: boolean; // default true for manual results
match_score?: number; // default 100
ai_analysis?: string; // default "Manually created"
scheduled_datetime?: string; // ISO 8601
on_schedule_email?: string; // overrides job default
on_schedule_webhook?: string; // overrides job default
on_proof_webhook?: string; // overrides job default
on_proof_email?: string; // overrides job default
destination_url?: string; // overrides job default
tracer?: string; // overrides job default
inbox_tracer?: string; // overrides job default
}

// Batch variant: { results: CreateResultRequest[] }

interface UpdateResultRequest {
approved?: boolean;
rejected?: boolean;
user_notes?: string;
tweet_text?: string; // edit the tweet text
match_score?: number;
bookmarked?: boolean;
scheduled_datetime?: string; // ISO 8601
on_schedule_email?: string;
on_schedule_webhook?: string;
on_proof_webhook?: string;
on_proof_email?: string;
destination_url?: string;
}

interface UpdateScheduledRequest {
tweet_text?: string;
scheduled_datetime?: string;
on_schedule_email?: string;
on_schedule_webhook?: string;
on_proof_webhook?: string;
on_proof_email?: string;
destination_url?: string;
instruction_prompt?: string;
}

interface SubmitProofRequest {
proof_url: string; // REQUIRED — URL of the posted tweet
reported_by?: string;
}

// ── Response Wrappers ──────────────────────────────────────────

interface ApiResponse<T = unknown> {
success: boolean;
data?: T;
error?: { code: string; message: string };
}

interface PaginatedResponse<T> {
items: T[];
next_cursor?: string;
total?: number;
}

// ── Webhook Event Payloads (outbound) ──────────────────────────

interface WebhookEvent<T> {
id: string;
action: string;
payload: T;
timestamp: string;
}

interface JobCompletedPayload {
job_id: string;
user_id: string;
status: "COMPLETED" | "FAILED";
results_created: number;
results_approved: number;
credits_spent: number;
tracer?: string;
error_message?: string;
}

interface ScheduleFiredPayload {
scheduled_id: string;
result_id: string;
job_id: string;
user_id: string;
tweet_text: string;
volunteer_magic_link: string;
submit_proof_endpoint: string;
destination_url?: string;
tracer?: string;
inbox_tracer?: string;
}

interface ProofSubmittedPayload {
scheduled_id: string;
result_id: string;
job_id: string;
user_id: string;
proof_url: string;
proof_timestamp: string;
tweet_text: string;
tracer?: string;
inbox_tracer?: string;
}
'''

---

## Endpoint Reference

All endpoints return 'ApiResponse<T>'. Paginated endpoints return 'ApiResponse<PaginatedResponse<T>>'.

### Health

| Method | Path      | Auth |
| ------ | --------- | ---- |
| GET    | '/health' | No   |

---

### Auth

| Method | Path          | Auth | Description                                   |
| ------ | ------------- | ---- | --------------------------------------------- |
| POST   | '/auth/login' | No   | Exchange install credentials for token + user |
| GET    | '/auth/me'    | Yes  | Get current user                              |

'''typescript
// Login
const { data } = await fetch('{API}/auth/login', {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
officex_customer_id: "...", // required on first login
officex_install_id: "...",
officex_install_secret: "...",
}),
}).then((r) => r.json());
// data = { user: User, token: string }
'''

---

### Jobs

| Method | Path            | Auth | Description                                          |
| ------ | --------------- | ---- | ---------------------------------------------------- |
| GET    | '/jobs'         | Yes  | List jobs (paginated)                                |
| POST   | '/jobs'         | Yes  | Create job (AI mode: reserves credits + generates; MANUAL mode: ready immediately) |
| GET    | '/jobs/:job_id' | Yes  | Get job details                                      |
| PATCH  | '/jobs/:job_id' | Yes  | Update job metadata (notes, bookmarked, title)       |
| DELETE | '/jobs/:job_id' | Yes  | Delete job (cancels credit reservation if PENDING)   |

'''typescript
// Create a job
const { data } = await fetch('{API}/jobs', {
method: "POST",
headers,
body: JSON.stringify({
content_prompt: "Thought leadership on AI in healthcare",
output_quantity: 20,
publish_mode: "AI_REVIEW",
publish_prompt: "Approve only tweets that sound authoritative and cite specifics",
on_job_finish_webhook: "https://my-server.com/hooks/xab",
tracer: "campaign-q1-2026",
}),
}).then((r) => r.json());
// data = { job_id, status: "PENDING", estimated_cost: 0.42 }

// List jobs with pagination
const { data } = await fetch('{API}/jobs?limit=20&cursor={nextCursor}', {
headers,
}).then((r) => r.json());
// data = { items: Job[], next_cursor?: string }
'''

**Job lifecycle (AI mode):** 'PENDING' → 'PROCESSING' → 'COMPLETED' | 'FAILED'
Processing takes ~30–90 seconds depending on 'output_quantity'. Poll 'GET /jobs/:job_id'
or use 'on_job_finish_webhook' to avoid polling.

**Job lifecycle (MANUAL mode):** Immediately 'COMPLETED' — no generation, no credits reserved.
Add results via 'POST /jobs/:job_id/results'.

---

### Results

| Method | Path                                         | Auth | Description                                               |
| ------ | -------------------------------------------- | ---- | --------------------------------------------------------- |
| GET    | '/jobs/:job_id/results'                      | Yes  | List results for a job (paginated)                        |
| POST   | '/jobs/:job_id/results'                      | Yes  | Create results manually (single or batch)                 |
| PATCH  | '/results/:result_id?job_id=X'               | Yes  | Update result (approve, edit, set datetime, etc.)         |
| POST   | '/results/:result_id/schedule?job_id=X'      | Yes  | Schedule an approved result (requires scheduled_datetime) |
| POST   | '/results/:result_id/auto-schedule?job_id=X' | Yes  | AI picks optimal datetime and schedules                   |

'job_id' is **required** on all result endpoints — pass as query param or in body.

'''typescript
// Approve and edit a tweet
await fetch('{API}/results/{resultId}?job_id={jobId}', {
method: "PATCH",
headers,
body: JSON.stringify({
approved: true,
tweet_text: "Edited tweet text here",
}),
});

// Manual schedule (set datetime first, then schedule)
await fetch('{API}/results/{resultId}?job_id={jobId}', {
method: "PATCH",
headers,
body: JSON.stringify({ scheduled_datetime: "2026-02-15T14:30:00Z" }),
});
await fetch('{API}/results/{resultId}/schedule?job_id={jobId}', {
method: "POST",
headers,
});

// Auto-schedule (AI picks time, no datetime needed)
const { data: scheduled } = await fetch(
'{API}/results/{resultId}/auto-schedule?job_id={jobId}',
{ method: "POST", headers }
).then((r) => r.json());
'''

'''typescript
// Create results manually (single)
const { data: result } = await fetch('{API}/jobs/{jobId}/results', {
method: "POST",
headers,
body: JSON.stringify({
tweet_text: "Your pre-written tweet here",
approved: true,
scheduled_datetime: "2026-03-15T14:00:00Z",
}),
}).then((r) => r.json());

// Create results in batch
const { data: batch } = await fetch('{API}/jobs/{jobId}/results', {
method: "POST",
headers,
body: JSON.stringify({
results: [
{ tweet_text: "First pre-written tweet", approved: true },
{ tweet_text: "Second pre-written tweet", approved: true },
{ tweet_text: "Third pre-written tweet", approved: true },
],
}),
}).then((r) => r.json());
// batch = { items: Result[], count: 3 }
'''

**Preconditions for scheduling:**

- Result must be 'approved: true'
- Manual schedule requires 'scheduled_datetime' set on the result
- Result must not already be 'scheduled: true'

---

### Scheduled

| Method | Path                       | Auth | Description                                      |
| ------ | -------------------------- | ---- | ------------------------------------------------ |
| GET    | '/scheduled'               | Yes  | List scheduled posts (filterable by date range)  |
| GET    | '/scheduled/:scheduled_id' | Yes  | Get scheduled post details                       |
| PATCH  | '/scheduled/:scheduled_id' | Yes  | Update scheduled post (text, datetime, webhooks) |
| DELETE | '/scheduled/:scheduled_id' | Yes  | Cancel/delete scheduled post                     |

'''typescript
// List upcoming scheduled posts
const { data } = await fetch(
'{API}/scheduled?start_date=2026-02-01T00:00:00Z&end_date=2026-02-28T23:59:59Z&limit=100',
{ headers }
).then((r) => r.json());

// Reschedule
await fetch('{API}/scheduled/{scheduledId}', {
method: "PATCH",
headers,
body: JSON.stringify({
scheduled_datetime: "2026-02-20T09:00:00Z",
tweet_text: "Updated tweet content",
}),
});

// Cancel
await fetch('{API}/scheduled/{scheduledId}', {
method: "DELETE",
headers,
});
'''

**Send Now:** If you PATCH 'scheduled_datetime' to within 2 minutes of the current time and
the post hasn't fired yet, the notification fires immediately (email + webhook dispatched right away).

---

### Calendar

| Method | Path                           | Auth | Description                                    |
| ------ | ------------------------------ | ---- | ---------------------------------------------- |
| GET    | '/calendar/month/:year/:month' | Yes  | Scheduled posts grouped by day for a month     |
| GET    | '/calendar/week/:year/:week'   | Yes  | Scheduled posts grouped by day for an ISO week |

'''typescript
// Get February 2026 calendar
const { data } = await fetch('{API}/calendar/month/2026/2', { headers })
.then((r) => r.json());
// data = { year: 2026, month: 2, days: { "2026-02-10": [Scheduled, ...], ... }, total: 15 }

// Get ISO week 7 of 2026
const { data } = await fetch('{API}/calendar/week/2026/7', { headers })
.then((r) => r.json());
'''

---

### Volunteer (Public — No Auth)

| Method | Path                             | Auth | Description                         |
| ------ | -------------------------------- | ---- | ----------------------------------- |
| GET    | '/volunteer/:scheduled_id'       | No   | View volunteer task (tweet to post) |
| POST   | '/volunteer/:scheduled_id/proof' | No   | Submit proof of posting             |

'''typescript
// View task (public)
const { data } = await fetch('{API}/volunteer/{scheduledId}')
.then((r) => r.json());
// data = { scheduled_id, tweet_text, scheduled_datetime, destination_url?,
// fired, proof_url?, instruction_prompt?,
// batch?: { total, completed, current_index, next_task_id? } }

// Submit proof (public)
const { data } = await fetch('{API}/volunteer/{scheduledId}/proof', {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
proof_url: "https://x.com/user/status/123456789",
}),
}).then((r) => r.json());
// data = { proof_url, proof_timestamp, has_next_task, next_task_link? }
'''

Volunteer magic links are auto-generated when a post is scheduled. Use 'inbox_tracer'
on the job to group scheduled posts into sequential batches — volunteers see a progress
bar and auto-navigate to the next task.

---

### Webhooks (Inbound)

| Method | Path                | Auth | Description                      |
| ------ | ------------------- | ---- | -------------------------------- |
| POST   | '/webhooks/officex' | No   | OfficeX install/uninstall events |

Handled automatically by OfficeX. On 'INSTALL', the API creates a user and returns
'agent_context' with the auth token.

---

### NocoDB Views

| Method | Path                                | Auth | Description                                               |
| ------ | ----------------------------------- | ---- | --------------------------------------------------------- |
| GET    | '/nocodb/views'                     | Yes  | Get shareable NocoDB view URLs for jobs/results/scheduled |
| GET    | '/nocodb/jobs/:job_id/results-view' | Yes  | Get NocoDB view filtered to a specific job's results      |

---

## Credit Pricing

Credits are OfficeX ecosystem currency ($0.01 per credit).

| Quantity   | Estimated Cost | USD Equivalent |
| ---------- | -------------- | -------------- |
| 10 tweets  | ~0.22 credits  | $0.0022        |
| 25 tweets  | ~0.52 credits  | $0.0052        |
| 50 tweets  | ~1.02 credits  | $0.0102        |
| 100 tweets | ~2.02 credits  | $0.0202        |

Formula: '0.1 \* ceil(output_quantity / 5) + 0.02'

Additionally, each scheduled email notification costs **0.2 credits** (for volunteer dispatch
and proof confirmation emails via 'on_schedule_email' / 'on_proof_email').

Credits are **reserved** on job creation and **settled** after generation completes.
If the job fails, the reservation is cancelled and credits are refunded.

**MANUAL mode jobs cost 0 credits** for job creation and result submission. You only pay
for email notifications (0.2 credits each) if configured.

---

## Outbound Webhooks

The system fires webhooks to URLs you provide. All use POST with JSON body:

**JOB_COMPLETED** → 'job.on_job_finish_webhook'
'''json
{
"id": "job-complete-{job_id}-{ts}",
"action": "JOB_COMPLETED",
"payload": {
"job_id": "...", "status": "COMPLETED",
"results_created": 10, "results_approved": 8,
"credits_spent": 0.22, "tracer": "my-ref"
},
"timestamp": "2026-02-06T12:00:00Z"
}
'''

**SCHEDULE_FIRED** → 'scheduled.on_schedule_webhook'
Fired at exact scheduled datetime. Includes 'volunteer_magic_link' and 'submit_proof_endpoint'.

**PROOF_SUBMITTED** → 'scheduled.on_proof_webhook'
Fired when a volunteer submits proof of posting.

---

## Error Codes

All errors return '{ success: false, error: { code, message } }'.

| HTTP | Code                     | Meaning                                       |
| ---- | ------------------------ | --------------------------------------------- |
| 400  | 'MISSING_CONTENT_PROMPT' | Job creation requires 'content_prompt'        |
| 400  | 'MISSING_TWEET_TEXT'     | Result creation requires 'tweet_text'         |
| 400  | 'EMPTY_RESULTS'          | Batch must contain at least one result        |
| 400  | 'TOO_MANY_RESULTS'       | Maximum 100 results per batch request         |
| 400  | 'MISSING_CREDENTIALS'    | No auth token or query params provided        |
| 400  | 'MISSING_CUSTOMER_ID'    | First-time login requires 'officex_customer_id' |
| 400  | 'MISSING_JOB_ID'         | Result endpoints require 'job_id'             |
| 400  | 'NOT_APPROVED'           | Cannot schedule an unapproved result          |
| 400  | 'NO_DATETIME'            | Manual schedule requires 'scheduled_datetime' |
| 400  | 'INVALID_DATE'           | Invalid year/month/week for calendar endpoint |
| 400  | 'MISSING_PROOF_URL'      | Proof submission requires 'proof_url'         |
| 401  | 'UNAUTHORIZED'           | Missing or invalid auth token                 |
| 401  | 'INVALID_CREDENTIALS'    | Wrong install_secret                          |
| 402  | 'INSUFFICIENT_CREDITS'   | Not enough OfficeX credits                    |
| 404  | 'JOB_NOT_FOUND'          | Job ID does not exist                         |
| 404  | 'RESULT_NOT_FOUND'       | Result ID does not exist                      |
| 404  | 'SCHEDULED_NOT_FOUND'    | Scheduled ID does not exist                   |
| 404  | 'USER_NOT_FOUND'         | User not found for given install credentials  |
| 409  | 'ALREADY_SCHEDULED'      | Result is already scheduled                   |

---

## Common Automation Patterns

### Generate → Auto-Approve → Auto-Schedule (Full Autopilot)

'''typescript
// Create with AUTO_APPROVED mode
const { data: job } = await fetch('{API}/jobs', {
method: "POST",
headers,
body: JSON.stringify({
content_prompt: "Daily insights on startup fundraising",
output_quantity: 7,
publish_mode: "AUTO_APPROVED",
on_job_finish_webhook: "https://my-server.com/hooks/job-done",
on_schedule_email_default: "team@company.com",
inbox_tracer: 'week-{Date.now()}',
}),
}).then((r) => r.json());

// Wait for completion (or use webhook)
// ...then auto-schedule all approved results:
const { data: results } = await fetch(
'{API}/jobs/{job.job_id}/results',
{ headers }
).then((r) => r.json());

for (const result of results.items.filter((r: Result) => r.approved)) {
await fetch(
'{API}/results/{result.result_id}/auto-schedule?job_id={job.job_id}',
{ method: "POST", headers }
);
}
'''

### Webhook-Driven (No Polling)

'''typescript
// 1. Create job with webhook
await fetch('{API}/jobs', {
method: "POST",
headers,
body: JSON.stringify({
content_prompt: "...",
on_job_finish_webhook: "https://my-server.com/hooks/tweets-ready",
tracer: "order-abc123",
}),
});

// 2. Your webhook handler receives JOB_COMPLETED
// 3. Approve/schedule in your handler
// 4. on_schedule_webhook fires at scheduled time with volunteer link
// 5. on_proof_webhook fires when volunteer posts proof
'''

### Batch Calendar Population

'''typescript
const topics = [
"AI trends in 2026",
"Remote team management",
"SaaS pricing strategies",
];

const jobs = await Promise.all(
topics.map((topic) =>
fetch('{API}/jobs', {
method: "POST",
headers,
body: JSON.stringify({
content_prompt: topic,
output_quantity: 5,
publish_mode: "AUTO_APPROVED",
inbox_tracer: 'batch-{Date.now()}',
}),
}).then((r) => r.json())
)
);
// After jobs complete, auto-schedule all results — AI spaces them across days
'''

### Bring Your Own Tweets (MANUAL Mode)

'''typescript
// 1. Create a MANUAL job (no AI generation, no credits)
const { data: job } = await fetch('{API}/jobs', {
method: "POST",
headers,
body: JSON.stringify({
content_prompt: "Q1 product launch tweets",
generation_mode: "MANUAL",
on_schedule_webhook_default: "https://my-server.com/hooks/post-tweet",
inbox_tracer: 'launch-march-2026',
}),
}).then((r) => r.json());
// data = { job_id, status: "COMPLETED", generation_mode: "MANUAL", estimated_cost: 0 }

// 2. Push your pre-written tweets
await fetch('{API}/jobs/{job.job_id}/results', {
method: "POST",
headers,
body: JSON.stringify({
results: [
{ tweet_text: "We just launched v2.0!", approved: true },
{ tweet_text: "Here's what changed in v2.0...", approved: true },
{ tweet_text: "The #1 feature request was...", approved: true },
],
}),
});

// 3. Auto-schedule all results (AI picks optimal times)
const { data: results } = await fetch(
'{API}/jobs/{job.job_id}/results',
{ headers }
).then((r) => r.json());

for (const result of results.items) {
await fetch(
'{API}/results/{result.result_id}/auto-schedule?job_id={job.job_id}',
{ method: "POST", headers }
);
}
// Scheduling, calendar, webhooks, and volunteer flow all work the same as AI mode
'''

---

## Data Flow Summary

'''
┌──────────────────────────────────────────────────┐
│ AI MODE (default) │ MANUAL MODE │
│ │ │
│ POST /jobs ──→ credits reserved │ POST /jobs ──→ COMPLETED │
│ ──→ Gemini generates │ (0 credits) │
│ ──→ results created │ POST /jobs/:id/results │
│ │ (you push tweets) │
└───────────────────┬──────────────┴───────────────┘
│
┌──────────────────┼──────────────────┐
MANUAL_REVIEW AI_REVIEW AUTO_APPROVED
(you approve) (AI approves) (all approved)
│ │ │
└────────────────┴────────────────────┘
│
PATCH /results/:id ←── approve + edit
│
┌────────────────┴────────────────┐
/schedule /auto-schedule
(you set datetime) (AI picks datetime)
│ │
└──────────────┬───────────────┘
│
Scheduled entry created
volunteer_magic_link generated
│
At scheduled datetime:
→ email + webhook with magic link
│
Volunteer posts tweet
submits proof_url
│
on_proof_webhook fires ✓
'''

