Update Opportunity Stage
PATCH/external/v1/opportunities/:id/stageMoves an opportunity to a different stage. Lifecycle timestamps (sent_at, accepted_at, qualifying_started_at, qualified_at, scheduled_at, won_at, lost_at) are set automatically based on the target stage.
Behaviour since v8.6.1046
won_at / lost_at are now derived from the target stage's is_win_stage / is_loss_stage flags (no longer from the stage name); moving to a regular stage clears both. The move records opportunity_history, the activity log and refreshes the Kanban in real time, exactly like a human dragging the card. A stage_id from another account or pipeline returns 400 VALIDATION_ERROR; when the stage belongs to another pipeline, pipeline_id follows. The response gains moved (false when the opportunity was already in the stage; a provided discard_reason_id is still saved).
Authentication
Requires opportunities:write permission.
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The unique identifier of the opportunity |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
stage_id | string | Yes | The target stage ID (see List Pipelines) |
discard_reason_id | string | No | Loss reason, when moving to a loss stage |
INFO
The lifecycle timestamp is derived from the target stage name. Stages named invite_sent, accepted, qualifying, qualified, scheduled, won and lost set sent_at, accepted_at, qualifying_started_at, qualified_at, scheduled_at, won_at and lost_at respectively.
Request
curl --request PATCH \
--url "https://app.getraze.com/external/v1/opportunities/opp_abc123def456/stage" \
--header "Content-Type: application/json" \
--header "X-API-Key: YOUR_API_KEY" \
--data '{
"stage_id": "stg_won"
}'const axios = require('axios');
const opportunityId = 'opp_abc123def456';
const response = await axios.patch(
`https://app.getraze.com/external/v1/opportunities/${opportunityId}/stage`,
{ stage_id: 'stg_won' },
{
headers: {
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
}
}
);
console.log(response.data);import requests
opportunity_id = 'opp_abc123def456'
response = requests.patch(
f'https://app.getraze.com/external/v1/opportunities/{opportunity_id}/stage',
headers={
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
json={'stage_id': 'stg_won'}
)
print(response.json())Response
{
"success": true,
"data": {
"id": "opp_abc123def456",
"title": "Acme Inc - Plan Pro",
"stage_id": "stg_won",
"stage_name": "Won",
"won_at": "2026-01-25T10:00:00Z",
"contact_name": "John Doe",
"owner_user_name": "Maria Silva",
"updated_at": "2026-01-25T10:00:00Z"
},
"stage_change": {
"from": { "id": "stg_proposal", "name": "Proposal" },
"to": { "id": "stg_won", "name": "Won" }
}
}Response Fields
| Field | Type | Description |
|---|---|---|
data | object | The updated opportunity |
stage_change.from | object | Previous stage (id, name) |
stage_change.to | object | New stage (id, name) |
Errors
| Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | stage_id is required |
| 404 | NOT_FOUND | Opportunity not found |