Browse Docs
Archives (14)
Audio (38)
Documents (26)
Ebooks (7)
Fonts (13)
Images (62)
Video (10)
On This Page

Webhooks

Receive a POST notification when a job completes instead of polling. Pass webhookUrl and webhookSecret when submitting a job and your endpoint will be called as soon as the job reaches a terminal state.

Configuring Webhooks

Include these fields as form fields alongside your file upload in POST /convert:

FieldRequiredDescription
webhookUrlYesHTTPS endpoint that will receive webhook POST requests
webhookSecretYesShared secret for signature verification (16–256 characters)
webhookEventsNoComma-separated event filter: succeeded, failed, or both (default: succeeded,failed)

Example

curl -sS -X POST "https://api.tools.fast/convert" \
  -H "X-Fast-Api-Key: $API_KEY" \
  -F "file=@photo.heic" \
  -F "targetFormat=jpg" \
  -F "webhookUrl=https://example.com/webhooks/fast" \
  -F "webhookSecret=whsec_your_secret_here" \
  -F "webhookEvents=succeeded,failed"

URL Requirements

  • Must use HTTPS (HTTP is rejected)
  • Must use a hostname (IP addresses are not allowed)
  • Maximum 2,048 characters
  • Cannot target .local, .internal, or .localhost domains

Payload Schema

Webhooks are delivered as POST requests with a JSON body:

{
  "event": "job.succeeded",
  "eventId": "019502a3-7c1e-7f8a-9b2d-4e6f8a1b3c5d",
  "jobId": "abc123",
  "jobType": "convert",
  "site": "convert.fast",
  "status": "Succeeded",
  "deliveryAttempt": 1,
  "output": {
    "fileName": "document.pdf",
    "contentType": "application/pdf",
    "fileCount": 1,
    "downloadUrl": "https://s1.convert.fast/api/convert/job/abc123/download?token=...",
    "expiresAt": "2026-02-27T10:00:00Z"
  },
  "metrics": {
    "inputBytes": 204800,
    "outputBytes": 51200,
    "creditCost": 1,
    "processingTimeMs": 1500
  },
  "completedAt": "2026-02-26T10:00:00Z"
}

Field Reference

FieldTypePresentDescription
eventstringAlwaysjob.succeeded or job.failed
eventIdstringAlwaysUnique identifier for this webhook event (idempotency key)
jobIdstringAlwaysThe job ID
jobTypestringAlwaysconvert
sitestringAlwaysconvert.fast
statusstringAlwaysSucceeded or Failed
deliveryAttemptintAlwaysWhich delivery attempt (1–5)
outputobjectOn successPresent only when event is job.succeeded
output.fileNamestringOn successPrimary output filename
output.contentTypestringOn successMIME type of the output
output.fileCountintOn successNumber of output files
output.downloadUrlstringOn successPresigned download URL (see below)
output.expiresAtstringOn successWhen the download URL expires (ISO 8601 UTC)
errorobjectOn failurePresent only when event is job.failed
error.codestringOn failureMachine-readable error code
error.detailstringOn failureHuman-readable error description (max 256 chars)
metricsobjectAlwaysProcessing metrics
metrics.inputByteslongAlwaysInput file size in bytes
metrics.outputByteslongOn successOutput file size in bytes
metrics.creditCostintAlwaysCredits consumed
metrics.processingTimeMslongWhen availableProcessing duration in milliseconds
completedAtstringAlwaysWhen the job completed (ISO 8601 UTC)

Failed Job Payload

{
  "event": "job.failed",
  "eventId": "019502a3-8d2f-7a1b-9c3e-5f7a2b4d6e8f",
  "jobId": "abc123",
  "jobType": "convert",
  "site": "convert.fast",
  "status": "Failed",
  "deliveryAttempt": 1,
  "error": {
    "code": "jobs.timeout",
    "detail": "Processing exceeded timeout limit"
  },
  "metrics": {
    "inputBytes": 204800,
    "outputBytes": null,
    "creditCost": 0,
    "processingTimeMs": null
  },
  "completedAt": "2026-02-26T10:05:00Z"
}

Signature Verification

Every webhook includes an X-Fast-Signature header for verifying authenticity. The format follows Stripe's convention:

X-Fast-Signature: t=1708981200,v1=a1b2c3d4e5f6...

Verification Algorithm

  1. Extract the t (timestamp) and v1 (signature) values from the header
  2. Construct the signed content: {timestamp}.{raw_json_body}
  3. Compute HMAC-SHA256 using your webhook secret as the key
  4. Compare the computed hex digest with v1 using constant-time comparison

