# Scout4Work Agent API

Scout4Work exposes a small authenticated HTTP API for agents such as Codex, OpenCode, Claude, or local scripts.

Use this API to:

- Read the user's saved profile.
- Search, filter, load, patch, or delete saved Upwork jobs.
- Read and write optional user-defined JSON assets.
- Generate Scout analysis/proposal output.
- Save improved final analysis or proposal output back to Scout.

## Authentication

Use the Scout app origin as the base URL, for example `https://<your-scout-domain>`.

Use a user API key from the Settings page:

```http
Authorization: Bearer usk_...
```

For JSON request bodies, use:

```http
Content-Type: application/json
```

Do not send the Scout API key to Upwork or other third-party APIs.

Missing, malformed, deleted, or expired keys return:

```json
{
	"error": "Unauthorized"
}
```

## Recommended Workflow

1. Read the user's profile with `GET /api/profile`.
2. Create or reuse a local working folder for job-specific research, drafts, demos, and notes.
3. Optionally read the user's Scout asset index with `GET /api/assets`, then fetch relevant assets with `GET /api/assets?key=<key>`.
4. Search or load a saved job.
5. Research fit, draft proposals, answer screening questions, or create local demo work in the local working folder.
6. Save only final user-facing analysis or proposal output back to Scout with `PATCH /api/jobs/:externalId`.

Agent rules:

- Do not invent user experience.
- Do not invent projects, links, metrics, client names, credentials, or past results.
- If evidence is missing, ask the user or use user-provided assets/notes.
- Prefer concrete evidence from the user's profile, saved projects, local notes, or optional Scout assets.
- Prefer local files for work in progress. Use Scout assets only for shared, reusable JSON context that the user wants available through the Scout API.
- Keep private reasoning separate from final user-facing output.

## Profile

```http
GET /api/profile
PATCH /api/profile
```

Use `GET /api/profile` to read the user's saved profile context.

The response includes user metadata, `profile`, and assets.

Use `PATCH /api/profile` only when the user explicitly wants the saved profile updated. It requires a full profile object:

```json
{
	"profile": {
		"version": 1,
		"displayName": null,
		"headline": null,
		"bio": null,
		"location": null,
		"timezone": null,
		"availability": "unknown",
		"minimumHourlyRateUsd": null,
		"minimumFixedBudgetUsd": null,
		"skills": [],
		"strengths": [],
		"focusAreas": [],
		"preferredIndustries": [],
		"projectPreferences": [],
		"mustHaveSignals": [],
		"doNotWantSignals": [],
		"portfolio": []
	}
}
```

`availability` must be `unknown`, `open`, `limited`, or `closed`. Missing required profile fields, invalid enum values, invalid arrays, or invalid portfolio item shapes return `400`:

```json
{
	"error": "Invalid profile payload"
}
```

## Jobs

```http
GET /api/jobs?keyword=<text>&page=1&limit=20
GET /api/jobs/:externalId
PATCH /api/jobs/:externalId
DELETE /api/jobs/:externalId
```

Keyword search treats user input as plain text, not regex syntax. Matching is case-insensitive substring matching. Multiple words in one `keyword` value are treated as one literal phrase; there is no tokenization.

Use `/api/jobs?format=md` when you want markdown job context for LLM work.

Common query parameters:

- `keyword`: text search for `/api/jobs`.
- `field`: optional `title`, `description`, or `skill` for `/api/jobs`.
- `decision`: optional `new`, `apply`, `consider`, `skip`, `drafted`, `submitted`, `closed`, or `deleted`.
- `beginTime` / `endTime`: ISO timestamp filters for job creation time in `/api/jobs`.
- `usOnly=true` or `location=us`: US client filter.
- `offset` and `limit`: offset-style pagination for `/api/jobs`.
- `page` and `limit`: page-style pagination for `/api/jobs`.
- `format=md`: return markdown strings from `/api/jobs`.

`GET /api/jobs` returns:

```ts
type JobsResponse = {
	jobs: JobListItem[] | string[];
	total: number;
	index: number;
	limit: number;
	page: number;
	totalPages: number;
};
```

`GET /api/jobs/:externalId` accepts either Mongo `_id` or Upwork `externalId` and returns `{job}`.

Missing jobs return:

```json
{
	"error": "Job not found"
}
```

In default JSON mode, each `JobListItem` includes:

