When synchronizing data from external systems, you often need to create new records or update existing ones. The upsert pattern combines both operations into a single, idempotent request. This guide explains how upsert endpoints work in the Billabex API.
What is Upsert?
Upsert is a portmanteau of “update” and “insert”. An upsert operation:
- Checks if a matching record already exists
- Updates the record if found
- Creates a new record if not found
This eliminates the need to first check if a record exists before deciding whether to create or update it.
Why Use Upsert?
Upsert operations offer several advantages:
- Idempotence : Running the same request multiple times produces the same result
- Simplicity : One endpoint instead of separate create and update logic
- Reliability : No race conditions between “check” and “create” operations
- Sync-friendly : Perfect for periodic data synchronization
Available Upsert Endpoints
Billabex provides upsert endpoints for managing people associated with accounts:
| Endpoint | Description |
|---|---|
PUT /accounts/{accountId}/contacts/upsert |
Upsert a contact |
All endpoints require the accounts:all OAuth scope.
How Matching Works
The upsert endpoints use a two-step matching algorithm to find existing records:
Step 1: Email Match (Priority)
If an email is provided in the request, the system first looks for an existing contact with the same email address. Email matching is case-insensitive.
// This will match an existing contact with email "john@acme.com"
{
"email": "John@ACME.com",
"fullName": "John Smith"
}
Step 2: Name Match (Fallback)
If no email match is found (or no email was provided), the system looks for a name match using fuzzy matching based on the Levenshtein distance algorithm.
A name is considered a match if the edit distance is:
- At most 2 characters, or
- At most 20% of the shorter name’s length
Whichever is smaller.
// These names would match "John Smith":
'john smith'; // Case difference only
'John Smth'; // 1 character missing
'Jon Smith'; // 1 character different
// These would NOT match:
'Jonathan Smith'; // Too many extra characters
'J. Smith'; // Too different
Match Result
- If a match is found → The existing record is updated with the provided fields
- If no match is found → A new record is created (requires
fullNameoremail)
Request Format
All upsert endpoints accept the same request body:
{
"fullName": "John Smith",
"email": "john.smith@acme.com",
"language": "en"
}
| Field | Required | Description |
|---|---|---|
fullName |
See below | Person’s full name |
email |
See below | Email address (used for matching) |
language |
No | ISO 639-1 language code (e.g., en, fr) |
When updating, all fields are optional. Only provided fields will be modified. Sending
fullName: null on a contact this call matches leaves the existing name in place rather than
clearing it, so a nightly sync whose export has no name never wipes one: clearing a name goes
through the update endpoint.
When creating, one of fullName or email is required, not both: an address alone identifies a
contact and no name is derived from it. language is optional and defaults to the language of the
account’s first enabled contact, then to the organization’s default contact language, then to fr.
Examples
Upsert a Contact
const response = await fetch(
'[baseURL]/api/public/v1/accounts/ACCOUNT_ID/contacts/upsert',
{
method: 'PUT',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
fullName: 'John Smith',
email: 'john.smith@acme.com',
language: 'en',
}),
},
);
const contact = await response.json();
Using cURL
curl -X PUT "[baseURL]/api/public/v1/accounts/ACCOUNT_ID/contacts/upsert" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"fullName": "John Smith",
"email": "john.smith@acme.com",
"language": "en"
}'
Response
All upsert endpoints return the created or updated record:
{
"id": "789e0123-e89b-12d3-a456-426614174000",
"fullName": "John Smith",
"email": {
"address": "john.smith@acme.com",
"status": "Valid"
},
"language": "en"
}
Create vs Update Behavior
| Scenario | Result |
|---|---|
| Email matches existing record | Update that record |
| Name matches existing record (no email match) | Update that record |
No match found, fullName or email provided |
Create new record |
No match found, neither fullName nor email |
Error 400 |
Use Cases
Periodic Sync from ERP
Synchronize customer contacts from your ERP system daily:
for (const erpContact of erpContacts) {
await fetch(
`[baseURL]/api/public/v1/accounts/${erpContact.accountId}/contacts/upsert`,
{
method: 'PUT',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
fullName: erpContact.name,
email: erpContact.email,
language: erpContact.language || 'en',
}),
},
);
}
Webhook Handler
Handle incoming webhooks without worrying about record state:
app.post('/webhook/contact-updated', async (req, res) => {
const { accountId, contact } = req.body;
// Upsert handles both new and existing contacts
await fetch(`[baseURL]/api/public/v1/accounts/${accountId}/contacts/upsert`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(contact),
});
res.sendStatus(200);
});
Next Steps
- Contacts : Learn more about managing people
- Account Sources : Link accounts to external systems
- API Reference : Full endpoint documentation
Support
Questions about upsert operations?
Contact us via the website contact form.