Errors & Rate Limiting Guide

Handle errors gracefully and keep your integration running smoothly.

Error Response Format

All errors return RFC 7807 Problem Details:

{
  "type": "https://tools.ietf.org/html/rfc7807",
  "title": "Validation Error",
  "status": 400,
  "detail": "Email format is invalid",
  "instance": "/api/v1/network/contacts",
  "errors": {
    "email": ["The email field is not a valid e-mail address."]
  }
}

HTTP Status Codes

Status Meaning What To Do
200 Success Process response
201 Created Entity created successfully
204 No Content Delete succeeded
400 Validation Failed Check errors for field details
401 Unauthorized Token expired—refresh and retry
403 Forbidden Check permissions, or the type below
404 Not Found Entity doesn't exist
409 Conflict A problem body: read its type — a lost race (/help/concurrency-conflict): refetch and retry; a rule of the record (/help/operation-not-allowed): a retry answers the same. A likely duplicate on POST /api/v1/comps carries possibleDuplicates[] instead (see below)
412 Precondition Failed Stale If-Match / ETag—refetch and retry
422 Unprocessable A business rule refused the request (e.g. a completion before the signing); fix the request, don't retry it as is
429 Rate Limited Wait for Retry-After seconds
500 Server Error Retry with backoff
503 Service Unavailable Wait for Retry-After seconds

Problem Types

The type member tells you why a request was refused, so you can branch on it instead of reading detail.

/help/plan-required

403. The feature belongs to a plan your organisation is not on. The response names the plan in requiredPlan. Transactions (comps) and every plan gate added from now on answer with this type; a few older Professional features still answer a plain 403 without a type.

{
  "type": "/help/plan-required",
  "title": "Plan required",
  "status": 403,
  "detail": "This feature is part of the Professional plan, which your organisation is not on.",
  "requiredPlan": "professional"
}
  • What Professional includes: among other things Transactions (market transactions — the deals of your closed projects and the deals your team records). Plus does not include Professional.
  • Whose plan counts: the organisation the request acts for — the caller's active organisation. A member of several organisations reaches only the active organisation's deals, and only while that organisation is on Professional; another of their organisations being on it does not help.
  • How to upgrade: choose Upgrade where the app shows the Professional offer (for example on the Transactions page), or contact Propstreet. The same request succeeds once the organisation is on the plan; retrying before that answers the same 403.
  • It is different from a 403 without this type (your user or token lacks the permission) and from /help/operation-not-allowed (the record does not allow the operation in its current state).

/help/operation-not-allowed

403 or 409. The record does not allow the operation for you or in its current state, and repeating the request answers the same. title and detail say which rule. On comps, for example: hiding a deal or correcting an own property's facts when you are not the project's or the property's owner or an admin (403); changing a field a closed project owns, deleting a deal from a closed project, hiding a recorded deal, or merging two projects' deals (409).

/help/concurrency-conflict

409. Another change won a race for the same record — for example a project close or close correction that lost the race for its deal. Read the record again and retry.

Likely duplicates

409 on POST /api/v1/comps, with a body that is not a problem: { "possibleDuplicates": [ { "compId", "name", "dealDate", "reason", "differingFields" } ] }. Nothing was recorded; send the request again with allowDuplicate: true to record it anyway (see Comps).

/help/idempotency-conflict and /help/idempotency-outcome-unknown

422 when an Idempotency-Key is reused with a different body; 409 when an earlier request with the key may or may not have been saved — check the resource, then retry with a new key. See Idempotency.

Rate Limiting

Limits

Authentication Limit Best For
OAuth Client (Bot User) 600 req/min Production sync
Personal Access Token 60 req/min Testing

Response Headers

Every response includes rate limit info:

RateLimit-Limit: 600
RateLimit-Remaining: 542
RateLimit-Reset: 45
Header Meaning
RateLimit-Limit Your limit for this window
RateLimit-Remaining Requests left
RateLimit-Reset Seconds until reset
Retry-After Seconds to wait (on 429 and 503)

Handling 429 Responses

When you hit the limit:

{
  "type": "https://tools.ietf.org/html/rfc7807",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "Rate limit exceeded. Please retry after 12 seconds."
}

Just wait for Retry-After seconds, then continue.

Retry Strategy

For transient errors (429, 500, 502, 503, 504), use exponential backoff:

JavaScript

async function fetchWithRetry(url, options, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const response = await fetch(url, options);

    if (response.ok) return response.json();

    if (response.status === 429) {
      const retryAfter = parseInt(response.headers.get("Retry-After") || "60");
      await sleep(retryAfter * 1000);
      continue;
    }

    if (response.status >= 500) {
      await sleep(Math.pow(2, attempt) * 1000);
      continue;
    }

    throw new Error(`Request failed: ${response.status}`);
  }
  throw new Error("Max retries exceeded");
}

C#

public async Task<T> FetchWithRetryAsync<T>(string url, int maxRetries = 3)
{
    for (int attempt = 0; attempt < maxRetries; attempt++)
    {
        var response = await _httpClient.GetAsync(url);

        if (response.IsSuccessStatusCode)
            return await response.Content.ReadFromJsonAsync<T>();

        if (response.StatusCode == HttpStatusCode.TooManyRequests)
        {
            var retryAfter = response.Headers.RetryAfter?.Delta
                ?? TimeSpan.FromSeconds(60);
            await Task.Delay(retryAfter);
            continue;
        }

        if ((int)response.StatusCode >= 500)
        {
            await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)));
            continue;
        }

        response.EnsureSuccessStatusCode();
    }
    throw new Exception("Max retries exceeded");
}

Best Practices

  1. Check RateLimit-Remaining before making requests
  2. Respect Retry-After when you get a 429
  3. Use exponential backoff for server errors
  4. Log 4xx errors—they indicate bugs in your integration
  5. Use Bot Users for production—10x the rate limit of PATs

Large Network Sync

For networks with 100k+ contacts:

  1. Use max page_size=500 to minimize requests
  2. Implement delta sync—don't re-pull everything
  3. If you consistently hit rate limits, contact feedback@propstreet.com

Webhook Delivery Retries

Webhook deliveries use exponential backoff with jitter (±20%):

  • 9 total attempts (1 initial + 8 retries) over ~35 hours
  • Delays: 3s, 10s, 30s, 5m, 30m, 2h, 8h, 24h
  • Auto-disable after 10 consecutive delivery failures

For complete webhook documentation, see the Webhooks Guide.