Additional Headers

HeaderDescription
X-Fast-Signaturet={unix_timestamp},v1={hex_hmac_sha256}
X-Fast-Event-IdThe eventId (same as in the JSON body)
X-Fast-Delivery-AttemptThe attempt number (same as deliveryAttempt)
Content-Typeapplication/json

Code Examples

Node.js

import crypto from "crypto";

function verifyWebhookSignature(rawBody, signatureHeader, secret) {
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((p) => p.split("=", 2))
  );
  const timestamp = parts.t;
  const signature = parts.v1;

  const signedContent = `${timestamp}.${rawBody}`;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(signedContent)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(signature, "hex"),
    Buffer.from(expected, "hex")
  );
}

// Express middleware example
app.post("/webhooks/fast", express.raw({ type: "application/json" }), (req, res) => {
  const sig = req.headers["x-fast-signature"];
  if (!verifyWebhookSignature(req.body.toString(), sig, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send("Invalid signature");
  }

  const event = JSON.parse(req.body);
  // Process event using event.eventId for idempotency
  console.log(`${event.event} for job ${event.jobId}`);
  res.status(200).send("OK");
});

Python

import hashlib
import hmac

def verify_webhook_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    timestamp = parts["t"]
    signature = parts["v1"]

    signed_content = f"{timestamp}.{raw_body.decode()}"
    expected = hmac.new(
        secret.encode(), signed_content.encode(), hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected)

# Flask example
@app.route("/webhooks/fast", methods=["POST"])
def handle_webhook():
    sig = request.headers.get("X-Fast-Signature")
    if not verify_webhook_signature(request.data, sig, WEBHOOK_SECRET):
        return "Invalid signature", 401

    event = request.json
    # Process event using event["eventId"] for idempotency
    print(f"{event['event']} for job {event['jobId']}")
    return "OK", 200

C#

using System.Security.Cryptography;
using System.Text;

bool VerifyWebhookSignature(string rawBody, string signatureHeader, string secret)
{
    var parts = signatureHeader.Split(',')
        .Select(p => p.Split('=', 2))
        .ToDictionary(p => p[0], p => p[1]);

    var timestamp = parts["t"];
    var signature = parts["v1"];

    var signedContent = $"{timestamp}.{rawBody}";
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(signedContent));
    var expected = Convert.ToHexStringLower(hash);

    return CryptographicOperations.FixedTimeEquals(
        Encoding.UTF8.GetBytes(signature),
        Encoding.UTF8.GetBytes(expected));
}

Retry Policy

If your endpoint does not return a 2xx status code, the webhook is retried with exponential backoff:

AttemptDelay After Failure
1Immediate (initial delivery)
21 second
35 seconds
430 seconds
5120 seconds (2 minutes)

Maximum attempts: 5 (total time window: ~2.5 minutes)

Retryable vs Terminal Responses

Status CodeBehavior
2xxSuccess — delivery complete
429Retryable — too many requests
500+Retryable — server error
Connection failureRetryable — timeout or DNS error
4xx (except 429)Terminal — no more retries

Timeout: Your endpoint must respond within 10 seconds.

Presigned Download URLs

When a job succeeds, the webhook payload includes a output.downloadUrl field containing a presigned URL:

https://s1.convert.fast/api/convert/job/{jobId}/download?token={encrypted_token}
  • Authentication: The token query parameter contains an encrypted, tamper-proof token. No additional authentication headers are needed.
  • Expiration: URLs expire at the time indicated by output.expiresAt (1 hour after job completion).
  • Reusable: Each URL can be used for multiple downloads until it expires.
  • Expired URLs: Return HTTP 401 (invalid/expired token) or HTTP 410 (artifacts deleted).

To download:

curl -o output.pdf "https://s1.convert.fast/api/convert/job/abc123/download?token=..."

Event Filtering

Use webhookEvents to receive only the events you care about:

  • webhookEvents=succeeded — only success notifications
  • webhookEvents=failed — only failure notifications
  • webhookEvents=succeeded,failed — both (default)

Events not matching your filter are silently skipped (no HTTP request is made).

Idempotency

Each webhook delivery includes an eventId that is stable across retry attempts. Use this to deduplicate:

  1. When you receive a webhook, check if you've already processed that eventId
  2. If yes, return 200 OK without reprocessing
  3. If no, process the event and store the eventId

The eventId is unique per job completion event. Retries of the same delivery carry the same eventId with an incremented deliveryAttempt.

Copied.