> ## 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.

# Download assíncrono

> Tutorial — enfileirar jobs, polling e URL assinada de download

Downloads passam por um **worker** assíncrono. A API retorna `202 Accepted` com `job_id` — consulte o status até `completed`.

## Enfileirar

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  curl -X POST https://api.shappire.tools/v1/media/download \
    -H "X-API-Key: $SHAPPIRE_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"media_id":"med_abc123","format":"video_best"}'
  ```

  ```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 enqueueDownload(mediaId, format) {
    const res = await fetch(`${BASE}/media/download`, {
      method: 'POST',
      headers: {
        'X-API-Key': API_KEY,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ media_id: mediaId, format }),
    });

    if (res.status !== 202) {
      const err = await res.json();
      throw new Error(err.error?.message ?? res.statusText);
    }

    const { data } = await res.json();
    return data.job_id;
  }
  ```

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

  API_KEY = os.environ["SHAPPIRE_API_KEY"]
  BASE = "https://api.shappire.tools/v1"

  def enqueue_download(media_id: str, format: str) -> str:
      response = requests.post(
          f"{BASE}/media/download",
          headers={"X-API-Key": API_KEY},
          json={"media_id": media_id, "format": format},
          timeout=30,
      )
      response.raise_for_status()
      return response.json()["data"]["job_id"]
  ```

  ```go Go theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  func EnqueueDownload(mediaID, format string) (string, error) {
  	body, _ := json.Marshal(map[string]string{
  		"media_id": mediaID,
  		"format":   format,
  	})
  	req, err := http.NewRequest(http.MethodPost, baseURL+"/media/download", bytes.NewReader(body))
  	if err != nil {
  		return "", err
  	}
  	req.Header.Set("X-API-Key", os.Getenv("SHAPPIRE_API_KEY"))
  	req.Header.Set("Content-Type", "application/json")

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

  	var out struct {
  		Data struct {
  			JobID string `json:"job_id"`
  		} `json:"data"`
  	}
  	if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
  		return "", err
  	}
  	return out.Data.JobID, nil
  }
  ```

  ```php PHP theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  function enqueueDownload(string $mediaId, string $format): string {
      global $apiKey, $base;

      $ch = curl_init("$base/media/download");
      curl_setopt_array($ch, [
          CURLOPT_POST => true,
          CURLOPT_RETURNTRANSFER => true,
          CURLOPT_HTTPHEADER => [
              "X-API-Key: $apiKey",
              'Content-Type: application/json',
          ],
          CURLOPT_POSTFIELDS => json_encode([
              'media_id' => $mediaId,
              'format' => $format,
          ]),
      ]);

      $body = curl_exec($ch);
      curl_close($ch);
      return json_decode($body, true)['data']['job_id'];
  }
  ```

  ```ruby Ruby theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  def enqueue_download(media_id, format)
    uri = URI("#{BASE}/media/download")
    req = Net::HTTP::Post.new(uri)
    req['X-API-Key'] = API_KEY
    req['Content-Type'] = 'application/json'
    req.body = { media_id: media_id, format: format }.to_json

    res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
    JSON.parse(res.body)['data']['job_id']
  end
  ```

  ```java Java theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  public String enqueueDownload(String mediaId, String format) throws Exception {
    String body = String.format(
        "{\"media_id\":\"%s\",\"format\":\"%s\"}", mediaId, format);
    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create(BASE + "/media/download"))
        .header("X-API-Key", apiKey)
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(body))
        .build();

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

## Polling

<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>

## Resposta inicial (`202`)

```json theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
{
  "data": {
    "job_id": "job_x9y8z7",
    "status": "queued"
  }
}
```

## Job concluído

```json theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
{
  "data": {
    "id": "job_x9y8z7",
    "status": "completed",
    "progress": 100,
    "result": {
      "download_url": "https://...",
      "filename": "video.mp4",
      "mime_type": "video/mp4",
      "size_bytes": null,
      "expires_at": "2026-09-15T12:00:00.000Z"
    }
  }
}
```

<Info>
  `size_bytes` é atualmente sempre `null`. Use `filename` e `mime_type` para identificar o arquivo.
</Info>

## Baixar o arquivo

```bash theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
curl -L -OJ "URL_ASSINADA_DA_RESPOSTA"
```

<Warning>
  URLs assinadas expiram em `result.expires_at` (TTL padrão: 1 hora). Não armazene como link permanente.
</Warning>

## Limite de concorrência

Máximo de **2 jobs ativos** (`queued` + `processing`) por projeto. `429` com `concurrency_limit_exceeded` se exceder — veja [Polling e retentativas](/pages/guides/polling-e-retentativas).
