Events and payloads
Every webhook event, its payload schema, and annotated examples.
The envelope
Every delivery wraps its event data in the same envelope:
{
"id": "del_9f2c4e6a",
"event": "job.new",
"version": "2026-04-20",
"createdAt": 1754450123,
"data": { }
}| Field | Notes |
|---|---|
id | The delivery id. Also sent as the X-Lancer-Delivery-Id header. Use it for idempotency. |
event | One of the event names below. Also sent as the X-Lancer-Event header. |
version | The payload schema version, currently 2026-04-20. |
createdAt | UNIX seconds. |
data | Event-specific payload, described per event below. |
Timestamp units differ
The envelope's createdAt is UNIX seconds. Every timestamp inside data.lead (such as
lead.createdAt and lead.datetime) is UNIX milliseconds.
Events
job.new (campaign webhooks)
A job was handed to your system. Fires for campaigns in either external mode, after the job passes the campaign's saved filters:
- In External decision & proposal (
external) it fires for every job that passes the filters, unscored. Lancer's own suitability AI is skipped; deciding is your job, andsuitabilityRating/suitabilityReasonarenull. - In External proposal (
external_proposal) it fires only for jobs Lancer's suitability AI rated suitable, and the lead carries the verdict:suitabilityRating(always 80 or above for a delivered job) andsuitabilityReason. Deciding is done; writing is your job.
Payloads sent by the Test and Trigger buttons bypass suitability, so the two fields can be null on those even in External proposal mode.
A complete, realistic payload with every section annotated:
{
"id": "YDDYfNaFpafJMIQCdqC8",
"event": "job.new",
"version": "2026-04-20",
"createdAt": 1776861204, // UNIX seconds
"data": {
"organizationId": "4IumEJtmvJEE2nShMWwM",
"campaignId": "ICdP5Knfizog5IkDRyTV",
"leadId": "ma6H1U9jYesVEzLjzhAc",
// Ready-made action URLs. Call one of them to answer.
"bidUrl": "https://api.lancer.app/v1/campaigns/ICdP5Knfizog5IkDRyTV/leads/ma6H1U9jYesVEzLjzhAc/bid",
"rejectUrl": "https://api.lancer.app/v1/campaigns/ICdP5Knfizog5IkDRyTV/leads/ma6H1U9jYesVEzLjzhAc/reject",
"lead": {
"id": "ma6H1U9jYesVEzLjzhAc",
"createdAt": 1762332662524, // UNIX ms
"campaignId": "ICdP5Knfizog5IkDRyTV",
// Resolved at dispatch time. null if the campaign was deleted
// or otherwise unreadable when the webhook fired.
"campaignName": "Zoho / CRM specialists",
"organizationId": "4IumEJtmvJEE2nShMWwM",
// Lancer's suitability verdict, 0 to 100, and its reasoning.
// Populated in External proposal mode (delivered jobs are >= 80).
// null in External decision & proposal mode and on Test/Trigger
// payloads for jobs Lancer has not scored.
"suitabilityRating": 92,
"suitabilityReason": "Shopify support role for an established eCommerce brand with a verified, high-spend client; matches the profile's Gorgias and Shopify experience.",
"title": "Customer Support Phone Agent eCommerce | Shopify | Gorgias",
"category": "Customer Service & Tech Support",
"skills": [
{ "name": "Shopify" },
{ "name": "Customer Support" },
{ "name": "Gorgias" }
],
"datetime": 1762332592952, // when the job was posted, UNIX ms
"description": "About Us: We're a fast-growing beauty brand...",
"projectType": "Hiring 4 freelancers",
// "Less than 1 month" | "1 to 3 months" | "3 to 6 months" | "More than 6 months" | null
"projectDuration": "3 to 6 months",
// Your bid must answer these, quoting each question verbatim.
"questions": [
"Are you fine doing email support as well as inbound/outbound call support?",
"Which e-commerce brands did you work for before?"
],
"jobUrl": "https://www.upwork.com/jobs/~021985992773868976101",
"metadata": {
"hours": null,
// "Less than 30 hrs/week" | "More than 30 hrs/week" | null
"duration": "More than 30 hrs/week",
// "Entry level" | "Intermediate" | "Expert"
"experienceLevel": "Expert",
"hourlyRate": { "min": 5, "max": 20 },
// "Hourly" | "Fixed-price"
"paymentType": "Hourly",
"fixedPrice": null
},
"clientInfo": {
"isPaymentVerified": true,
"isPhoneVerified": false,
"rating": 4.79,
"numberOfReviews": 59,
"country": "United States",
"region": "Worldwide",
"jobsPosted": 210,
"totalSpent": 164280.2,
"numberOfHires": 119,
"activeEngagements": 57,
"openJobs": 15, // count of the client's other open jobs
"hireRate": 57,
"memberSince": "2021-06-19T00:00:00.000Z",
"companyIndustry": null,
"companySize": null,
"enterpriseClient": false,
"avgHourlyRatePaid": 16.5,
"companyId": "1406231475600871425"
},
"vendorQualifications": {
// if not null, only freelancers from this location can bid
"location": null,
// "Agency" | "Independent" | "ANY"; if specified, only that type can apply
"talentType": "ANY",
// "FLUENT" | "CONVERSATIONAL" | "NATIVE_OR_BILINGUAL" | "BASIC" | null
"englishLevel": "FLUENT",
// freelancer must have earned more than this (USD) to bid
"minimumEarnings": null,
// freelancers with a Job Success Score below this cannot bid
"jobSuccessScore": 0,
"includeRisingTalent": false
},
"isFeatured": false,
"clientReviews": [],
// "Worldwide" | "USOnly" | "UKOnly"
"region": "Worldwide",
// The Upwork occupation for the job post.
"occupation": "Email, Phone & Chat Support",
// Files the client attached to the job post. Empty array (or null)
// when the client did not attach anything. Each uri is an
// Upwork-relative path; prefix with https://www.upwork.com to
// download. The download requires the freelancer's Upwork session,
// so links typically only work in a browser logged in to Upwork.
"attachments": [
{
"fileName": "Brand_guidelines_v3.pdf",
"length": 532872, // bytes
"uri": "/att/download/openings/2058619791124720878/attachments/cd3c2aea-3602-426c-a16f-d57c0b79f905/download"
}
],
// The client's other currently open postings. Useful for spotting
// clients hiring across multiple roles right now. Distinct from
// clientInfo.openJobs above, which is only a count.
"openJobs": [
{
"id": "1985992773868976101",
// The opaque Upwork cipher; build the job URL as
// https://www.upwork.com/jobs/~02<ciphertext without prefix>
"ciphertext": "~021985992773868976101",
"title": "Shopify storefront redesign",
// "HOURLY" | "FIXED"
"type": "HOURLY",
// Private Talent Cloud posting flag. Most jobs are false.
"isPtcPrivate": false
}
]
}
}
}Respond by calling bidUrl or rejectUrl with your API key. No response means no bid: the lead simply stays parked, there is no timeout fallback.
suitable_job (organization webhooks)
Lancer's suitability AI approved a job for one of your campaigns. Pure notification, informational only; Lancer (or, in External proposal mode, your system) proceeds from the campaign side. data has the same shape as job.new, with data.lead identical to the lead object, including the suitabilityRating and suitabilityReason that produced the verdict. For leads that came from an Upwork invitation rather than a campaign, campaignId is null and the action URLs are omitted.
In External proposal mode this event arrives alongside job.new for the same lead, one to your organization webhooks and one to the campaign webhook. If a single endpoint receives both, act only on job.new: check the X-Lancer-Event header (or the envelope's event) and treat suitable_job as a notification.
{
"id": "wd_abc123",
"event": "suitable_job",
"version": "2026-04-20",
"createdAt": 1713580800,
"data": {
"organizationId": "org_...",
"campaignId": "cmp_...",
"leadId": "lead_...",
"bidUrl": "https://api.lancer.app/v1/campaigns/cmp_.../leads/lead_.../bid",
"rejectUrl": "https://api.lancer.app/v1/campaigns/cmp_.../leads/lead_.../reject",
"lead": { /* the lead object */ }
}
}unsuitable_job (organization webhooks)
Lancer analyzed a job and decided it does not match the campaign. Useful for auditing what the AI is filtering out. Fires for Standard and External proposal campaigns; never for External decision & proposal, where suitability is skipped. The rating and reason are also on data.lead itself.
{
"data": {
"organizationId": "org_123",
"campaignId": "cmp_123",
"leadId": "lead_abc123",
"lead": { "...": "the lead object" },
"suitabilityRating": 3,
"suitabilityReason": "The job requires on-site presence in Berlin, which conflicts with..."
}
}bid_complete (organization webhooks)
A proposal was successfully submitted on Upwork. This is the confirmation that a bid (automatic, or one you submitted via the API) actually went through.
{
"id": "wd_xyz456",
"event": "bid_complete",
"version": "2026-04-20",
"createdAt": 1713580900,
"data": {
"organizationId": "org_...",
"campaignId": "cmp_...",
"leadId": "lead_...",
"lead": { /* the lead object */ },
// The cover letter that was actually submitted.
"proposal": "Hi, I noticed you are looking for..."
}
}Dry-run bids via the API fire a simulated bid_complete so you can test end to end.
lead.new (organization webhooks)
A new message room appeared in your Upwork inbox, usually a client replying to a proposal or inviting you directly. Fires once per room, at creation. Later messages in the same room do not fire again.
{
"id": "wd_lead_new_001",
"event": "lead.new",
"version": "2026-04-20",
"createdAt": 1776861204,
"data": {
"organizationId": "4IumEJtmvJEE2nShMWwM",
// The Upwork bidder account whose inbox received the room.
// Useful if you have multiple accounts connected, so you can
// route the event to the right workflow per account.
"accountId": "TlMAG4bpU31hZOQ5ZkLz",
// The Lancer campaign this room is tied to, enriched with the
// campaign name so you do not need a second API call. Only
// populated when the room is a reply to a proposal you submitted
// via a Lancer campaign (typically roomTypeExtended "fl-proposal"
// with a resolvable room.jobClientHistory.campaignId).
// null for cold invitations, direct-hire offers, and DMs.
"campaign": {
"id": "ICdP5Knfizog5IkDRyTV",
"name": "Zoho / CRM specialists"
},
"room": {
"id": "room_9f3c41d0a28b4e2c9d5f81b07e6a3145",
"roomId": "room_9f3c41d0a28b4e2c9d5f81b07e6a3145",
// The human-readable name Upwork uses for this thread.
// Usually the client's name, sometimes with a company suffix.
"roomName": "Jane Doe, Acme Co",
// Upwork's subject line for the thread. Empty for pure DMs.
"topic": "Zoho implementation",
// What kind of inbox entry this is.
// "client-invitation": client invited you to apply
// "direct_hire_FL_profile": client sent a direct-hire offer
// from your profile (no prior bid)
// "fl-proposal": room created because YOU submitted
// a proposal and the client replied.
// This is the only variant where
// data.campaign is typically set.
// "direct_message": generic DM, no job attached
"roomTypeExtended": "fl-proposal",
// Unix ms. The moment of the latest story in this room on
// Upwork's side (i.e. the newest message).
"recentTimestamp": 1776861180000,
"numUnread": 1,
"latestStory": {
"storyId": "story_a12b34c56d78e90f",
"created": 1776861180000,
"updated": 1776861180000,
"deleted": 0,
"isSystemStory": 0,
"blockedTimestamp": -1,
"userId": "2000000000000000001",
// The first line of the incoming message. Note Upwork prefixes
// with the sender's first name (e.g. "Jane: ...") or "You: ..."
// when the story is yours.
"message": "Jane: Hi, we'd love to chat about our Zoho rollout..."
},
// Opaque Upwork context. Field set varies by roomTypeExtended.
// Anything can be absent; treat every key as optional.
"context": {
"jobName": "Zoho implementation",
"amount": "$15.0/hr",
"associatedAgency": "Your Agency",
"associatedAgencyOrgId": "1055837229072420865",
"clientId": "1587051781037477888",
"clientOrgId": "1587051781037477889",
"clientName": "Jane Doe",
"jobPosterName": "Jane Doe",
"jobTitle": "Zoho implementation",
"jobUid": "2034987699281050049",
"applicationUid": "2034988290957822635",
"applicantName": "Your Name",
"freelancerName": "Your Name",
"freelancerId": "1055837229068226560",
"freelancerOrgId": "1055837229072420865",
// "Activated" | "new" | "withdrawn" | "CANCELLED" | "accepted"
// | "expired" | ... (Upwork's application-status string).
"currentStatus": "Activated",
"showAsAgency": "false"
},
// Present when Lancer has previously seen the underlying Upwork
// job (e.g. you bid on it via a campaign). Null-ish fields mean
// Lancer does not have that data. campaignId links the room back
// to the Lancer campaign that bid; null if there is no match.
// When non-null, the top-level data.campaign above mirrors it
// with the resolved campaign name.
"jobClientHistory": {
"jobUrl": "https://www.upwork.com/jobs/~022034987699281050049",
"title": "Zoho implementation",
"campaignId": "ICdP5Knfizog5IkDRyTV",
"biddingAmount": null,
"boostingAmount": null,
"boostedForPlace": null,
"biddedAt": null,
"jobDatetime": null,
// Same shape as the lead object's clientInfo, where available.
"clientInfo": {
"isPaymentVerified": true,
"isPhoneVerified": true,
"rating": 5,
"numberOfReviews": 3,
"country": "USA",
"region": "Worldwide",
"jobsPosted": 17,
"totalSpent": 3458.21,
"numberOfHires": 7,
"activeEngagements": 4,
"openJobs": 5,
"hireRate": 42,
"memberSince": "2022-10-31T00:00:00.000Z",
"companyIndustry": null,
"companySize": null,
"enterpriseClient": false,
"avgHourlyRatePaid": 20.7,
"companyId": "1587051781037477889"
}
}
}
}
}Subscribing to this event requires a bidder account connected to chat.
The lead object
The lead in job.new, suitable_job, unsuitable_job, and bid_complete payloads, and in GET /leads responses, has this shape. Fields that Upwork does not provide for a given job are null.
| Field | Type | Notes |
|---|---|---|
id | string | Stable lead id. Use it with the Public API. |
createdAt | number | When Lancer created the lead, ms. |
title | string | Job title. |
description | string | Full job description. |
jobUrl | string | Link to the job on Upwork. |
datetime | number | When the job was posted, ms. |
category | string | Upwork category. |
occupation | string | Upwork occupation. |
skills | array | { "name": string } per skill. |
questions | string[] | Screening questions. Answers must match these verbatim when bidding. |
projectType, projectDuration | string | Project shape. |
isFeatured | boolean | Featured job flag. |
region | string | Worldwide, USOnly, or UKOnly. |
metadata | object | experienceLevel, paymentType, hourlyRate ({min,max} or null), fixedPrice, duration, hours. |
clientInfo | object | Payment and phone verification, rating, numberOfReviews, country, region, totalSpent, numberOfHires, jobsPosted, hireRate, memberSince, companyIndustry, companySize, enterpriseClient, avgHourlyRatePaid, activeEngagements, openJobs (count), and more. |
clientReviews | array | The client's past reviews: freelancer feedback both ways, ratings, billed amounts. |
vendorQualifications | object | What the client asks of freelancers: location, talentType, English level, minimum earnings, job success score, and similar. |
attachments | array or null | Files on the job post: { fileName, length, uri }. uri is relative; prefix https://www.upwork.com. |
openJobs | array or null | The client's other open postings: { id, ciphertext, title, type, isPtcPrivate }. |
campaignId | string | The matching campaign. |
campaignName | string or null | Its name. |
organizationId | string | Your organization. |
proposalId | string | Set once a proposal exists. |
suitabilityRating | number or null | Lancer's suitability score, 0 to 100 (80 and above counts as suitable). Populated once the suitability AI has scored the lead: Standard and External proposal campaigns. null whenever suitability has not run: External decision & proposal campaigns, leads materialized by GET /leads, and Test/Trigger payloads for unscored jobs. |
suitabilityReason | string or null | The suitability AI's reasoning in plain text. Same population rules as suitabilityRating. |
Anything not listed here is internal and never included.