The composite tree resource always creates new records, which is why it gives you that behavior. Instead, you'd use a composite request. This example gives you the closest behavior you have available. You'd start by using a GET /services/data/v48.0/sobjects/Account/External_Id__c/x165
, which returns the account, then use that reference in further updates.
Here's your example, put into a form you can use.
URL: POST /services/data/v47.0/composite/
Body:
{
"allOrNone": true,
"compositeRequest":
[
{
"method": "GET",
"url": "/services/data/v47.0/sobjects/Account/External_Id__c/x165",
"referenceId": "x165"
},
{
"method": "POST",
"url": "/services/data/v47.0/composite/sobjects",
"referenceId": "contacts",
"body":
{
"allOrNone": true,
"records":
[
{
"attributes": { "type": "Contact", "referenceId": "johnDoe" },
"FirstName": "John",
"LastName": "Doe",
"AccountId": "@{x165.Id}"
},
{
"attributes": { "type": "Contact", "referenceId": "janeDoe" },
"FirstName": "Jane",
"LastName": "Doe",
"AccountId": "@{x165.Id}"
}
]
}
}
]
}
First, we retrieve the account by Id, and assign it a "reference". We then use the reference via @{x165.id}
in subsequent records. You can include up to 25 subrequests in a request, five of which can be queries. This would be the ideal way to load many records that all have the same related records.
There is, unfortunately, no way to directly reference the external ID as you would in SOAP (as far as I'm aware), so this is probably the best solution. This limitation exists because it is possible to create nested records, and there isn't a way to distinguish if you meant to reference an existing record, or create a new one.