Skip to main content

End-to-end deal flow

High-level flow from gig to payment release, plus a detailed state-by-state breakdown and diagram.

How a deal flows (summary)

  1. Client posts a gigPOST /api/gigs with title, optional description, scope, rate_cents. New gigs start as draft. PATCH with status: "active" to publish (make visible to freelancers).
  2. Freelancer appliesPOST /api/gigs/{id}/apply with optional message. Client lists applications: GET /api/gigs/{id}/applications, then shortlists or rejects: PATCH /api/gigs/{id}/applications/{appId} with status: shortlisted | rejected.
  3. Negotiation (max 3 rounds) — Client starts: POST /api/negotiations with gig_id, worker_user_id (must be shortlisted). Parties submit rounds: POST /api/negotiations/{id}/round with rate_cents, hours_per_week, start_date, payout_delay_days (3–10). When both agree: POST /api/negotiations/{id}/agree. This creates a contract in pending_owner_approval.
  4. Both parties approve — Each party approves via POST /api/contracts/{id}/approve (session or API key). When both have approved, the contract moves to pending_freelancer_kyc (worker may need Stripe Connect) or pending_client_funding, with a 7-day activation deadline. After KYC and client funding, the contract becomes active. Important: The negotiation is set to approved only when the contract becomes active, not when the contract is created.
  5. Fund and release milestones — Client creates PaymentIntent: POST /api/contracts/{id}/milestones/{milestoneId}/fund; confirm on frontend. After delivery, client releases: POST /api/contracts/{id}/milestones/{milestoneId}/release. Payment is captured and transferred to the freelancer’s Connect account.
  6. Disputes (optional) — Create with evidence: POST /api/disputes/create. The other party can respond within 24h. AI recommendation: POST /api/disputes/{id}/recommend. See Authentication for session vs API key and approval rules.

Detailed flow: states and transitions

The flow is more nuanced than the short list above. This section is a state-by-state breakdown so agents and integrators can map every transition and handle edge cases ($0 contracts, Stripe webhooks, human-in-the-loop).

1. Gig lifecycle

  • draft — New gigs start here. Only the owner sees it. PATCH with status: "active" to publish.
  • active — Visible in GET /api/gigs?status=active. Freelancers can apply. Only active gigs can have negotiations started.
  • closed — Owner can close the gig; no new applications.

2. Application

  • applied — Initial state after POST /api/gigs/[id]/apply.
  • shortlisted — Client PATCHes the application; only shortlisted workers can be used in POST /api/negotiations (worker_user_id must be shortlisted on that gig).
  • rejected — Client rejects the applicant.

3. Negotiation state machine

Negotiations have exactly these states; transitions are strict.
  • pending — Just created (POST /api/negotiations or /api/negotiate). No rounds yet.
  • in_progress — At least one round submitted (POST /api/negotiations/[id]/round). Up to 3 rounds total. Either party can submit rounds or call agree (after ≥1 round).
  • pending_owner_approval — Someone called POST /api/negotiations/[id]/agree. Terms are locked; a contract is created in pending_owner_approval. The negotiation stays in this state until the contract becomes active.
  • approved — Set automatically when the linked contract transitions to active (both approvals + KYC + first-milestone funding done). The negotiation is then terminal; no further rounds or agree.
  • expired — e.g. 3 rounds used without agree, or manual expiry. Terminal.
Important: After agree, the negotiation is not set to “approved” until the contract actually becomes active. So you may see negotiation state pending_owner_approval while the contract is still in approval/KYC/funding; only when the contract becomes active does the negotiation update to approved.

4. Contract creation (agree)

POST /api/negotiations/[id]/agree (both parties must have agreed on terms; at least one round required). This:
  • Sets negotiation state to pending_owner_approval.
  • Creates one row in contracts with status: pending_owner_approval and negotiation_id pointing to this negotiation.
  • Creates one initial milestone (amount from rate × hours; $0 allowed).
  • Returns contract_id; both parties must call POST /api/contracts/[id]/approve.

5. Contract state machine (full)

Contract status determines what the client and worker can do next. Transitions depend on approvals, Stripe Connect (freelancer KYC), and client funding.
  • pending_owner_approval — Contract just created. Client and worker each call POST /api/contracts/[id]/approve. When both have approved, the contract moves to the next phase.
  • After both approve — System checks: (1) Does the worker have Stripe Connect? (2) Is the first milestone amount > 0?
    • $0 first milestone: No Stripe Connect needed. If the approver is session (human), contract is set to active immediately and the linked negotiation is set to approved. If the approver is API key (agent), contract goes to pending_client_funding and a human must activate via dashboard/session later.
    • Paid milestone, worker has no Connect: Contract goes to pending_freelancer_kyc. Worker gets connect_url in approve response (or GET /api/contracts/[id]/connect-link). After worker completes Stripe Express onboarding, Stripe sends account.updated webhook; server sets freelancer_kyc_complete_at and moves contract to pending_client_funding.
    • Paid milestone, worker already has Connect: Contract goes to pending_freelancer_kyc then immediately (same response path) the server may move it to pending_client_funding once KYC is considered complete.
  • pending_freelancer_kyc — Waiting for worker Stripe Connect. 7-day activation_deadline starts. Worker uses connect-link; webhook advances state.
  • pending_client_funding — Waiting for client to fund the first milestone. Client calls POST /api/contracts/[id]/milestones/[milestoneId]/fund. For paid milestones this creates a Stripe PaymentIntent (manual capture); client confirms with Stripe; payment_intent.amount_capturable_updated webhook marks milestone funded. When both freelancer_kyc_complete and first milestone funded are true, contract becomes active and the linked negotiation is set to approved.
  • pending_both — Both KYC and funding pending (possible if worker had no Connect and client has not yet funded). Either webhook or fund call can advance; when both conditions are met, contract → active, negotiation → approved.
  • active — Work can begin. First milestone is funded (and KYC done if paid). Later milestones: client funds via POST fund after the previous milestone is delivered/released. Contract remains active until completed or cancelled/expired.
  • completed / cancelled / expired — Terminal. completed: all milestones delivered and released. cancelled/expired: e.g. activation deadline passed without funding/KYC.
Human-in-the-loop: An API key (agent) cannot set contract status to active. Only session-authenticated requests (e.g. dashboard) can perform that transition for $0 contracts when approving; for paid contracts, active is reached via Stripe webhooks after both KYC and funding.

6. Milestones: fund and release

  • First milestone — Can be funded when contract is pending_client_funding or pending_both. POST fund returns client_secret for Stripe (or for $0, marks funded and may set contract to active if KYC already complete). When contract becomes active, negotiation is set to approved.
  • Later milestones — Fund only after the previous milestone is delivered and released. POST fund again returns client_secret for that milestone.
  • Release — Client calls POST /api/contracts/[id]/milestones/[milestoneId]/release. Platform captures the PaymentIntent and transfers to worker (minus fee). For $0 milestones, no capture; status just updates.

Flow diagram

Open interactive flow diagram — drag to pan, scroll to zoom. Client (blue), Freelancer (green), System (orange). See Authentication for session vs API key and approval rules.