> ## Documentation Index
> Fetch the complete documentation index at: https://docs.d-edge.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> This site documents the D-EDGE APIs for technology partners (web agencies, OTAs, PMS and RMS vendors, booking and CRM tools). It is written for developers integrating with hotels that use the D-EDGE platform.
> Seven APIs, grouped by domain. Connectivity: Channel Manager API (distribute a hotel's inventory to your channel and send bookings back). Pricing & Availability: Best Price API (best offers to highlight), Quotation API (all bookable offers for a stay), Daily Rates API (daily availability between two dates). Booking: Booking Engine API (a hotel's full direct booking funnel), Gift Shop API (gift vouchers from catalogue to payment). Guest Management: CRM API (guest profiles).
> Pick the API by the need: to display prices, use the Best Price API or the Daily Rates API; to quote a specific stay, use the Quotation API; to book, use the Booking Engine API; to sell gift vouchers, use the Gift Shop API; to manage guest profiles, use the CRM API; to receive hotel inventory and send bookings back as a distribution channel, use the Channel Manager API.
> Base URLs, hotel identifiers, and credentials are delivered by the D-EDGE partnership team during onboarding. Do not invent base URLs, endpoints, parameters, or example credentials that do not appear on these pages.
> Authentication differs per API: OAuth 1.0 for Best Price, Quotation, Daily Rates, and Booking Engine; an X-API-KEY header for Gift Shop; OAuth 2.0 client credentials for CRM; partner-defined credentials carried in the XML message for the Channel Manager API. Read each API's Authentication page before writing code.
> A mock environment returns realistic but fictitious data without credentials. The Postman and Insomnia collections on the API collections page target it by default. Use it for exploration and tests, never for production traffic.
> Use the official names exactly: D-EDGE, D-EDGE platform, Channel Manager API, partnership team. Do not use Back Office or Extranet.
> Each API has a Changelog page. Check it for the current behaviour before relying on an older answer. For questions these pages do not answer, direct the reader to the Contact us page.

# Duplicate profile cleanup

> Find duplicate profiles, score them, and merge without losing an id

The same guest often exists several times in one entity: once from the direct booking, once from the OTA that sent the reservation, once from the email given at check-in. The hotel then sends the same campaign twice and reads a stay history split across three records. Cleaning that up is a partner job, because you are the one who sees the duplicates.

## How it works

Four steps, three of them API calls:

* **Walk the base** with **Browse guests**, page by page.
* **Group candidates** on your side, on whatever your data makes reliable: the same address, the same domain plus the same surname, the same phone.
* **Score each group** with **Compute merge confidence**, which returns a number on the same scale as the merge threshold.
* **Merge above your threshold** with **Merge guests**, and keep the result.

> [!NOTE]
>
> The examples reuse the bearer token from the [Quick start](/crm-api/get-started/quick-start.md). Replace `{entityId}` with your entity.

## Walk the base

The query model needs at least one criterion, so a negation on a value no guest carries walks the whole entity. Send `limit` in the body, then send back the `afterKey` the response returns until it stops coming.

```bash theme={null}
curl --request POST \
  --url 'https://{apiBaseUrl}/api/guestprofile/entities/{entityId}/guests/browse' \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{
    "limit": 100,
    "query": {
      "logic": "AND",
      "criteria": [
        {"logic": "AND", "criteria": [
          {"field": "email", "operator": "!=", "value": "none@invalid"}
        ]}
      ]
    }
  }'
```

```json theme={null}
{
  "guests": [
    {
      "guest": {
        "id": "55468764-64be-4fc1-a43f-e37239e012bf",
        "entityId": "{entityId}",
        "firstname": "Martin",
        "lastname": "Dupont",
        "emails": "martin.dupont@example.com",
        "emailDomains": "example.com"
      }
    }
  ],
  "afterKey": "55468764-64be-4fc1-a43f-e37239e012bf"
}
```

Run **Count guests** with the same query first if you want to size the job before starting.

The browse projection is an index, not the full profile: it carries what searching needs and flattens a single value to a string. Read a candidate with **Get a guest** when you need the sourced attributes behind it.

