RocketLauncher AI

API and Recipes

GoHighLevel Opportunities API: Pipeline Stage Update Recipes

By Marnix Geerkens. Published 2026-05-28. Updated 2026-05-28.

In short

The GoHighLevel Opportunities API lets you create deals, move them between pipeline stages, update their monetary value, and mark them won or lost. The key IDs you need are the pipeline ID, the pipeline stage ID, and the contact ID. Pull these from your GHL settings or a GET /opportunities/pipelines call.

  • Create an opportunity: POST /opportunities with a pipelineId, pipelineStageId, and contactId.
  • Move a stage: PUT /opportunities/{id} with the new pipelineStageId.
  • Mark won or lost: set the status field to "won" or "lost" in the same PUT call.
  • Pull all pipeline and stage IDs from GET /opportunities/pipelines before building any sync.

Endpoints

MethodPathScopesRate limit
GET/opportunities/pipelinesopportunities.readonlySee GHL API docs
GET/opportunities/searchopportunities.readonlySee GHL API docs
GET/opportunities/{id}opportunities.readonlySee GHL API docs
POST/opportunitiesopportunities.writeSee GHL API docs
PUT/opportunities/{id}opportunities.writeSee GHL API docs
DELETE/opportunities/{id}opportunities.writeSee GHL API docs

Authentication

Opportunity endpoints use the same Bearer token authentication as the Contacts API. Include Authorization: Bearer YOUR_TOKEN and Version: 2021-07-28 in every request.

The Location-Id header (sub-account ID) is required. You can also pass locationId in the request body on POST calls.

Fetch your pipeline and stage IDs once and cache them. IDs do not change unless you delete and recreate stages. Use GET /opportunities/pipelines to retrieve them.

Recipe 1. Fetch all pipelines and stage IDs

Before you can create or move opportunities, you need the pipeline ID and the stage IDs. Run this once per location and store the results. The IDs stay stable as long as you do not delete stages.

cURL
curl -G https://services.leadconnectorhq.com/opportunities/pipelines \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Version: 2021-07-28" \
  --data-urlencode "locationId=YOUR_LOCATION_ID"
Node.js (fetch)
const res = await fetch(
  `https://services.leadconnectorhq.com/opportunities/pipelines?locationId=YOUR_LOCATION_ID`,
  {
    headers: {
      Authorization: 'Bearer YOUR_TOKEN',
      Version: '2021-07-28',
    },
  }
);
const { pipelines } = await res.json();
for (const pipeline of pipelines) {
  console.log(pipeline.id, pipeline.name);
  for (const stage of pipeline.stages) {
    console.log('  stage:', stage.id, stage.name);
  }
}
Response (200 OK)
{
  "pipelines": [
    {
      "id": "pipeline_abc",
      "name": "Sales Pipeline",
      "stages": [
        { "id": "stage_001", "name": "New Lead", "position": 1 },
        { "id": "stage_002", "name": "Qualified", "position": 2 },
        { "id": "stage_003", "name": "Proposal Sent", "position": 3 },
        { "id": "stage_004", "name": "Closed Won", "position": 4 }
      ]
    }
  ]
}

Recipe 2. Create an opportunity for a contact

Use POST /opportunities to add a deal to a specific pipeline stage. The contactId links the opportunity to an existing contact. If the contact does not exist yet, create them first using the Contacts API.

The monetaryValue is optional but useful for revenue forecasting inside GHL. Set it in the base currency of the sub-account.

cURL
curl -X POST https://services.leadconnectorhq.com/opportunities \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Version: 2021-07-28" \
  -d '{
    "pipelineId": "pipeline_abc",
    "locationId": "YOUR_LOCATION_ID",
    "name": "Jane Smith - Pro Plan",
    "pipelineStageId": "stage_001",
    "status": "open",
    "contactId": "CONTACT_ID",
    "monetaryValue": 4997,
    "assignedTo": "USER_ID"
  }'
Node.js (fetch)
const res = await fetch('https://services.leadconnectorhq.com/opportunities', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer YOUR_TOKEN',
    'Content-Type': 'application/json',
    Version: '2021-07-28',
  },
  body: JSON.stringify({
    pipelineId: 'pipeline_abc',
    locationId: 'YOUR_LOCATION_ID',
    name: 'Jane Smith - Pro Plan',
    pipelineStageId: 'stage_001',
    status: 'open',
    contactId: 'CONTACT_ID',
    monetaryValue: 4997,
  }),
});
const { opportunity } = await res.json();
console.log('Created:', opportunity.id);
Python (requests)
import requests