```ts
type JobListItem = {
	externalId: string;
	opening: {
		job: {
			status?: string;
			info?: {
				type?: string;
				access?: string;
				title?: string;
				createdOn?: string;
			};
			budget?: {
				amount?: number;
				currencyCode?: string;
			};
			clientActivity?: {
				totalApplicants?: number;
				totalHired?: number;
				totalInvitedToInterview?: number;
				unansweredInvites?: number;
				invitationsSent?: number;
			};
		};
		qualifications?: object;
	};
	buyer?: {
		info?: {
			location?: {
				offsetFromUtcMillis?: number;
				countryTimezone?: string;
				city?: string | null;
				country?: string;
			};
			stats?: {
				score?: number;
			};
			company?: {
				name?: string | null;
			};
		};
	};
	scrapedAt?: string;
	analysis?: object;
	analyzedAt?: string;
	notes?: {
		text: string | null;
		classificationOverride: "apply" | "consider" | "skip" | "drafted" | "submitted" | null;
	} | null;
	proposals?: object[];
	proposalGeneratedAt?: string;
	deletedAt?: string;
};
```

In `format=md` mode, `jobs` is an array of markdown strings.

## Updating Jobs

`PATCH /api/jobs/:externalId` only accepts these top-level fields:

- `analysis`
- `proposal`
- `notes`

Use it when an agent has produced a better final analysis or proposal locally and needs to save it back to Scout, or when the user wants job notes saved back to Scout.

Do not use this endpoint to modify job title, description, budget, client metadata, raw Upwork data, or other imported job fields.

Patch example:

```json
{
	"proposal": {
		"coverLetter": "Final client-facing proposal text.",
		"questions": [
			{
				"question": "Describe your relevant experience.",
				"position": 0,
				"answer": "I have shipped similar AI workflow and review systems."
			}
		],
		"instruction": null
	}
}
```

Notes patch example:

```json
{
	"notes": {
		"text": "Client prefers a US timezone overlap. Revisit after checking healthcare compliance details.",
		"classificationOverride": "consider"
	}
}
```

`proposal.coverLetter` must be a non-empty string. `proposal.instruction` must be a string or `null`; use it only as a short note about how this version was written.

`proposal.questions` is optional and defaults to `[]` when omitted. When present, each item must be:

```ts
type ProposalQuestion = {
	question: string;
	position: number;
	answer: string;
};
```

`proposal.questions` maps to `job.opening.questions`. Include it only when `job.opening.questions` has source screening questions to answer. Do not invent screening answers for jobs without source questions. Do not merge screening-question answers into `proposal.coverLetter` as the durable saved data model.

When you save a proposal, `proposal.coverLetter` is the final client-facing cover letter and `proposal.questions` is the final screening-answer set for that version. Scout appends a proposal-history entry internally. Do not send proposal history, plan data, or private reasoning in this patch request.

`analysis` must be a complete structured analysis object, not markdown and not a partial patch.

`notes` must be an object or `null`.

`notes.text` must be a string or `null`. Empty strings are normalized to `null`.

`notes.classificationOverride` must be one of:

- `apply`
- `consider`
- `skip`
- `drafted`
- `submitted`
- `null`

Do not use `new` as a manual notes classification override.

Analysis patch example:

```json
{
	"analysis": {
			"version": 4,
		"overallScore": 4,
		"decision": "apply",
		"recommendation": "Apply with a scoped proposal focused on the matching project evidence.",
		"blockingUnknowns": [],
		"fit": {
			"score": 4,
			"level": "strong",
			"matchedCapabilities": ["LLM product engineering"],
			"gaps": [],
			"positiveSignals": ["The project asks for experience matching saved profile assets."],
			"negativeSignals": [],
			"proposalAngle": "Lead with the most relevant shipped project and propose a small first milestone.",
			"confidence": 0.8,
			"evidence": ["Job description mentions LLM email workflow."],
			"summary": "Strong fit if timeline and data access are realistic."
		},
		"feasibility": {
			"score": 4,
			"positives": ["Scope can be staged."],
			"concerns": [],
			"evidence": ["Six-week project window."],
			"summary": "Feasible with clear milestone control."
		},
		"clarity": {
			"score": 3,
			"positives": [],
			"concerns": ["Exact integrations need confirmation."],
			"evidence": [],
			"summary": "Some requirements need clarification."
		},
		"complexity": {
			"score": 4,
			"positives": ["Known implementation patterns."],
			"concerns": [],
			"evidence": [],
			"summary": "Moderate complexity."
		},
		"pricing": {
			"score": 3,
			"positives": [],
			"concerns": ["Budget must match the requested delivery speed."],
			"evidence": [],
			"summary": "Pricing needs validation."
		},
		"clientQuality": {
			"score": 3,
			"positives": [],
			"concerns": [],
			"evidence": [],
			"summary": "Not enough client evidence to overstate quality."
		},
			"risk": {
				"score": 3,
				"positives": [],
				"concerns": ["Possible scope creep."],
				"evidence": [],
				"summary": "Manage risk with milestones."
			},
			"proposalStrategy": {
				"hardProblem": "The client needs a reliable workflow, not just a model response.",
				"clientNeed": "A scoped implementation plan with evidence, controls, and clear questions.",
				"proposalAngle": "Lead with the workflow risk and propose a small first milestone.",
				"openingPosition": "Open by naming the workflow risk and the first milestone.",
				"implementationFocus": ["Evidence capture", "Review flow"],
				"bestProofPoints": [
					{
						"title": "Relevant portfolio project",
						"url": "https://example.com",
						"relevance": "Shows similar workflow delivery.",
						"evidenceType": "portfolio",
						"confidence": 0.8
					}
				],
				"safeClaims": ["Can design and build a scoped workflow."],
				"claimsToAvoid": ["Do not claim direct domain experience unless profile evidence proves it."],
				"clientQuestions": ["Which outcome should trigger manual review?"],
				"demoOrPocFit": {
					"level": "good",
					"reason": "A small proof can validate the workflow before full buildout."
				}
			}
		}
	}
```

