Skip to main content

REST API

Call Upwork Job Search MCP tools directly from any HTTPS client. The REST surface is a thin wrapper over the same handlers the MCP server uses — billing, rate limits, and analytics are identical.

Base URL
https://api.getmany.com.ua

When to use REST instead of MCP

  • You're wiring up a cron job, a Zapier/Make/n8n workflow, an internal dashboard, or a one-off script. No LLM involved.
  • You want a single deterministic HTTP request per tool call — no tool discovery, no protocol negotiation, no streaming.
  • Your runtime is an HTTP-only environment — Edge Functions, BI tools, spreadsheet add-ons, webhooks.

If you're building an agent that needs tool discovery, sampling, or multi-tool orchestration, stick with the MCP transport — see /docs/setup.


Authentication

Every request must carry a Bearer token in the Authorization header. Keys are issued from your dashboard and start with the gm_ prefix. The key is shown once on creation — store it in a secret manager.

Required header
Authorization: Bearer gm_your_api_key_here

All requests must send Content-Type: application/json.

Full key-management guidance — rotation, revocation, scope — lives in /docs/authentication.


Endpoints

All endpoints accept POST only (other methods return 405). The request body is the tool input directly — there is no JSON-RPC envelope, no method field, no params wrapper. Full per-tool input schemas (every filter, every flag) are in /docs/tools.

POST /v1/search_jobs

Search Upwork jobs with 40+ filters across keywords, budget, client requirements, and vendor preferences. Returns up to 10,000 rows per call.

Request body — POST /v1/search_jobs
{
  "limit": 100,
  "searchPeriod": "24 hours",
  "jobCategories": ["Web Development"],
  "budget.hourlyRate.min": "50"
}

POST /v1/get_job

Fetch full details for a single job by UID — description, skills, budget, client stats, and application cost.

Request body — POST /v1/get_job
{
  "jobUid": "1955020056847176693",
  "withClientHistory": false
}

POST /v1/get_client_activity

Premium addon. Proposal count, last client activity, interviewing candidates, invites sent, and unanswered invites for a given job.

Request body — POST /v1/get_client_activity
{ "jobUid": "1955020056847176693" }

POST /v1/get_client_history

Premium addon. The client's work history and contractor feedback from previous freelancers on the same job posting.

Request body — POST /v1/get_client_history
{ "jobUid": "1955020056847176693" }

Full examples

Three copy-paste examples for search_jobs — pick the language of your runtime.

cURL

Terminal
curl -X POST https://api.getmany.com.ua/v1/search_jobs \
  -H "Authorization: Bearer gm_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "limit": 100,
    "searchPeriod": "24 hours",
    "jobCategories": ["Web Development"],
    "budget.hourlyRate.min": "50"
  }'

Node.js / TypeScript

search-jobs.ts
const response = await fetch("https://api.getmany.com.ua/v1/search_jobs", {
  method: "POST",
  headers: {
    "Authorization": "Bearer gm_your_api_key_here",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    limit: 100,
    searchPeriod: "24 hours",
    jobCategories: ["Web Development"],
    "budget.hourlyRate.min": "50"
  })
});

const { data, usage } = await response.json();
console.log(data);

Python

search_jobs.py
import requests

response = requests.post(
    "https://api.getmany.com.ua/v1/search_jobs",
    headers={
        "Authorization": "Bearer gm_your_api_key_here",
        "Content-Type": "application/json"
    },
    json={
        "limit": 100,
        "searchPeriod": "24 hours",
        "jobCategories": ["Web Development"],
        "budget.hourlyRate.min": "50",
    }
)

body = response.json()
print(body["data"])

And the same pattern for get_job — just swap the path and body shape.

cURL — get_job

Terminal
curl -X POST https://api.getmany.com.ua/v1/get_job \
  -H "Authorization: Bearer gm_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "jobUid": "1955020056847176693" }'

Node.js / TypeScript — get_job

get-job.ts
const response = await fetch("https://api.getmany.com.ua/v1/get_job", {
  method: "POST",
  headers: {
    "Authorization": "Bearer gm_your_api_key_here",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({ jobUid: "1955020056847176693" })
});

const { data } = await response.json();
console.log(data);

Response shape

Successful calls return HTTP 200 with a top-level data field carrying the tool result and a usage field carrying per-call credit metering.

200 OK
{
  "data": {
    "jobs": [
      {
        "uid": "1955020056847176693",
        "title": "Senior React Developer for SaaS",
        "hourlyRate": 65,
        "createdAt": "2026-05-27T10:30:00.000Z"
      }
    ],
    "total": 42
  },
  "usage": {
    "consumedMicrocents": 5790,
    "remainingMicrocents": 24210000
  }
}

Errors return a non-2xx status with error (human message) and error_code (machine-readable enum). Switch on error_code — the message is for humans and may change.

400 validation_error
{
  "error": "Invalid jobUid",
  "error_code": "validation_error"
}

Error codes

error_codeHTTPMeaning
validation_error400Malformed JSON body, missing required field (e.g. jobUid), or an input that failed schema validation.
invalid_key401Missing, malformed, or unknown Bearer token. Re-issue a key from the dashboard.
insufficient_credits402Your monthly pool plus any prepaid credit packs are empty. Top up a pack or wait for the next renewal — no metered overage.
addon_forbidden403The tool is a premium addon and your plan does not include it. Upgrade to Pro to call get_client_activity or get_client_history.
method_not_allowed405Tool paths only accept POST. Any other method returns 405 with an Allow: POST header.
payload_too_large413Request body exceeded the per-call size cap (10 MiB). Trim filters, split into multiple calls, or set a tighter limit.
rate_limited429Per-API-key rate limit hit. Honour the Retry-After header (seconds) before retrying.
runtime_error500Internal server fault. Rare; retry with exponential backoff. The message is intentionally generic — server-side logs carry the detail.

Rate limits and quota

Rate limits are enforced per API key. When you exceed the window the server returns HTTP 429 with a Retry-After header (seconds). Honour it — retry sooner and you'll just be rejected again.

429 rate_limited
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json

{
  "error": "rate limited",
  "error_code": "rate_limited",
  "retryAfterMs": 60000
}

When your monthly credit pool plus any prepaid credit packs are exhausted, the server returns HTTP 402 with error_code: insufficient_credits and a hint pointing to the dashboard. There is no metered overage — you top up a pack or wait for the next renewal.

402 insufficient_credits
HTTP/1.1 402 Payment Required
Content-Type: application/json

{
  "error": "insufficient_credits",
  "error_code": "insufficient_credits",
  "message": "Out of credits — buy a credit pack (or enable auto-refill) to keep going.",
  "neededMicrocents": 5790,
  "availableMicrocents": 0,
  "upgradeUrl": "https://app.getmany.com.ua/dashboard?upgrade=pro",
  "hint": "Buy a credit pack at /dashboard"
}

Full rate-limit windows, quota math, and credit-pack pricing live in /docs/rate-limits and /docs/billing.


Next steps