From clari-pack
Builds typed Python and TypeScript clients for Clari v4 REST API, handling forecast exports, job polling, and data pipelines.
How this skill is triggered — by the user, by Claude, or both
Slash command
/clari-pack:clari-sdk-patternsThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Clari has no official SDK -- build typed wrappers around the v4 REST API. These patterns cover the Export API for forecasts, job polling, and data transformation pipelines.
Clari has no official SDK -- build typed wrappers around the v4 REST API. These patterns cover the Export API for forecasts, job polling, and data transformation pipelines.
clari-install-auth setup# clari_client.py
import os
import time
import requests
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class ClariConfig:
api_key: str
base_url: str = "https://api.clari.com/v4"
poll_interval: int = 5
max_poll_attempts: int = 60
class ClariClient:
def __init__(self, config: Optional[ClariConfig] = None):
self.config = config or ClariConfig(
api_key=os.environ["CLARI_API_KEY"]
)
self.session = requests.Session()
self.session.headers.update({
"apikey": self.config.api_key,
"Content-Type": "text/plain",
})
def list_forecasts(self) -> list[dict]:
resp = self.session.get(f"{self.config.base_url}/export/forecast/list")
resp.raise_for_status()
return resp.json()["forecasts"]
def export_forecast(
self,
forecast_name: str,
time_period: str,
types: list[str] = None,
currency: str = "USD",
export_format: str = "JSON",
) -> dict:
payload = {
"timePeriod": time_period,
"typesToExport": types or [
"forecast", "quota", "forecast_updated",
"adjustment", "crm_total", "crm_closed"
],
"currency": currency,
"schedule": "NONE",
"includeHistorical": False,
"exportFormat": export_format,
}
resp = self.session.post(
f"{self.config.base_url}/export/forecast/{forecast_name}",
json=payload,
)
resp.raise_for_status()
return resp.json()
def wait_for_job(self, job_id: str) -> dict:
for attempt in range(self.config.max_poll_attempts):
resp = self.session.get(
f"{self.config.base_url}/export/jobs/{job_id}",
)
resp.raise_for_status()
status = resp.json()
if status["status"] == "COMPLETED":
return status
if status["status"] == "FAILED":
raise ClariExportError(f"Job {job_id} failed: {status}")
time.sleep(self.config.poll_interval)
raise ClariExportError(f"Job {job_id} timed out after {self.config.max_poll_attempts} attempts")
def download_export(self, download_url: str) -> dict:
resp = requests.get(download_url)
resp.raise_for_status()
return resp.json()
def export_and_download(
self, forecast_name: str, time_period: str
) -> dict:
job = self.export_forecast(forecast_name, time_period)
completed = self.wait_for_job(job["jobId"])
return self.download_export(completed["downloadUrl"])
class ClariExportError(Exception):
pass
// clari-client.ts
interface ClariConfig {
apiKey: string;
baseUrl?: string;
pollIntervalMs?: number;
maxPollAttempts?: number;
}
interface ForecastExport {
entries: ForecastEntry[];
}
interface ForecastEntry {
ownerName: string;
ownerEmail: string;
forecastAmount: number;
quotaAmount: number;
crmTotal: number;
crmClosed: number;
adjustmentAmount: number;
timePeriod: string;
}
class ClariClient {
private apiKey: string;
private baseUrl: string;
private pollIntervalMs: number;
private maxPollAttempts: number;
constructor(config: ClariConfig) {
this.apiKey = config.apiKey;
this.baseUrl = config.baseUrl ?? "https://api.clari.com/v4";
this.pollIntervalMs = config.pollIntervalMs ?? 5000;
this.maxPollAttempts = config.maxPollAttempts ?? 60;
}
private async request<T>(path: string, options?: RequestInit): Promise<T> {
const response = await fetch(`${this.baseUrl}${path}`, {
...options,
headers: {
apikey: this.apiKey,
"Content-Type": "text/plain",
...options?.headers,
},
});
if (!response.ok) {
throw new Error(`Clari API ${response.status}: ${await response.text()}`);
}
return response.json();
}
async listForecasts(): Promise<{ forecasts: any[] }> {
return this.request("/export/forecast/list");
}
async exportForecast(forecastName: string, timePeriod: string): Promise<any> {
return this.request(`/export/forecast/${forecastName}`, {
method: "POST",
body: JSON.stringify({
timePeriod,
typesToExport: ["forecast", "quota", "crm_total", "crm_closed"],
currency: "USD",
schedule: "NONE",
includeHistorical: false,
exportFormat: "JSON",
}),
});
}
async exportAndDownload(
forecastName: string,
timePeriod: string
): Promise<ForecastExport> {
const job = await this.exportForecast(forecastName, timePeriod);
const completed = await this.waitForJob(job.jobId);
const resp = await fetch(completed.downloadUrl);
return resp.json();
}
private async waitForJob(jobId: string): Promise<any> {
for (let i = 0; i < this.maxPollAttempts; i++) {
const status = await this.request(`/export/jobs/${jobId}`);
if (status.status === "COMPLETED") return status;
if (status.status === "FAILED") throw new Error(`Job failed: ${jobId}`);
await new Promise((r) => setTimeout(r, this.pollIntervalMs));
}
throw new Error(`Job ${jobId} timed out`);
}
}
| Status | Meaning | Action |
|---|---|---|
| 401 | Invalid API key | Regenerate token |
| 403 | Insufficient permissions | Admin must grant API access |
| 404 | Wrong forecast name | List forecasts first |
| 429 | Rate limited | Back off and retry |
Apply patterns in clari-core-workflow-a for forecast export pipelines.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin clari-packExports Clari forecasts and pipeline snapshots via REST API. Tests connectivity, polls job status, downloads JSON data. For initial integrations post-auth.
Builds typed SalesLoft REST API v2 clients in TypeScript with pagination iterators, rate-limit handling, and axios singleton. For integrations and custom SDKs.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.