How Auto-Apply Works
End-to-end walkthrough of the UpHunt auto-apply pipeline — from API call to terminal state.
UpHunt's auto-apply runs on a dedicated business developer — an Upwork account UpHunt manages — that lives inside your Upwork agency as a regular member. When you call /apply, the proposal is submitted through our business developer. Thanks to Upwork's Agency Plus feature, it goes out on behalf of your agency and the specific freelancer you pick.
This page walks through the pipeline end-to-end so you can design integrations that are resilient to every failure mode along the way. If you haven't set things up yet, start with Auto-Apply Setup.
The pipeline at a glance
You submit an application
Your request hits POST /api/auto-apply-v2/apply with a jobId, coverLetter, and optional proposal fields. We validate the payload, resolve the job (URL, ciphertext, or processed UUID all map to the same internal job), and check that your account has at least one auto-apply credit.
If any of that fails, you get a non-2xx response immediately and no credit is touched.
The job is queued
On success we insert a queue record (queueId), return it to you, and hand the job off to the worker pool. Your API call returns in under a second — everything after this happens asynchronously.
Credits are reserved, not spent. If the worker fails before Upwork accepts the proposal, the reservation is released and you keep the credit.
A worker picks it up
A background worker claims the queue record and has our business developer (already authenticated on Upwork) open the job page.
The profileId on your request decides which freelancer in your agency the proposal is sent on behalf of. Because Agency Plus lets any agency member submit proposals under the agency, you can pick any freelancer from your roster — their name, title, and rate show on the proposal. List available freelancers with GET /freelancers. If you omit profileId, we fall back to the business developer's default profile.
The proposal is filled
The worker opens the job page, detects hourly vs. fixed-price, and populates:
- Cover letter — the
coverLetteryou sent, verbatim. - Bid amount —
proposal.hourlyRateorproposal.fixedBid. If omitted, we use the profile's default rate from the selectedprofileId. - Timeline —
proposal.timeline(defaults to what the job post suggests). - Boost Connects —
proposal.boostBidsadditional Connects, if you opted in. - Screening questions — answered by GPT from your profile context when
autoFillOtherQuestions: true. Otherwise left blank.
Upwork accepts or rejects
The worker clicks Send Proposal and waits for Upwork's response. One of three things happens:
- Accepted — the proposal is submitted. Status transitions to
applied, one credit is deducted, and a webhook fires if you've configured one. - Rejected by Upwork — eligibility, connects, or availability issue. Status transitions to a specific terminal state (see below). No credit deducted.
- Worker exception — network error, page timeout, unexpected layout change. Status transitions to
failedwith anerrorMessage. No credit deducted.
You observe the result
You observe the terminal state either by polling GET /status with the queueId, or by receiving a webhook on the application.applied or application.failed event.
The state machine
Every auto-apply job moves through exactly one of these terminal states:
| Status | Meaning |
|---|---|
processing | Worker has the job but hasn't reached a terminal state yet |
applied | Proposal successfully submitted — credit deducted |
failed | Worker exception or unexpected error — credit refunded, see errorMessage |
not_available | Job was closed/deleted before we could apply — credit refunded |
not_eligible | Upwork rejected based on profile fit (location, skills, tier) — credit refunded |
not_enough_connects | Your agency is out of Connects — credit refunded, top up on Upwork |
logged_out | Business developer session temporarily invalidated — credit refunded, UpHunt reconnects automatically |
The only state that deducts a credit is applied. Every other terminal state releases the reservation.
How the proposal is actually submitted
Proposals are submitted through our business developer — an Upwork account UpHunt manages that's already a member of your agency. Because Agency Plus lets any agency member submit proposals on behalf of the agency (and any other freelancer in it), clients see the proposal exactly as they'd see one from a human agency member — sent on behalf of your agency and the freelancer you pick, not us.
Two practical details worth knowing:
- Authentication: UpHunt keeps each business developer authenticated on Upwork in the background. If something ever breaks the authentication, the next apply briefly returns
logged_outwhile we reconnect — automatically and without any action from you. - Rate shaping: applications from the same business developer are paced to match human behavior. Back-to-back
/applycalls on the same agency add a few seconds of queue time; applications across different agencies run fully in parallel.
Latency budget
| Status | Meaning |
|---|---|
API response | < 1s — just validation + enqueue |
Worker pickup | Usually < 5s, up to 30s under load |
Proposal submission | 20–60s — dominated by Upwork-side latency |
End-to-end p50 | ~40s from /apply to terminal state |
End-to-end p95 | ~90s |
Poll every 5 seconds for the first minute, then every 15 seconds thereafter. See the polling recipe for a ready-made implementation.
Webhooks vs. polling
Both work. Pick one:
- Webhooks — push-based, no polling cost, ideal for automation. The delivery header
X-UpHunt-Deliverylets you dedupe retries. See Webhooks. - Polling — pull-based, simpler to build, fine for interactive UIs where a user is already waiting.
Don't mix: if you use webhooks, stop polling once you receive the terminal event.