## Score, then merge

Both calls run in the background. They answer `200` with an empty body and a `location` header pointing at a task, which you read until its `status` settles. A confidence task carries the score in `result`; a merge task carries an object whose `rid` ends with the identifier of the merged profile.

```bash cURL theme={null}
# 1. score the candidates
curl --include --request POST \
  --url 'https://{apiBaseUrl}/api/guestprofile/entities/{entityId}/guests/compute-merge-confidence' \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{"guestIds": ["55468764-64be-4fc1-a43f-e37239e012bf", "55464550-c3d9-4115-814f-a9b83b6baaa5"]}'
# location: /api/guestprofile/tasks/22ab132e-43cb-484e-bd86-e58993495adb

# 2. read the task until it ends
curl --request GET \
  --url 'https://{apiBaseUrl}/api/guestprofile/tasks/22ab132e-43cb-484e-bd86-e58993495adb' \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN'
# {"progress":100,"status":"completed","result":82}

# 3. merge when the score clears your threshold
curl --include --request POST \
  --url 'https://{apiBaseUrl}/api/guestprofile/entities/{entityId}/guests/merge' \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{"guestIds": ["55468764-64be-4fc1-a43f-e37239e012bf", "55464550-c3d9-4115-814f-a9b83b6baaa5"], "threshold": 75}'
```

```javascript Node.js theme={null}
const base = "https://{apiBaseUrl}/api/guestprofile";
const headers = {
  Authorization: `Bearer ${accessToken}`,
  "Content-Type": "application/json",
};

async function runTask(path, body) {
  const started = await fetch(`${base}${path}`, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
  });
  const taskPath = started.headers.get("location");

  for (;;) {
    const task = await (await fetch(`https://{apiBaseUrl}${taskPath}`, { headers })).json();
    if (task.status === "completed") return task.result;
    if (task.status === "failed") throw new Error(task.error);
    await new Promise((r) => setTimeout(r, 500));
  }
}

const guestIds = [
  "55468764-64be-4fc1-a43f-e37239e012bf",
  "55464550-c3d9-4115-814f-a9b83b6baaa5",
];

const score = await runTask(`/entities/${entityId}/guests/compute-merge-confidence`, { guestIds });

if (score >= 75) {
  const result = await runTask(`/entities/${entityId}/guests/merge`, { guestIds, threshold: 75 });
  console.log(result.rid.split(".").pop());
}
```

```python Python theme={null}
import time

base = "https://{apiBaseUrl}/api/guestprofile"
headers = {"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"}


def run_task(path, body):
    started = requests.post(f"{base}{path}", headers=headers, json=body)
    task_path = started.headers["location"]

    while True:
        task = requests.get(f"https://{{apiBaseUrl}}{task_path}", headers=headers).json()
        if task["status"] == "completed":
            return task["result"]
        if task["status"] == "failed":
            raise RuntimeError(task["error"])
        time.sleep(0.5)


guest_ids = [
    "55468764-64be-4fc1-a43f-e37239e012bf",
    "55464550-c3d9-4115-814f-a9b83b6baaa5",
]

score = run_task(f"/entities/{entity_id}/guests/compute-merge-confidence", {"guestIds": guest_ids})

if score >= 75:
    result = run_task(f"/entities/{entity_id}/guests/merge", {"guestIds": guest_ids, "threshold": 75})
    print(result["rid"].split(".")[-1])
```

```php PHP theme={null}
$base = 'https://{apiBaseUrl}/api/guestprofile';

function runTask($client, $base, $path, $body) {
    $started = $client->post($base . $path, ['json' => $body]);
    $taskPath = $started->getHeaderLine('location');

    while (true) {
        $task = json_decode($client->get('https://{apiBaseUrl}' . $taskPath)->getBody(), true);
        if ($task['status'] === 'completed') {
            return $task['result'];
        }
        if ($task['status'] === 'failed') {
            throw new RuntimeException($task['error']);
        }
        usleep(500000);
    }
}

$guestIds = [
    '55468764-64be-4fc1-a43f-e37239e012bf',
    '55464550-c3d9-4115-814f-a9b83b6baaa5',
];