Analysis schema:

```ts
type Score = 1 | 2 | 3 | 4 | 5;

type AnalysisDimension = {
	score: Score;
	positives: string[];
	concerns: string[];
	evidence: string[];
	summary: string;
};

type FitAnalysis = {
	score: Score;
	level: "strong" | "moderate" | "weak" | "poor";
	matchedCapabilities: string[];
	gaps: string[];
	positiveSignals: string[];
	negativeSignals: string[];
	proposalAngle: string;
	confidence: number; // 0 to 1
	evidence: string[];
	summary: string;
};

type ProposalProofPoint = {
	title: string;
	url: string | null;
	relevance: string;
	evidenceType: "portfolio" | "profile" | "work_history" | "user_note";
	confidence: number; // 0 to 1
};

type ProposalStrategy = {
	hardProblem: string;
	clientNeed: string;
	proposalAngle: string;
	openingPosition: string;
	implementationFocus: string[];
	bestProofPoints: ProposalProofPoint[];
	safeClaims: string[];
	claimsToAvoid: string[];
	clientQuestions: string[];
	demoOrPocFit: {
		level: "good" | "possible" | "poor";
		reason: string;
	};
};

type JobAnalysis = {
	version: 4;
	overallScore: Score;
	decision: "apply" | "consider" | "skip";
	recommendation: string;
	blockingUnknowns: string[];
	fit: FitAnalysis;
	feasibility: AnalysisDimension;
	clarity: AnalysisDimension;
	complexity: AnalysisDimension;
	pricing: AnalysisDimension;
	clientQuality: AnalysisDimension;
	risk: AnalysisDimension;
	proposalStrategy: ProposalStrategy;
};
```

All arrays must contain strings.

Patch behavior:

- Missing both `analysis` and `proposal` returns `400`.
- Unknown top-level fields return `400`.
- Invalid `analysis`, invalid `proposal`, or explicit `null` for either field returns `400`.
- Provided `analysis` replaces the saved analysis object; it is not deep-merged.
- Provided `proposal` appends proposal history internally.
- `analyzedAt` and `proposalGeneratedAt` are set automatically for the fields you provide.
- Success returns `{ "job": <updated job>, "patched": true }`.

## Deleting Jobs

`DELETE /api/jobs/:externalId` removes a saved job. Use it only when the user explicitly asks.

Success:

```json
{
	"deleted": true
}
```

Not found:

```json
{
	"error": "Job not found"
}
```

## Analysis And Proposal Generation

```http
POST /api/jobs/analyze?externalId=<externalId>
POST /api/jobs/analyze-batch
POST /api/jobs/proposal
```

These endpoints generate and save analysis/proposal output. If you research and write a better analysis or proposal yourself, save it with `PATCH /api/jobs/:externalId`.

`POST /api/jobs/analyze?externalId=<externalId>` has no request body. It requires `externalId` in the query string and requires enough saved profile context for fit analysis.

Success:

```ts
type AnalyzeResponse = {
	externalId: string;
	analysis: object;
	saved: true;
};
```

`POST /api/jobs/analyze-batch` accepts only explicit job IDs. It does not accept filters, limits, overwrite options, or search parameters.

Request:

```json
{
	"externalIds": [
		"~022050427377395164115"
	]
}
```

Duplicate, blank, and non-string IDs are ignored.

Success:

```ts
type AnalyzeBatchResponse = {
	concurrency: 10;
	total: number;
	succeeded: number;
	failed: number;
	results: Array<
		| { externalId: string; ok: true; analysis: object }
		| { externalId: string; ok: false; error: string }
	>;
};
```

