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
- Check
RateLimit-Remainingbefore making requests - Respect
Retry-Afterwhen you get a 429 - Use exponential backoff for server errors
- Log 4xx errors—they indicate bugs in your integration
- Use Bot Users for production—10x the rate limit of PATs
Large Network Sync
For networks with 100k+ contacts:
- Use max
page_size=500to minimize requests - Implement delta sync—don't re-pull everything
- If you consistently hit rate limits, contact feedback@propstreet.com