# POST /opportunities/merge

**Resource:** [Opportunities](./opportunities.md)  
**MCP tool:** `merge_opportunities`  
**Scopes:** `opportunities:delete`  
**Write operation:** yes

Merge duplicate opportunitys into one. Every child record on the duplicates is re-parented onto the winner before the duplicates are deleted, across the complete foreign-key graph, so nothing is orphaned or destroyed. Pass preview: true to see what would move without writing. This is the only safe way to dedupe: deleting a duplicate directly destroys its linked records.

## Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `winner_id` | body | string | yes | The opportunity that survives. Every duplicate is folded into this one, and it keeps its own values wherever both records have something. |
| `loser_ids` | body | array | yes | The duplicate opportunitys to absorb, up to 100. Each is emptied onto the winner and then deleted. Ids that are not in this workspace are rejected before anything is written. |
| `preview` | body | boolean | no | Set true to get the per-table summary of what WOULD move, without writing anything. Use it to review a large dedupe before committing. |
| `fill_empty_fields` | body | boolean | no | Fill blank fields on the winner from the duplicates. Defaults to true. The winner never loses a value it already had. |
| `transfer` | body | object | no | Per-category opt-outs, e.g. { "activities": false } leaves activities behind. Anything not named is carried. A category cannot switch off a relationship the database deletes on its own (work orders, stage history, stakeholder links and the like): those are always carried, because skipping them would destroy the rows rather than leave them alone, and the response lists them under forced_categories. |
| `field_overrides` | body | object | no | Explicit keep-this-value picks from a choose-what-survives review, as field name to value. Applied before the blank-fill pass, which never talks over one. |
| `append_notes` | body | boolean | no | Append notes from each duplicate onto the winner, separated by a merged marker, instead of letting them die with the row. Defaults to false. |
| `union_tags` | body | boolean | no | Union tags from each duplicate onto the winner, matching on tag name so an existing tag is not duplicated. Tags live on the row, so without this they die with it. Defaults to false. |
| `require_approval` | body | boolean | no | Optional. Set true to route this write into the approval queue for human review instead of executing it immediately (returns 202 + an approval_id). Works even when your key/token has permission to execute directly. |

## Request example

```bash
curl -X POST   "https://api.trustpager.com/functions/v1/api/v1/opportunities/merge"   -H "Authorization: Bearer YOUR_API_KEY"   -H "Content-Type: application/json"   -d '{"winner_id":"...","loser_ids":"...","preview":"..."}'
```

---
Base URL: `https://api.trustpager.com/functions/v1/api/v1` — Auth: `Authorization: Bearer YOUR_API_KEY`