Skip to content

Create an opportunity ​

Creates an opportunity in a pipeline, optionally placing it in a specific stage and tagging it.

A contact can hold several opportunities in the same pipeline — for example a returning customer placing a second order. How the contact is identified decides whether you get a new opportunity or the existing one:

You sendContactOpportunity
contact_idThat existing contact, used as-isAlways a new one, even if the contact already has opportunities in this pipeline
contact (phone / email)Matched on phone or email, or created if no matchThe contact's current (newest) opportunity in this pipeline is returned if one exists; a new one is created only when there is none

Stage selection

If you omit stage_id, the opportunity is placed in the first stage of the pipeline (ordered by order). If you pass stage_id, that stage must belong to the given pipeline.

New lead (match or create the contact) ​

POST/api/user/opportunities
Required scope: opportunities:write
Request body (JSON)
curl -X POST "https://loopx-console.test/api/user/opportunities" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"pipeline_id":17,"stage_id":189,"name":"New lead from website","notes":"Came in via summer campaign","status":"active","context":{"source_campaign":"summer-2026"},"tags":["vip","hot"],"contact":{"name":"Ali Hassan","phone":"201062691152","email":"ali@example.com","priority":"high"}}'
<?php

use GuzzleHttp\Client;

$client = new Client();
$response = $client->request('POST', 'https://loopx-console.test/api/user/opportunities', [
    'headers' => [
        'Authorization' => 'Bearer $TOKEN',
        'Accept'        => 'application/json',
    ],
    'json' => [
        'pipeline_id' => 17,
        'stage_id' => 189,
        'name' => 'New lead from website',
        'notes' => 'Came in via summer campaign',
        'status' => 'active',
        'context' => [
            'source_campaign' => 'summer-2026'
        ],
        'tags' => [
            'vip',
            'hot'
        ],
        'contact' => [
            'name' => 'Ali Hassan',
            'phone' => '201062691152',
            'email' => 'ali@example.com',
            'priority' => 'high'
        ]
    ]
]);

$data = json_decode($response->getBody(), true);
const res = await fetch('https://loopx-console.test/api/user/opportunities', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer $TOKEN',
    Accept: 'application/json',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    "pipeline_id": 17,
    "stage_id": 189,
    "name": "New lead from website",
    "notes": "Came in via summer campaign",
    "status": "active",
    "context": {
      "source_campaign": "summer-2026"
    },
    "tags": [
      "vip",
      "hot"
    ],
    "contact": {
      "name": "Ali Hassan",
      "phone": "201062691152",
      "email": "ali@example.com",
      "priority": "high"
    }
  })
});

const data = await res.json();

Example response

{
  "status": true,
  "code": 200,
  "validation": [],
  "message": "Opportunity Created",
  "data": {
    "id": 98315,
    "status": "active",
    "contact_id": 44017,
    "stage_id": 189,
    "pipeline_id": 17,
    "name": "New lead from website",
    "notes": "Came in via summer campaign",
    "context": {
      "source_campaign": "summer-2026"
    },
    "created_at": "2026-07-20T10:00:00.000000Z",
    "updated_at": "2026-07-20T10:00:00.000000Z",
    "source": null,
    "source_id": null,
    "contact_name": "Ali Hassan",
    "contact_custom_name": null,
    "contact_priority": "high",
    "contact_identifier": null,
    "contact_email": "ali@example.com",
    "contact_custom_email": null,
    "contact_phone": "201062691152",
    "contact_custom_phone": null,
    "stage_nature": "neutral",
    "stage_name": "New Lead",
    "tags": [
      {
        "id": 12,
        "name": "vip"
      },
      {
        "id": 13,
        "name": "hot"
      }
    ],
    "conversations": []
  }
}

Further opportunity on an existing contact ​

Pass contact_id to open another opportunity on a contact you already have — for example to start a new deal after the previous one was won or lost. The contact object is not needed and is ignored when contact_id is sent.

