Create a contact and link to a donor¶
A constituent isn't a single record in akoyaGO — it's a contact plus (usually) an akoya_donor record that references the contact via a primary or secondary contact lookup. This guide walks through the creation pattern.
Before working through these, read The constituent model for why this split exists.
Conventions
Examples below assume ORG (env URL), access_token (valid bearer), headers (standard Dataverse headers dict), and service (a ServiceClient instance). See Authentication.
Pre-flight: check if the contact already exists¶
Most ETL workflows hit an existing contact before creating one. Look up by email — the simplest business-unique identifier for individuals.
var query = new QueryExpression("contact") {
ColumnSet = new ColumnSet("contactid", "fullname"),
Criteria = {
Conditions = {
new ConditionExpression("emailaddress1", ConditionOperator.Equal, "jane.smith@example.org")
}
},
TopCount = 1
};
var hits = service.RetrieveMultiple(query);
Guid? contactId = hits.Entities.Count > 0 ? hits.Entities[0].Id : null;
If no row is returned, create the contact. If it exists, decide whether to reuse the existing donor link, attach the contact as a secondary contact on an existing donor, or create a new donor record that points to the existing contact.
Scenario 1: Brand-new individual donor¶
End-to-end: create a contact, create a donor, link the donor to the contact as its primary contact.
# 1. Create the contact
POST {org}/api/data/v9.2/contacts
Content-Type: application/json
Prefer: return=representation
{
"firstname": "Jane",
"lastname": "Smith",
"emailaddress1": "jane.smith@example.org",
"mobilephone": "+1-555-0199"
}
Take the contactid from the response body, then:
# 2. Create the donor with primary-contact binding
POST {org}/api/data/v9.2/akoya_donors
Content-Type: application/json
Prefer: return=representation
{
"akoya_formaldefault": "Smith, Jane",
"akoya_informalgreeting": "Jane",
"akoya_donortype": false,
"akoya_temperature": 730850001,
"akoya_stage": 100000001,
"akoya_PrimaryContact@odata.bind": "/contacts({contactid})"
}
# 1. Create the contact
CONTACT_RESPONSE=$(curl -s -X POST "https://{org}.crm.dynamics.com/api/data/v9.2/contacts" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Prefer: return=representation" \
-d '{
"firstname": "Jane",
"lastname": "Smith",
"emailaddress1": "jane.smith@example.org",
"mobilephone": "+1-555-0199"
}')
CONTACT_ID=$(echo "$CONTACT_RESPONSE" | jq -r '.contactid')
# 2. Create the donor
curl -X POST "https://{org}.crm.dynamics.com/api/data/v9.2/akoya_donors" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Prefer: return=representation" \
-d "{
\"akoya_formaldefault\": \"Smith, Jane\",
\"akoya_informalgreeting\": \"Jane\",
\"akoya_donortype\": false,
\"akoya_temperature\": 730850001,
\"akoya_stage\": 100000001,
\"akoya_PrimaryContact@odata.bind\": \"/contacts($CONTACT_ID)\"
}"
write_headers = {**headers, "Content-Type": "application/json", "Prefer": "return=representation"}
# 1. Create the contact
r = requests.post(
f"{ORG}/api/data/v9.2/contacts",
json={
"firstname": "Jane",
"lastname": "Smith",
"emailaddress1": "jane.smith@example.org",
"mobilephone": "+1-555-0199",
},
headers=write_headers,
)
r.raise_for_status()
contact_id = r.json()["contactid"]
# 2. Create the donor, binding the contact as primary
r = requests.post(
f"{ORG}/api/data/v9.2/akoya_donors",
json={
"akoya_formaldefault": "Smith, Jane",
"akoya_informalgreeting": "Jane",
"akoya_donortype": False,
"akoya_temperature": 730850001,
"akoya_stage": 100000001,
"akoya_PrimaryContact@odata.bind": f"/contacts({contact_id})",
},
headers=write_headers,
)
r.raise_for_status()
donor_id = r.json()["akoya_donorid"]
// 1. Create the contact
var contact = new Entity("contact");
contact["firstname"] = "Jane";
contact["lastname"] = "Smith";
contact["emailaddress1"] = "jane.smith@example.org";
contact["mobilephone"] = "+1-555-0199";
var contactId = service.Create(contact);
// 2. Create the donor, binding the contact as primary
var donor = new Entity("akoya_donor");
donor["akoya_formaldefault"] = "Smith, Jane";
donor["akoya_informalgreeting"] = "Jane";
donor["akoya_donortype"] = false;
donor["akoya_temperature"] = new OptionSetValue(730850001);
donor["akoya_stage"] = new OptionSetValue(100000001);
donor["akoya_primarycontact"] = new EntityReference("contact", contactId);
var donorId = service.Create(donor);
Make it transactional with $batch
The two POSTs above are two round-trips. To execute them atomically — either both succeed or both roll back — wrap them in a single $batch with a change set. See Execute batch operations on Microsoft Learn.
Scenario 2: Add a secondary contact (spouse) to an existing donor¶
Most individual donors eventually need a spouse or co-advisor added as a secondary contact.
# 1. Create the spouse contact
POST {org}/api/data/v9.2/contacts
Content-Type: application/json
Prefer: return=representation
{ "firstname": "John", "lastname": "Smith", "emailaddress1": "john.smith@example.org" }
Then patch the donor:
DONOR_ID="00000000-0000-0000-0000-000000000000"
# 1. Create the spouse contact
SPOUSE_ID=$(curl -s -X POST "https://{org}.crm.dynamics.com/api/data/v9.2/contacts" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Prefer: return=representation" \
-d '{"firstname": "John", "lastname": "Smith", "emailaddress1": "john.smith@example.org"}' \
| jq -r '.contactid')
# 2. Patch the donor
curl -X PATCH "https://{org}.crm.dynamics.com/api/data/v9.2/akoya_donors($DONOR_ID)" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "If-Match: *" \
-d "{\"akoya_SecondaryContact@odata.bind\": \"/contacts($SPOUSE_ID)\"}"
donor_id = "00000000-0000-0000-0000-000000000000"
write_headers = {**headers, "Content-Type": "application/json", "Prefer": "return=representation"}
# 1. Create the spouse contact
r = requests.post(
f"{ORG}/api/data/v9.2/contacts",
json={"firstname": "John", "lastname": "Smith", "emailaddress1": "john.smith@example.org"},
headers=write_headers,
)
r.raise_for_status()
spouse_id = r.json()["contactid"]
# 2. Patch the donor
requests.patch(
f"{ORG}/api/data/v9.2/akoya_donors({donor_id})",
json={"akoya_SecondaryContact@odata.bind": f"/contacts({spouse_id})"},
headers={**headers, "Content-Type": "application/json", "If-Match": "*"},
).raise_for_status()
var donorId = new Guid("00000000-0000-0000-0000-000000000000");
// 1. Create the spouse contact
var spouse = new Entity("contact");
spouse["firstname"] = "John";
spouse["lastname"] = "Smith";
spouse["emailaddress1"] = "john.smith@example.org";
var spouseId = service.Create(spouse);
// 2. Patch the donor
var donor = new Entity("akoya_donor", donorId);
donor["akoya_secondarycontact"] = new EntityReference("contact", spouseId);
service.Update(donor);
Scenario 3: Organizational donor¶
For an organization donor (foundation, corporation, family foundation), create a standard Dataverse account for the organization, add a representative contact, then create an akoya_donor record typed as an organization.
Account → donor linkage is pending product confirmation
The canonical relationship between an organizational account and its akoya_donor record is being confirmed with the akoyaGO product team. The pattern below uses akoya_donortype = true (organization) and sets the primary contact to a representative person at the org; it does not yet show an account → akoya_donor lookup because that direction isn't established. See the constituent-model page for status.
# 1. Create the organization account
POST {org}/api/data/v9.2/accounts
Content-Type: application/json
Prefer: return=representation
{ "name": "Smith Family Foundation", "emailaddress1": "info@smithff.org", "telephone1": "+1-555-0100" }
write_h=(-H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" -H "Prefer: return=representation")
# 1. Account
ACCOUNT_ID=$(curl -s -X POST "https://{org}.crm.dynamics.com/api/data/v9.2/accounts" \
"${write_h[@]}" \
-d '{"name": "Smith Family Foundation", "emailaddress1": "info@smithff.org", "telephone1": "+1-555-0100"}' \
| jq -r '.accountid')
# 2. Primary contact
CONTACT_ID=$(curl -s -X POST "https://{org}.crm.dynamics.com/api/data/v9.2/contacts" \
"${write_h[@]}" \
-d "{
\"firstname\": \"Robert\",
\"lastname\": \"Smith\",
\"jobtitle\": \"President\",
\"parentcustomerid_account@odata.bind\": \"/accounts($ACCOUNT_ID)\"
}" \
| jq -r '.contactid')
# 3. Donor (organization-typed)
curl -X POST "https://{org}.crm.dynamics.com/api/data/v9.2/akoya_donors" \
"${write_h[@]}" \
-d "{
\"akoya_formaldefault\": \"Smith Family Foundation\",
\"akoya_donortype\": true,
\"akoya_PrimaryContact@odata.bind\": \"/contacts($CONTACT_ID)\"
}"
write_headers = {**headers, "Content-Type": "application/json", "Prefer": "return=representation"}
# 1. Create the organization account
r = requests.post(
f"{ORG}/api/data/v9.2/accounts",
json={
"name": "Smith Family Foundation",
"emailaddress1": "info@smithff.org",
"telephone1": "+1-555-0100",
},
headers=write_headers,
)
account_id = r.json()["accountid"]
# 2. Create the primary contact for the org
r = requests.post(
f"{ORG}/api/data/v9.2/contacts",
json={
"firstname": "Robert",
"lastname": "Smith",
"jobtitle": "President",
"parentcustomerid_account@odata.bind": f"/accounts({account_id})",
},
headers=write_headers,
)
contact_id = r.json()["contactid"]
# 3. Create the donor record typed as organization
requests.post(
f"{ORG}/api/data/v9.2/akoya_donors",
json={
"akoya_formaldefault": "Smith Family Foundation",
"akoya_donortype": True,
"akoya_PrimaryContact@odata.bind": f"/contacts({contact_id})",
},
headers=write_headers,
).raise_for_status()
// 1. Create the organization account
var account = new Entity("account");
account["name"] = "Smith Family Foundation";
account["emailaddress1"] = "info@smithff.org";
account["telephone1"] = "+1-555-0100";
var accountId = service.Create(account);
// 2. Create the primary contact for the org
var contact = new Entity("contact");
contact["firstname"] = "Robert";
contact["lastname"] = "Smith";
contact["jobtitle"] = "President";
contact["parentcustomerid"] = new EntityReference("account", accountId);
var contactId = service.Create(contact);
// 3. Create the donor record typed as organization
var donor = new Entity("akoya_donor");
donor["akoya_formaldefault"] = "Smith Family Foundation";
donor["akoya_donortype"] = true;
donor["akoya_primarycontact"] = new EntityReference("contact", contactId);
service.Create(donor);
Setting option-set values¶
Option-set (choice) values are always integers. Examples from akoya_donor:
akoya_temperature: Hot730850000, Warm730850001, Cold730850002, Unknown730850003akoya_stage: Identification100000001, Qualification100000002, Cultivation100000003, Solicitation100000004, Stewardship100000005akoya_inclination: A-Highly inclined100000000through X-Will not give100000004
Always look up the integer value on the corresponding entity reference page — never infer from a label. Values are not sequential across akoyaGO option sets; some use the 730850xxx prefix, some 100000xxx. See the akoya_gifttype warning for an example of mixed prefixes within a single option set.
In the C# SDK, wrap integer values in new OptionSetValue(...).
Setting lookup values with @odata.bind¶
All lookup fields follow the same pattern — append @odata.bind to the navigation property name (PascalCase), point at the entity set URL + GUID:
{
"akoya_PrimaryContact@odata.bind": "/contacts(00000000-...)",
"akoya_SecondaryContact@odata.bind": "/contacts(11111111-...)",
"akoya_Staff@odata.bind": "/systemusers(22222222-...)"
}
In the C# SDK, set lookups with new EntityReference(targetLogicalName, id).
Navigation property names are listed on every entity reference page's Lookups — quick reference table.
Related¶
- Donor reference — full attribute list, option-set values, relationship targets.
- Contact reference — akoyaGO's extensions on the standard
contactentity. - The constituent model — conceptual overview.
- Create an entity using the Web API — Microsoft Learn.
- Associate and disassociate entities — Microsoft Learn (
@odata.binddetails).