Idempotency & Concurrency Guide
Safely retry requests and handle concurrent updates.
When syncing your CRM with Propstreet, you need to handle network failures and concurrent edits gracefully. This guide shows you how.
ETags for Optimistic Concurrency
ETags prevent "lost update" problems when multiple systems modify the same contact.
How It Works
- GET returns an
ETagheader with a version identifier - PUT/PATCH includes
If-Matchheader with that ETag - If the contact changed since you fetched it, you get a 409 Conflict
- Refetch, merge changes, and retry
Example
# 1. Fetch the contact
curl "https://app.propstreet.com/api/v1/network/contacts/123" \
-H "Authorization: Bearer $TOKEN"
Response:
ETag: "abc123def456"
Content-Type: application/json
{
"id": 123,
"firstName": "Jane",
"lastName": "Doe"
}
# 2. Update with If-Match
curl -X PUT "https://app.propstreet.com/api/v1/network/contacts/123" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "If-Match: \"abc123def456\"" \
-d '{"firstName": "Janet", "lastName": "Doe"}'
Handling 409 Conflict
If someone else updated the contact:
{
"type": "https://tools.ietf.org/html/rfc7807",
"title": "Conflict",
"status": 409,
"detail": "The entity has been modified. Please refetch and retry."
}
Resolution:
- Refetch the contact to get the latest version
- Merge your changes with the current state
- Retry the update with the new ETag
External References for Idempotent Creates
Use externalRefs to make create operations safe to retry:
curl -X POST "https://app.propstreet.com/api/v1/network/contacts" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Jane",
"lastName": "Doe",
"externalRefs": [
{"source": "my-crm", "externalId": "contact-12345"}
]
}'
If you retry and the contact already exists with that external ID, you get the existing contact instead of a duplicate.
Safe Retry Patterns
Creating Contacts
async function createContactSafely(contact) {
const payload = {
...contact,
externalRefs: [{ source: "my-crm", externalId: contact.crmId }],
};
try {
return await createContact(payload);
} catch (error) {
if (error.status === 409) {
// Already exists—fetch and return it
return await getContactByExternalRef("my-crm", contact.crmId);
}
throw error;
}
}
Updating Contacts
async function updateContactSafely(id, changes, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const { data, etag } = await getContact(id);
const updated = { ...data, ...changes };
try {
return await updateContact(id, updated, etag);
} catch (error) {
if (error.status === 409 && attempt < maxRetries - 1) {
continue; // Retry with fresh data
}
throw error;
}
}
throw new Error("Max retries exceeded");
}
Batch Operations
For efficient sync, use batch endpoints:
curl -X POST "https://app.propstreet.com/api/v1/network/contacts/batch" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"ids": [123, 456, 789]}'
Single request, multiple contacts.
Best Practices
- Always use ETags for updates—prevents overwriting changes
- Include external refs on create—makes creates idempotent
- Implement retry with backoff—handles transient failures
- Use batch operations—reduces request count
Conflict Resolution Strategies
| Strategy | When to Use |
|---|---|
| Last Write Wins | Simple integrations |
| Merge | Different fields updated |
| Timestamp-Based | Bidirectional sync with clear "latest wins" |
| Manual Review | Critical data requiring human decision |
For bidirectional sync, compare updatedUtc:
- If Propstreet is newer → apply to your CRM
- If your CRM is newer → push to Propstreet (with ETag)