Skip to main content
AI/MLjeremylongshore

clari-rate-limits

'Handle Clari API rate limits with backoff and export job scheduling.

Stars
2,267
Source
jeremylongshore/claude-code-plugins-plus-skills
Updated
2026-05-31
Slug
jeremylongshore--claude-code-plugins-plus-skills--clari-rate-limits
View on GitHubRaw SKILL.md

// install — copy + paste into any project

mkdir -p .claude/skills && curl -fsSL https://raw.githubusercontent.com/jeremylongshore/claude-code-plugins-plus-skills/HEAD/plugins/saas-packs/clari-pack/skills/clari-rate-limits/SKILL.md -o .claude/skills/clari-rate-limits.md

Drops the SKILL.md into .claude/skills/clari-rate-limits.md. Works with Claude Code, Cursor, and any agent that loads SKILL.md files from .claude/skills/.

Clari Rate Limits

Overview

The Clari API enforces rate limits per API key. Export jobs are asynchronous and queued server-side, so the primary concern is polling frequency and concurrent export requests.

Prerequisites

  • A scoped API token and approved forecast export scope
  • Persistent job/attempt tracking for retry and idempotency decisions
  • A scheduler that can defer work without spawning duplicate workers
  • Monitoring for queue depth, response class, and terminal failures

Rate Limit Behavior

Aspect Value
Scope Per API key
Response on limit HTTP 429
Export job queue Server-managed, async
Recommended polling 5-10 second intervals

Instructions

Exponential Backoff for Export Polling

import time
import requests

def poll_with_backoff(
    job_id: str,
    api_key: str,
    max_attempts: int = 60,
    base_delay: float = 5.0,
    max_delay: float = 60.0,
) -> dict:
    for attempt in range(max_attempts):
        resp = requests.get(
            f"https://api.clari.com/v4/export/jobs/{job_id}",
            headers={"apikey": api_key},
        )

        if resp.status_code == 429:
            retry_after = int(resp.headers.get("Retry-After", base_delay))
            time.sleep(retry_after)
            continue

        resp.raise_for_status()
        status = resp.json()

        if status["status"] in ("COMPLETED", "FAILED"):
            return status

        delay = min(base_delay * (1.5 ** attempt), max_delay)
        time.sleep(delay)

    raise TimeoutError(f"Job {job_id} did not complete in {max_attempts} attempts")

Sequential Export Scheduler

def export_all_periods(
    client,
    forecast_name: str,
    periods: list[str],
    delay_between: float = 10.0,
) -> list[dict]:
    results = []
    for period in periods:
        print(f"Exporting {period}...")
        job = client.export_forecast(forecast_name, period)
        result = poll_with_backoff(job["jobId"], client.config.api_key)
        results.append(result)
        time.sleep(delay_between)  # Avoid hitting rate limits
    return results

Error Handling

Scenario Detection Response
429 with Retry-After Check header Wait exact duration
429 without header Status code only Backoff from 5s
Job queue full Multiple pending jobs Wait for completion before new exports

Output

Return a per-job state record with export period, provider job ID, attempts, applied delay, response class, and terminal decision. Do not report tokens or raw forecast records; callers must receive a bounded retryable failure rather than silently starting a parallel export.

Examples

Submit one export for a staging period and persist its job ID before polling. On a 429 with Retry-After, reschedule the same job after that interval; if the attempt budget expires, mark the run unavailable and alert the scheduler instead of issuing another export request.

Resources

Next Steps

For security configuration, see clari-security-basics.