POST/api/user/opportunities
Required scope: opportunities:write
Request body (JSON)
curl -X POST "https://loopx-console.test/api/user/opportunities" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"pipeline_id":17,"contact_id":44017,"name":"Second order — 20 units","notes":"Returning customer","tags":["repeat"]}'
<?php

use GuzzleHttp\Client;

$client = new Client();
$response = $client->request('POST', 'https://loopx-console.test/api/user/opportunities', [
    'headers' => [
        'Authorization' => 'Bearer $TOKEN',
        'Accept'        => 'application/json',
    ],
    'json' => [
        'pipeline_id' => 17,
        'contact_id' => 44017,
        'name' => 'Second order — 20 units',
        'notes' => 'Returning customer',
        'tags' => [
            'repeat'
        ]
    ]
]);

$data = json_decode($response->getBody(), true);
const res = await fetch('https://loopx-console.test/api/user/opportunities', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer $TOKEN',
    Accept: 'application/json',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    "pipeline_id": 17,
    "contact_id": 44017,
    "name": "Second order — 20 units",
    "notes": "Returning customer",
    "tags": [
      "repeat"
    ]
  })
});

const data = await res.json();

Example response

{
  "status": true,
  "code": 200,
  "validation": [],
  "message": "Opportunity Created",
  "data": {
    "id": 98410,
    "status": "active",
    "contact_id": 44017,
    "stage_id": 187,
    "pipeline_id": 17,
    "name": "Second order — 20 units",
    "notes": "Returning customer",
    "context": null,
    "created_at": "2026-09-28T09:00:00.000000Z",
    "updated_at": "2026-09-28T09:00:00.000000Z",
    "conversation": {
      "id": 5521,
      "contact_id": 44017,
      "status": "active"
    }
  }
}

What happens:

  • A new opportunity is created on that contact, in stage_id or the pipeline's first stage. Earlier opportunities are left untouched, whatever their stage.
  • The new opportunity joins the contact's existing conversation on the pipeline's channel, so the chat history stays in one thread in the inbox. A conversation is created only if the contact has none there yet.
  • It becomes the contact's current opportunity in the pipeline: new inbound messages from the contact are attached to it, and the AI agent sees it first.
  • Stage-start automations of its stage run (stage actions, opening message, followups), as for any new opportunity.
  • The contact's saved details (custom name, email, phone, priority) are not changed. Use the contact endpoints to edit them.

Which contact ids can I use?

contact_id must be a contact that already has at least one opportunity in any of your pipelines. Find it in the contact_id field of an opportunity (see Retrieve an opportunity).

Request body ​

FieldTypeRequiredDescription
pipeline_idintegeryesThe pipeline to create the opportunity in
stage_idintegernoPlace the opportunity in this stage. Defaults to the first stage in the pipeline if omitted
contact_idintegernoAn existing contact to open a new opportunity on. When sent, contact.* fields are not required and are ignored. See Further opportunity on an existing contact
namestring | nullnoOpportunity display name. Max 255 chars
notesstring | nullnoFree-form notes
statusenumnopending, active, won, lost. Defaults to active
contextobject | nullnoArbitrary key-value metadata passed to the AI agent alongside the conversation
tagsstring[]noTag names to attach on creation (created if they don't exist yet)
is_testbooleannoMark as a test opportunity. When true, contact.email and contact.phone must be unique across all contacts
contact.namestring | nullnoContact display name
contact.emailstring | nullconditionalRequired when neither contact.phone nor contact_id is sent
contact.phonestring | nullconditionalRequired when neither contact.email nor contact_id is sent
contact.priorityenumnolow, medium, high. Defaults to medium

Errors ​

CodeWhen
403Token missing opportunities:write scope
422pipeline_id not found, stage_id doesn't exist, contact_id isn't one of your contacts, none of contact_id / email / phone provided, or (when is_test: true) email / phone already exists
400 Create a stage firstThe pipeline has no stages yet, or stage_id belongs to another pipeline