$score = runTask($client, $base, "/entities/$entityId/guests/compute-merge-confidence", ['guestIds' => $guestIds]);

if ($score >= 75) {
    $result = runTask($client, $base, "/entities/$entityId/guests/merge", ['guestIds' => $guestIds, 'threshold' => 75]);
    echo substr(strrchr($result['rid'], '.'), 1);
}
```

```csharp C# theme={null}
var guestIds = new[] {
    "55468764-64be-4fc1-a43f-e37239e012bf",
    "55464550-c3d9-4115-814f-a9b83b6baaa5"
};

async Task<JsonElement> RunTask(string path, object body)
{
    var started = await client.PostAsJsonAsync($"/api/guestprofile{path}", body);
    var taskPath = started.Headers.Location!.ToString();

    while (true)
    {
        var task = await client.GetFromJsonAsync<JsonElement>(taskPath);
        var status = task.GetProperty("status").GetString();
        if (status == "completed") return task.GetProperty("result");
        if (status == "failed") throw new Exception(task.GetProperty("error").GetString());
        await Task.Delay(500);
    }
}

var score = (await RunTask($"/entities/{entityId}/guests/compute-merge-confidence",
    new { guestIds })).GetInt32();

if (score >= 75)
{
    var result = await RunTask($"/entities/{entityId}/guests/merge",
        new { guestIds, threshold = 75 });
    var rid = result.GetProperty("rid").GetString()!;
    Console.WriteLine(rid[(rid.LastIndexOf('.') + 1)..]);
}
```

```java Java theme={null}
String base = "https://{apiBaseUrl}/api/guestprofile";
List<String> guestIds = List.of(
    "55468764-64be-4fc1-a43f-e37239e012bf",
    "55464550-c3d9-4115-814f-a9b83b6baaa5");

JsonNode runTask(String path, String body) throws Exception {
    HttpResponse<String> started = client.send(HttpRequest.newBuilder()
        .uri(URI.create(base + path))
        .header("Authorization", "Bearer " + accessToken)
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(body))
        .build(), HttpResponse.BodyHandlers.ofString());

    String taskPath = started.headers().firstValue("location").orElseThrow();

    while (true) {
        HttpResponse<String> read = client.send(HttpRequest.newBuilder()
            .uri(URI.create("https://{apiBaseUrl}" + taskPath))
            .header("Authorization", "Bearer " + accessToken)
            .GET().build(), HttpResponse.BodyHandlers.ofString());

        JsonNode task = mapper.readTree(read.body());
        String status = task.get("status").asText();
        if (status.equals("completed")) return task.get("result");
        if (status.equals("failed")) throw new IllegalStateException(task.get("error").asText());
        Thread.sleep(500);
    }
}
```

## Undo a wrong merge

A merge is recoverable. **Unmerge a guest** takes the merged profile in the path and splits it back into the profiles it was made of:

```bash theme={null}
curl --request POST \
  --url 'https://{apiBaseUrl}/api/guestprofile/entities/{entityId}/guests/{mergedGuestId}/unmerge' \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN'
```

```json theme={null}
{"success": true}
```

The identifier must be a merged profile, the one whose `type` is `merge`. Pass a plain guest and the call answers `400` with the code `system.invalidQuery`. After the split, the merged identifier stops resolving and each source answers on its own identifier again.

## Tips

* **Score before merging on anything but an exact address match.** Two guests sharing a surname and a domain are often colleagues, not one person. The score is what separates them.
* **Keep the identifiers you already stored.** After a merge, reading a source identifier returns the merged profile, so nothing on your side breaks. Pass `redirect=false` to read the original and `expand=true` to inspect the sources behind it.
* **Record the score you merged on.** It is the only way to tell later whether a threshold of 75 was right for your data, and the input for raising or lowering it.

## Related

> **[Compute merge confidence](/crm-api/build/reference/merge/post-guests-compute-merge-confidence.md)**
>
> The score, and the task that carries it.

> **[Unmerge a guest](/crm-api/build/reference/merge/post-guest-unmerge.md)**
>
> Split a merged profile back apart.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.