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
404 Not Found Entity doesn't exist
409 Conflict ETag mismatch—refetch and retry
429 Rate Limited Wait for Retry-After seconds
500 Server Error Retry with backoff

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 only)

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