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 |
PUT /accounts/{accountId}/internal-representatives/upsert | Upsert an internal representative |
PUT /accounts/{accountId}/party/upsert | Upsert with auto-detection |
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 or internal representative 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",
"role": "Billing Manager",
"notes": "Primary contact for invoices",
"isPrimary": true
}
| 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) |
role | No | Job title or role |
notes | No | Additional notes |
isPrimary | No | Whether this is the primary contact |
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',
role: 'Billing Manager',
isPrimary: true,
}),
});
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",
"role": "Billing Manager",
"isPrimary": true
}'
Upsert an Internal Representative
const response = await fetch('[baseURL]/api/public/v1/accounts/ACCOUNT_ID/internal-representatives/upsert', {
method: 'PUT',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
fullName: 'Sarah Johnson',
email: 'sarah@yourcompany.com',
language: 'en',
role: 'Account Manager',
}),
});
Upsert Party (Auto-Detection)
The party endpoint automatically determines whether to create a contact or internal representative based on the email domain:
const response = await fetch('[baseURL]/api/public/v1/accounts/ACCOUNT_ID/party/upsert', {
method: 'PUT',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
fullName: 'Alex Brown',
email: 'alex@example.com',
language: 'en',
}),
});
If alex@example.com’s domain matches your organization’s email domain, they become an internal representative. Otherwise, they become a contact.
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",
"role": "Billing Manager",
"notes": null,
"isPrimary": true
}
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',
role: erpContact.jobTitle,
}),
});
}
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 & Representatives – 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.