`POST /api/jobs/proposal` generates and saves a proposal from the job, saved profile context, and saved v4 analysis. If the job has no saved analysis or only older analysis, run `POST /api/jobs/analyze` first.

Request:

```ts
type ProposalRequest = {
	externalId: string;
	instruction?: string;
	style?: 'concise' | 'balanced' | 'detailed';
	includeQuestions?: 'auto' | 'yes' | 'no';
	proofPointSelection?: 'auto' | 'none' | {title: string; url: string | null};
};
```

Success:

```ts
type ProposalRun = {
	id: string;
	version: 1;
	source: 'ai' | 'manual' | 'agent' | 'edited';
	plan: object | null;
	coverLetter: string | null;
	questions: Array<{
		question: string;
		position: number;
		answer: string;
	}>;
	instruction: string | null;
	status: 'planned' | 'complete' | 'failed';
	failureReason: string | null;
	createdAt: string;
	completedAt: string | null;
};

type ProposalResponse = {
	externalId: string;
	plan: object;
	proposalRun: ProposalRun;
	qualityNotes: string[];
	saved: true;
};
```

`proposalRun.plan.questions` is an internal clarifying-questions field for the freelancer. It is not the same as `proposalRun.questions`, which stores Upwork screening-question answers.

## User Assets

```http
GET /api/assets
GET /api/assets?key=<key>
PUT /api/assets?key=<key>
DELETE /api/assets?key=<key>
```

Assets are optional user-defined K/V JSON storage. They are a convenience for small reusable JSON context that should be available through the Scout API. They are not the preferred place for job-specific working files, private drafts, large research notes, demos, or scratch data.

Best practice: store working data in a local project folder, then save back to Scout only the final user-facing analysis or proposal output that should appear in the app. Use assets only when the user wants reusable context available across agents or devices through Scout.

Rules:

- `key` is unique per user.
- `key` is trimmed and must be non-empty.
- `key` can contain any characters accepted in a URL query parameter.
- `key` must be 120 characters or fewer.
- `description` is required, trimmed, non-empty, and 500 characters or fewer.
- `payload` is required and can be any valid JSON value.
- `PUT /api/assets?key=<key>` creates or replaces that key.
- `DELETE /api/assets?key=<key>` returns success even if the key did not exist.

`GET /api/assets` returns a lightweight index:

```json
[
	{
		"projects": "Projects I can reference"
	}
]
```

`GET /api/assets?key=projects` returns:

```ts
type AssetResponse = {
	key: string;
	description: string;
	payload: any JSON value;
	createdAt: string;
	updatedAt: string;
};
```

Missing asset:

```json
{
	"error": "Asset not found"
}
```

Write request:

```json
{
	"description": "Projects I can reference in proposals",
	"payload": [
		{
			"name": "Example project",
			"summary": "What was built and why it matters"
		}
	]
}
```

Write response:

```ts
type PutAssetResponse = {
	asset: AssetResponse;
};
```

Delete response:

```json
{
	"deleted": true
}
```

Recommended asset use cases:

- Saved projects that can be referenced in proposals.
- Reusable bio fragments.
- Short reusable notes about prior experience.
- Proposal style preferences.
- Screening answer templates.

Do not store API keys, passwords, session cookies, private tokens, or third-party credentials in assets.

## Recommended Local Output

For complex job-specific work, create local files outside the app. A local folder is the recommended default because it keeps drafts, private reasoning, demos, and large notes under the user's direct control.

Example:

```text
applications/<externalId>/research.md
applications/<externalId>/proposal.md
applications/<externalId>/screening_answers.md
applications/<externalId>/demo_plan.md
```

Recommended separation:

- `research.md`: notes, evidence, job/client research, risks, and assumptions.
- `proposal.md`: final client-facing proposal text only.
- `screening_answers.md`: direct answers to Upwork screening questions; these map to `proposal.questions` when saved back to Scout.
- `demo_plan.md`: implementation plan, demo scope, or proof-of-work notes.

Keep private reasoning, reusable assets, and final user-facing proposal text in separate files. Save back to Scout only the final analysis/proposal that should be visible in the app.

## Safety Rules

- Do not fabricate projects, links, metrics, or client names.
- Prefer concrete evidence from the user's profile, saved projects, assets, or local notes.
- Ask before making claims that are not supported by available evidence.
- Treat API keys and private job/profile data as sensitive.
- Do not send Scout API keys to Upwork or other third-party APIs.
- Do not store secrets in assets.
- Do not expose private reasoning in final user-facing analysis or proposal text.
