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 and your
If-Matchis stale, you get412 Precondition Failed - 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: W/"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: W/"abc123def456"' \
-d '{"firstName": "Janet", "lastName": "Doe"}'
Handling 412 Precondition Failed
If someone else updated the contact:
{
"type": "https://tools.ietf.org/html/rfc7807",
"title": "Precondition failed",
"status": 412,
"detail": "ETag mismatch - resource was modified"
}
Resolution:
- Refetch the contact to get the latest version
- Merge your changes with the current state
- Retry the update with the new ETag
Idempotency-Key for Creates
Send an Idempotency-Key header (a UUID v4) on a POST that creates something, so a retry after a timeout cannot create it twice:
curl -X POST "https://app.propstreet.com/api/v1/comps" \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: 6f1c2b7e-8a4d-4c3e-9f20-1b5d7e9a0c11" \
-H "Content-Type: application/json" \
-d @deal.json
- The key is kept for 24 hours. A retry with the same key and the same body answers the first response again (same status and body) without writing anything.
- The same key with a different body — or, on a child route, for another parent — is refused with
422(/help/idempotency-conflict). - When the first request may or may not have been saved, a retry answers
409(/help/idempotency-outcome-unknown): check the resource, then retry with a new key. - The routes that take a key include the single creates (
POSTon contacts, companies, links, mandates, projects, prospects and tasks), the batch write routes documented as keyed in their own guides (for exampleprospects:batchUpdateandprospects/tasks:batch), taskmove/replace, and on comps:POST /api/v1/comps,POST /api/v1/comps/{id}/parties,.../sourcesand.../notes(the key covers the deal and the body), andPOST /api/v1/comps:merge. - Not every batch route replays:
POST /api/v1/projects/{id}/prospects:batchignores the key, and a retry runs the batch again. PATCHandDELETEignore the header: useIf-MatchforPATCH, and treat a second404onDELETEas done.
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": [
{"namespace": "my-crm", "id": "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: [{ namespace: "my-crm", id: 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 === 412 && 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)