res = requests.post(
    "https://services.leadconnectorhq.com/opportunities",
    headers={
        "Authorization": "Bearer YOUR_TOKEN",
        "Content-Type": "application/json",
        "Version": "2021-07-28",
    },
    json={
        "pipelineId": "pipeline_abc",
        "locationId": "YOUR_LOCATION_ID",
        "name": "Jane Smith - Pro Plan",
        "pipelineStageId": "stage_001",
        "status": "open",
        "contactId": "CONTACT_ID",
        "monetaryValue": 4997,
    },
)
res.raise_for_status()
print(res.json()["opportunity"]["id"])

Recipe 3. Move an opportunity to a new pipeline stage

To advance a deal through your pipeline, PUT /opportunities/{id} with the new pipelineStageId. You can also change the status to "won" or "lost" in the same call when closing a deal.

Changing the pipeline stage triggers any GHL workflow that has a "Opportunity Stage Changed" trigger, so your automations fire automatically.

cURL
curl -X PUT https://services.leadconnectorhq.com/opportunities/OPPORTUNITY_ID \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Version: 2021-07-28" \
  -d '{
    "pipelineStageId": "stage_003",
    "status": "open"
  }'
Node.js (close as won)
const res = await fetch(
  `https://services.leadconnectorhq.com/opportunities/OPPORTUNITY_ID`,
  {
    method: 'PUT',
    headers: {
      Authorization: 'Bearer YOUR_TOKEN',
      'Content-Type': 'application/json',
      Version: '2021-07-28',
    },
    body: JSON.stringify({
      pipelineStageId: 'stage_004', // "Closed Won" stage
      status: 'won',
    }),
  }
);
const { opportunity } = await res.json();
console.log('Status:', opportunity.status);
Python (requests)
import requests

res = requests.put(
    "https://services.leadconnectorhq.com/opportunities/OPPORTUNITY_ID",
    headers={
        "Authorization": "Bearer YOUR_TOKEN",
        "Content-Type": "application/json",
        "Version": "2021-07-28",
    },
    json={"pipelineStageId": "stage_004", "status": "won"},
)
res.raise_for_status()
print(res.json()["opportunity"]["status"])

Common errors and fixes

400 Bad Request: often means the pipelineStageId does not belong to the pipelineId you specified. Fetch the pipeline list again and confirm the IDs match.

404 Not Found: the opportunity ID or contact ID does not exist in this location. Cross-check the locationId in your token with the sub-account that owns the record.

422 Unprocessable Entity: a required field is missing. pipelineId, pipelineStageId, contactId, and name are all required on POST.

429 Too Many Requests: you are syncing too fast. Add a delay between batches and implement exponential back-off. See the official GHL API docs for current rate-limit numbers.

Copy as an MCP tool

MCP tool definition (JSON)
{
  "name": "ghl_update_opportunity_stage",
  "description": "Move a GoHighLevel opportunity to a new pipeline stage. Optionally mark it won or lost.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "opportunityId": { "type": "string" },
      "pipelineStageId": { "type": "string" },
      "status": {
        "type": "string",
        "enum": ["open", "won", "lost", "abandoned"]
      }
    },
    "required": ["opportunityId", "pipelineStageId"]
  }
}

You need a GoHighLevel account to use the API. Start the 30-day trial through our link.

Frequently asked questions

How do I find my pipeline stage IDs?

Call GET /opportunities/pipelines with your locationId. The response lists every pipeline and each stage inside it, with their IDs. Store these IDs in your application config so you do not need to fetch them on every request.

Does moving a stage trigger GHL automations?

Yes. Any workflow with an "Opportunity Stage Changed" trigger fires when you change the pipelineStageId via the API, the same as a manual drag in the GHL kanban view.

Can I create an opportunity without an existing contact?

No. The contactId field is required. Create the contact first using POST /contacts, then use the returned ID when creating the opportunity.

What statuses does the opportunity support?

The status field accepts "open", "won", "lost", and "abandoned". Marking an opportunity "won" or "lost" closes it. Confirm current allowed values in the official GHL API reference, as they can change between API versions.

Related reading

Contacts create and updateCreate or upsert contacts before linking them to opportunities.Inbound webhook: payment eventsAuto-advance opportunity stages when a payment comes in.GHL to Airtable two-way syncKeep your pipeline data in sync with Airtable.GoHighLevel API overviewAll API and webhook recipes.