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

  1. GET returns an ETag header with a version identifier
  2. PUT/PATCH includes If-Match header with that ETag
  3. If the contact changed since you fetched it, you get a 409 Conflict
  4. 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:

  1. Refetch the contact to get the latest version
  2. Merge your changes with the current state
  3. 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

  1. Always use ETags for updates—prevents overwriting changes
  2. Include external refs on create—makes creates idempotent
  3. Implement retry with backoff—handles transient failures
  4. 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)