Upsert Operations
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
fullNameandlanguage)
Request Format
All upsert endpoints accept the same request body:
{
"fullName": "John Smith",
"email": "john.smith@acme.com",
"language": "en"
}
| Field | Required | Description |
|---|---|---|
fullName | For create | Person’s full name |
email | No | Email address (used for matching) |
language | For create | ISO 639-1 language code (e.g., en, fr) |
When updating, all fields are optional. Only provided fields will be modified.
When creating, fullName and language are required.
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 + language provided | Create new record |
No match found, missing fullName or language | 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.