API rate limits for bulk work order creation?

POST /work_orders — hitting 429 after ~50 sequential creates. Docs mention 100/min but no burst behavior specified. Need to push 5K records for migration.

Current approach:

for record in dataset:
    POST /work_orders
    sleep(0.6)  # naive rate limit

Still throttled. What's the actual window? Token bucket vs fixed window? Retry-After header sometimes missing.

Also: any batch endpoint planned? Single-request payload limits?

  • hey carlos — yeah this one tripped me up too when I first looked at it. the 100/min is actually per API key and it's a sliding window, not fixed, which is why your 0.6s sleep can still hit edge cases depending on when your first request started.

    the real gotcha: the burst allowance is not documented publicly but in practice you get about 10 requests in a 1-second burst before the throttle kicks in hard. after that it's ~1.67/sec sustained.

    here's what I ended up with for a similar migration:

    import time
    import requests
    from collections import deque
    
    class ThrottledClient:
        def __init__(self, key, rate=100, per=60):
            self.key = key
            self.min_interval = per / rate
            self.requests = deque()
        
        def _wait_if_needed(self):
            now = time.time()
            while self.requests and self.requests[0] < now - 60:
                self.requests.popleft()
            if len(self.requests) >= 100:
                sleep_time = 60 - (now - self.requests[0]) + 0.1
                time.sleep(max(0, sleep_time))
            
        def post(self, payload):
            self._wait_if_needed()
            # ... actual request ...
            self.requests.append(time.time())

    re: batch endpoint — nothing public yet, but I've heard rumblings about a bulk import API for Q4. no promises though, and it probably won't cover the full work order schema on first release.

    heads up: if you're creating with technician assignments in the same payload, that counts as an extra internal call against your limit. might explain why you're seeing fewer effective requests than expected.

  • Rate limit: 100 req/min per key, sliding window.

    Burst: undocumented, observed ~10 req/sec before 429.

    Retry-After: returned when limit exceeded, but not on all edge cases. Bug filed internally.

    Batch: none. Payload limit 10MB.

    Workaround: exponential backoff with jitter. API Rate Limits and Best Practices has implementation pattern.

    Migration: recommend staging chunks <100 with checkpoint resume. Do not parallelize keys — same account, shared limit.

  • @leo — Can you share the full request/response headers when Retry-After is missing? Need to verify if it's the rate limiter or nginx dropping it.

    @eli.torres — sliding window explains the variance. will implement token bucket on client side.

    re: batch — any webhook ordering guarantees if I chunk and retry? don't want completion events firing out of sequence.

  • webhook ordering: no strict guarantee across separate requests. if you fire job A then job B, and B's worker finishes faster, you'll get B's work_order.created before A's. same for status changes.

    if you need ordering, you'd have to implement sequencing in your receiver (check parent job refs, etc) or... honestly, sequential processing with confirmation before next request. slower but guaranteed.

    there's a feature request for transactional batching with ordered webhook delivery — upvote it if you find it in the feedback board. would solve exactly this.

  • YMMV but we ended up using a queue worker with Batch updates vs single record API calls pattern — basically攒 50 records, fire them off with 100ms spacing, then checkpoint. 5K records took ~6 minutes vs estimated 50+ with naive retry loops.

    worth noting: FieldPulse's external_id field is your friend here. set it to your source record ID so if you have to resume, you can query by that to see what's already migrated.

    heads up: custom fields in bulk creation have a separate validation path that's stricter than the web UI. we hit this with date formats — ISO 8601 only, no "3/15/2025" style strings.