Downloads em Lote com Webhooks
Submeta múltiplos jobs de download de vídeo de uma vez e receba notificações webhook quando cada um terminar — sem necessidade de polling.
Visão geral
Em vez de fazer polling, pode passar um webhook_url ao submeter um job. O HuntAPI irá enviar um POST com o resultado para o seu URL assim que o download estiver pronto. Este guia mostra como:
- Submeter um lote de jobs com um URL webhook
- Receber e processar o payload webhook
Pré-requisitos
- Uma chave de API HuntAPI — obtenha uma em app.huntapi.com
- Um endpoint HTTP acessível publicamente (use ngrok para testes locais)
- Instale as dependências para o seu idioma:
pip install requests flasknpm install express @types/expressExtensão curl ativada (ativa por padrão).
Sem dependências adicionais — utiliza net/http (Go 1.18+).
Sem dependências adicionais — utiliza com.sun.net.httpserver (integrado, Java 6+).
Sem dependências adicionais — utiliza ASP.NET minimal API (.NET 6+).
# Cargo.toml
[dependencies]
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["full"] }
serde_json = "1"
axum = "0.7"Passos
Submeter um lote de jobs com um URL webhook
Passe o seu endpoint publicamente acessível como webhook_url. Cada job é independente; o HuntAPI chamará o webhook quando terminar.
import requests
API_KEY = "YOUR_API_KEY"
WEBHOOK_URL = "https://yourserver.example.com/webhooks/huntapi"
URLS = [
"https://www.youtube.com/watch?v=VIDEO_ID_1",
"https://www.youtube.com/watch?v=VIDEO_ID_2",
"https://www.youtube.com/watch?v=VIDEO_ID_3",
]
job_ids = []
for video_url in URLS:
r = requests.get(
"https://api.huntapi.com/v1/video/download",
headers={"x-api-key": API_KEY},
params={"url": video_url, "quality": "best", "webhook_url": WEBHOOK_URL},
)
job_id = r.json()["job_id"]
job_ids.append(job_id)
print(f"Submetido: {job_id}")
print(f"\n{len(job_ids)} jobs submetidos. A aguardar webhooks...")const API_KEY = "YOUR_API_KEY";
const WEBHOOK_URL = "https://yourserver.example.com/webhooks/huntapi";
const URLS = [
"https://www.youtube.com/watch?v=VIDEO_ID_1",
"https://www.youtube.com/watch?v=VIDEO_ID_2",
"https://www.youtube.com/watch?v=VIDEO_ID_3",
];
const jobIds: string[] = [];
for (const videoUrl of URLS) {
const params = new URLSearchParams({ url: videoUrl, quality: "best", webhook_url: WEBHOOK_URL });
const res = await fetch(`https://api.huntapi.com/v1/video/download?${params}`, {
headers: { "x-api-key": API_KEY },
});
const { job_id } = await res.json();
jobIds.push(job_id);
console.log(`Submetido: ${job_id}`);
}
console.log(`\n${jobIds.length} jobs submetidos. A aguardar webhooks...`);<?php
$apiKey = "YOUR_API_KEY";
$webhookUrl = "https://yourserver.example.com/webhooks/huntapi";
$urls = [
"https://www.youtube.com/watch?v=VIDEO_ID_1",
"https://www.youtube.com/watch?v=VIDEO_ID_2",
"https://www.youtube.com/watch?v=VIDEO_ID_3",
];
$jobIds = [];
foreach ($urls as $videoUrl) {
$params = http_build_query(["url" => $videoUrl, "quality" => "best", "webhook_url" => $webhookUrl]);
$ch = curl_init("https://api.huntapi.com/v1/video/download?{$params}");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["x-api-key: {$apiKey}"]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
$jobIds[] = $data["job_id"];
echo "Submetido: {$data['job_id']}\n";
}
echo count($jobIds) . " jobs submetidos. A aguardar webhooks...\n";package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
)
func submitJob(apiKey, videoURL, webhookURL string) string {
params := url.Values{"url": {videoURL}, "quality": {"best"}, "webhook_url": {webhookURL}}
req, _ := http.NewRequest("GET", "https://api.huntapi.com/v1/video/download?"+params.Encode(), nil)
req.Header.Set("x-api-key", apiKey)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var data map[string]any
json.Unmarshal(body, &data)
return data["job_id"].(string)
}
func main() {
apiKey := "YOUR_API_KEY"
webhookURL := "https://yourserver.example.com/webhooks/huntapi"
urls := []string{
"https://www.youtube.com/watch?v=VIDEO_ID_1",
"https://www.youtube.com/watch?v=VIDEO_ID_2",
"https://www.youtube.com/watch?v=VIDEO_ID_3",
}
for _, u := range urls {
jobID := submitJob(apiKey, u, webhookURL)
fmt.Println("Submetido:", jobID)
}
fmt.Printf("%d jobs submetidos. A aguardar webhooks...\n", len(urls))
}import java.net.URI;
import java.net.URLEncoder;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import org.json.*;
var client = HttpClient.newHttpClient();
var apiKey = "YOUR_API_KEY";
var webhookUrl = "https://yourserver.example.com/webhooks/huntapi";
var urls = new String[]{
"https://www.youtube.com/watch?v=VIDEO_ID_1",
"https://www.youtube.com/watch?v=VIDEO_ID_2",
"https://www.youtube.com/watch?v=VIDEO_ID_3",
};
for (var videoUrl : urls) {
var encoded = URLEncoder.encode(videoUrl, StandardCharsets.UTF_8);
var wh = URLEncoder.encode(webhookUrl, StandardCharsets.UTF_8);
var url = "https://api.huntapi.com/v1/video/download?url=" + encoded + "&quality=best&webhook_url=" + wh;
var req = HttpRequest.newBuilder().uri(URI.create(url))
.header("x-api-key", apiKey).GET().build();
var resp = client.send(req, HttpResponse.BodyHandlers.ofString());
var jobId = new JSONObject(resp.body()).getString("job_id");
System.out.println("Submetido: " + jobId);
}using System.Net.Http;
using System.Text.Json;
var apiKey = "YOUR_API_KEY";
var webhookUrl = Uri.EscapeDataString("https://yourserver.example.com/webhooks/huntapi");
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("x-api-key", apiKey);
var urls = new[]
{
"https://www.youtube.com/watch?v=VIDEO_ID_1",
"https://www.youtube.com/watch?v=VIDEO_ID_2",
"https://www.youtube.com/watch?v=VIDEO_ID_3",
};
foreach (var videoUrl in urls)
{
var encoded = Uri.EscapeDataString(videoUrl);
var body = await client.GetStringAsync(
$"https://api.huntapi.com/v1/video/download?url={encoded}&quality=best&webhook_url={webhookUrl}");
var jobId = JsonDocument.Parse(body).RootElement.GetProperty("job_id").GetString();
Console.WriteLine($"Submetido: {jobId}");
}use reqwest::Client;
use serde_json::Value;
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let client = Client::new();
let api_key = "YOUR_API_KEY";
let webhook_url = "https://yourserver.example.com/webhooks/huntapi";
let urls = [
"https://www.youtube.com/watch?v=VIDEO_ID_1",
"https://www.youtube.com/watch?v=VIDEO_ID_2",
"https://www.youtube.com/watch?v=VIDEO_ID_3",
];
for video_url in &urls {
let data = client.get("https://api.huntapi.com/v1/video/download")
.header("x-api-key", api_key)
.query(&[("url", video_url), ("quality", &"best"), ("webhook_url", &webhook_url)])
.send().await?.json::<Value>().await?;
println!("Submetido: {}", data["job_id"].as_str().unwrap_or(""));
}
Ok(())
}Criar um recetor de webhook
O HuntAPI irá enviar um POST com um corpo JSON ao seu endpoint com job_id, status e download_url quando o job terminar.
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/webhooks/huntapi", methods=["POST"])
def huntapi_webhook():
payload = request.get_json()
job_id = payload.get("job_id")
status = payload.get("status")
download_url = payload.get("download_url")
print(f"Webhook recebido — Job: {job_id}, Estado: {status}")
if status == "done" and download_url:
print(f" URL de download: {download_url}")
return jsonify({"received": True}), 200
if __name__ == "__main__":
app.run(port=3000)import express from "express";
const app = express();
app.use(express.json());
app.post("/webhooks/huntapi", (req, res) => {
const { job_id, status, download_url } = req.body;
console.log(`Webhook recebido — Job: ${job_id}, Estado: ${status}`);
if (status === "done" && download_url) {
console.log(` URL de download: ${download_url}`);
}
res.json({ received: true });
});
app.listen(3000, () => console.log("Recetor de webhook em :3000"));<?php
// webhook.php — coloque este ficheiro num servidor público
$payload = json_decode(file_get_contents("php://input"), true);
$jobId = $payload["job_id"] ?? "";
$status = $payload["status"] ?? "";
$downloadUrl = $payload["download_url"] ?? "";
error_log("Webhook recebido — Job: {$jobId}, Estado: {$status}");
if ($status === "done" && $downloadUrl) {
error_log(" URL de download: {$downloadUrl}");
}
http_response_code(200);
echo json_encode(["received" => true]);package main
import (
"encoding/json"
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/webhooks/huntapi", func(w http.ResponseWriter, r *http.Request) {
var payload map[string]any
json.NewDecoder(r.Body).Decode(&payload)
jobID := payload["job_id"]
status := payload["status"]
downloadURL := payload["download_url"]
fmt.Printf("Webhook recebido — Job: %v, Estado: %v\n", jobID, status)
if status == "done" && downloadURL != nil {
fmt.Println(" URL de download:", downloadURL)
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"received":true}`))
})
fmt.Println("Recetor de webhook em :3000")
http.ListenAndServe(":3000", nil)
}import com.sun.net.httpserver.*;
import java.net.InetSocketAddress;
import org.json.*;
public class WebhookServer {
public static void main(String[] args) throws Exception {
var server = HttpServer.create(new InetSocketAddress(3000), 0);
server.createContext("/webhooks/huntapi", exchange -> {
var body = exchange.getRequestBody().readAllBytes();
var payload = new JSONObject(new String(body));
var jobId = payload.optString("job_id");
var status = payload.optString("status");
var downloadUrl = payload.optString("download_url");
System.out.printf("Webhook recebido — Job: %s, Estado: %s%n", jobId, status);
if ("done".equals(status) && !downloadUrl.isEmpty()) {
System.out.println(" URL de download: " + downloadUrl);
}
var resp = "{\"received\":true}".getBytes();
exchange.sendResponseHeaders(200, resp.length);
exchange.getResponseBody().write(resp);
exchange.close();
});
server.start();
System.out.println("Recetor de webhook em :3000");
}
}using System.Text.Json;
var app = WebApplication.Create();
app.MapPost("/webhooks/huntapi", async (HttpContext ctx) =>
{
using var reader = new StreamReader(ctx.Request.Body);
var body = await reader.ReadToEndAsync();
var payload = JsonDocument.Parse(body).RootElement;
var jobId = payload.GetProperty("job_id").GetString();
var status = payload.GetProperty("status").GetString();
var downloadUrl = payload.TryGetProperty("download_url", out var d) ? d.GetString() : null;
Console.WriteLine($"Webhook recebido — Job: {jobId}, Estado: {status}");
if (status == "done" && downloadUrl != null)
Console.WriteLine($" URL de download: {downloadUrl}");
return Results.Json(new { received = true });
});
Console.WriteLine("Recetor de webhook em :3000");
app.Run("http://0.0.0.0:3000");use axum::{extract::Json as AxumJson, routing::post, Router};
use serde_json::{json, Value};
async fn huntapi_webhook(AxumJson(payload): AxumJson<Value>) -> AxumJson<Value> {
let job_id = payload["job_id"].as_str().unwrap_or("");
let status = payload["status"].as_str().unwrap_or("");
let download_url = payload["download_url"].as_str().unwrap_or("");
println!("Webhook recebido — Job: {}, Estado: {}", job_id, status);
if status == "done" && !download_url.is_empty() {
println!(" URL de download: {}", download_url);
}
AxumJson(json!({ "received": true }))
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/webhooks/huntapi", post(huntapi_webhook));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("Recetor de webhook em :3000");
axum::serve(listener, app).await.unwrap();
}Testar localmente com ngrok
Durante o desenvolvimento, use ngrok para expor o seu servidor local à internet para que o HuntAPI possa alcançar o seu webhook.
# Inicie o seu servidor local primeiro, depois:
ngrok http 3000Copie o URL gerado https://xxxx.ngrok.io e use-o como parâmetro webhook_url.
O seu endpoint webhook deve devolver um código de estado 2xx em 10 segundos, caso contrário o HuntAPI voltará a tentar a entrega. Torne o processamento pesado assíncrono e reconheça o webhook imediatamente.