> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shappire.tools/llms.txt
> Use this file to discover all available pages before exploring further.

# Polling e retentativas

> Como aguardar jobs, backoff e idempotência

Downloads são assíncronos. Após `POST /media/download` você recebe `job_id` e deve consultar `GET /jobs/{jobId}` até conclusão.

## Intervalo recomendado

| Fase           | Intervalo |
| -------------- | --------- |
| Primeiros 30 s | 2–3 s     |
| Após 30 s      | 5 s       |
| Após 2 min     | 10 s      |

## Polling com backoff

```javascript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
async function pollJob(jobId) {
  let delay = 2000;
  for (let i = 0; i < 60; i++) {
    const job = await getJob(jobId);
    if (job.status === 'completed') return job;
    if (job.status === 'failed') throw new Error(job.error?.code);
    await sleep(delay);
    delay = Math.min(delay * 1.5, 10000);
  }
  throw new Error('timeout');
}
```

## Retentativa em 429

```python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
def request_with_retry(fn, max_attempts=5):
    for attempt in range(max_attempts):
        try:
            return fn()
        except requests.HTTPError as exc:
            if exc.response.status_code != 429:
                raise
            reset = int(exc.response.headers.get("X-RateLimit-Reset", 0))
            time.sleep(max(reset - time.time(), 2 ** attempt))
    raise RuntimeError("rate limit exceeded")
```

## Idempotência

* Guarde `job_id` no seu banco antes de fazer polling
* Não enfileire o mesmo download duas vezes para a mesma URL sem checar jobs ativos
* Use `request_id` do header `X-Request-Id` nos logs

## Quando parar

| Situação                       | Ação                        |
| ------------------------------ | --------------------------- |
| `completed`                    | Sucesso — baixar arquivo    |
| `failed` + `media_unavailable` | Não retentar — URL inválida |
| `failed` + `internal_error`    | Retentar com backoff        |
| `404` no job                   | Expirou — novo download     |
| `429`                          | Aguardar reset              |

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  curl https://api.shappire.tools/v1/jobs/job_xyz789 \
    -H "X-API-Key: $SHAPPIRE_API_KEY"
  ```

  ```javascript Node.js theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  const API_KEY = process.env.SHAPPIRE_API_KEY;
  const BASE = 'https://api.shappire.tools/v1';

  export async function waitForJob(jobId, { intervalMs = 3000, timeoutMs = 300000 } = {}) {
    const started = Date.now();

    while (Date.now() - started < timeoutMs) {
      const res = await fetch(`${BASE}/jobs/${jobId}`, {
        headers: { 'X-API-Key': API_KEY },
      });
      const { data } = await res.json();

      if (data.status === 'completed') return data;
      if (data.status === 'failed') throw new Error(data.error?.message ?? 'job failed');

      await new Promise((r) => setTimeout(r, intervalMs));
    }

    throw new Error('job timeout');
  }
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  import time

  def wait_for_job(job_id: str, interval: float = 3.0, timeout: float = 300.0) -> dict:
      started = time.time()
      while time.time() - started < timeout:
          response = requests.get(
              f"{BASE}/jobs/{job_id}",
              headers={"X-API-Key": API_KEY},
              timeout=30,
          )
          response.raise_for_status()
          data = response.json()["data"]

          if data["status"] == "completed":
              return data
          if data["status"] == "failed":
              raise RuntimeError(data.get("error", {}).get("message", "job failed"))

          time.sleep(interval)

      raise TimeoutError("job timeout")
  ```

  ```go Go theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  func WaitForJob(jobID string) (map[string]interface{}, error) {
  	for {
  		req, _ := http.NewRequest(http.MethodGet, baseURL+"/jobs/"+jobID, nil)
  		req.Header.Set("X-API-Key", os.Getenv("SHAPPIRE_API_KEY"))

  		resp, err := http.DefaultClient.Do(req)
  		if err != nil {
  			return nil, err
  		}
  		defer resp.Body.Close()

  		var out struct {
  			Data map[string]interface{} `json:"data"`
  		}
  		json.NewDecoder(resp.Body).Decode(&out)

  		status, _ := out.Data["status"].(string)
  		switch status {
  		case "completed":
  			return out.Data, nil
  		case "failed":
  			return nil, fmt.Errorf("job failed")
  		}

  		time.Sleep(3 * time.Second)
  	}
  }
  ```

  ```php PHP theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  function waitForJob(string $jobId, int $timeoutSeconds = 300): array {
      global $apiKey, $base;
      $started = time();

      while (time() - $started < $timeoutSeconds) {
          $ch = curl_init("$base/jobs/$jobId");
          curl_setopt_array($ch, [
              CURLOPT_RETURNTRANSFER => true,
              CURLOPT_HTTPHEADER => ["X-API-Key: $apiKey"],
          ]);
          $body = curl_exec($ch);
          curl_close($ch);

          $data = json_decode($body, true)['data'];
          if ($data['status'] === 'completed') return $data;
          if ($data['status'] === 'failed') throw new RuntimeException('job failed');

          sleep(3);
      }

      throw new RuntimeException('job timeout');
  }
  ```

  ```ruby Ruby theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  def wait_for_job(job_id, interval: 3, timeout: 300)
    started = Time.now
    loop do
      uri = URI("#{BASE}/jobs/#{job_id}")
      req = Net::HTTP::Get.new(uri)
      req['X-API-Key'] = API_KEY
      res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
      data = JSON.parse(res.body)['data']

      return data if data['status'] == 'completed'
      raise 'job failed' if data['status'] == 'failed'
      raise 'job timeout' if Time.now - started > timeout

      sleep interval
    end
  end
  ```

  ```java Java theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  public String getJob(String jobId) throws Exception {
    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create(BASE + "/jobs/" + jobId))
        .header("X-API-Key", apiKey)
        .GET()
        .build();

    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
    return response.body();
  }
  ```
</CodeGroup>
