DEVUP Docs
Back to Dashboard

Account & Security

Webhooks

Receive inference results asynchronously via HTTP callbacks.

Webhooks are a feature of the DEVUP Native API. They are not supported with the OpenAI-compatible API. Webhooks let you submit an inference request and receive the result via an HTTP callback, instead of waiting for the response synchronously. This is useful for long-running requests or fire-and-forget workloads.

How it works

Add a webhook parameter to your request. The API immediately responds with status queued, then dispatches the result to your webhook URL within about a minute once inference is complete.

Submitting an asynchronous request

Webhooks are exclusively supported on the Native API endpoint POST /v1/inference/{model}. The webhook parameter is silently ignored on all OpenAI-compatible endpoints.

curl "https://api.devupai.com/v1/inference/deepseek-ai/DeepSeek-V4-Pro" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $DEVUP_API_KEY" \
  -d '{
    "input": "Explain quantum computing in three sentences.",
    "parameters": {
      "max_new_tokens": 200,
      "temperature": 0.7
    },
    "webhook": "https://your-app.com/devupai-webhook"
  }'
⚠️

Set generation limits on raw input

Native API requests accept raw string inputs. Omitting a stop sequence or max_new_tokens limit can cause models to generate unbounded text until hitting context limits. In testing, the single word input "Hello" generated 21,278 and 31,454 completion tokens, costing 6.63 DZD and 9.80 DZD.

Immediate API Response

When the request is accepted, the API returns a 200 OK response confirming the job is queued:

json
{
  "id": "inf-4a8f9b2c-1d3e-4f5a-8b7c-9d0e1f2a3b4c",
  "model": "deepseek-ai/DeepSeek-V4-Pro",
  "inference_status": { "status": "queued" },
  "_devup": { "reserved_dzd": 1, "status": "pending_settlement" }
}

Understanding reserved_dzd

reserved_dzd is a temporary authorization hold placed on your account balance, not the final charge. The actual cost is calculated when generation finishes and settled when the delivery callback is sent. If the actual cost is lower than the reservation, the difference is released back to your balance immediately upon settlement.

Delivery callback

When inference completes, the webhook relay issues an HTTP POST request to your specified callback URL with the following headers:

http
X-DevUp-Signature: t=1740000000,v1=5d41402abc4b2a76b9719d911017c592b0c513e9a0c79326e479d268d06dfcfb
X-DevUp-Delivery-Id: whd_550e8400-e29b-41d4-a716-446655440000
User-Agent: DevUp-Webhook/1.0

Success is determined by the status field, which the relay derives from whether results were generated. The results and error fields are mutually exclusive: a delivery payload contains one or the other, never both, and never a results: null field beside an error object.

Successful Delivery Payload

When inference completes successfully, the request body contains:

json
{
  "id": "whd_550e8400-e29b-41d4-a716-446655440000",
  "status": "succeeded",
  "usage": { "prompt_tokens": 1, "completion_tokens": 249 },
  "cost_dzd": 6.6282,
  "settlement": "settled",
  "results": [
    {
      "generated_text": "Quantum computing harnesses quantum mechanical phenomena such as superposition and entanglement to process complex computations. Unlike classical bits that are strictly 0 or 1, qubits exist across multidimensional probability states simultaneously. This enables massive parallel computational advantages for specific cryptographic, optimization, and simulation workloads."
    }
  ]
}

Failed Delivery Payload

If generation or upstream execution fails, the payload replaces results with the error descriptor:

json
{
  "id": "whd_550e8400-e29b-41d4-a716-446655440000",
  "status": "failed",
  "usage": { "prompt_tokens": 0, "completion_tokens": 0 },
  "cost_dzd": null,
  "settlement": "failed",
  "error": {
    "type": "inference_failed",
    "message": "The inference request did not complete successfully."
  }
}

The error.message field is a fixed string and error.type is currently always "inference_failed" — integrations must not branch on the message text.

Verifying signatures

Every delivery includes an X-DevUp-Signature header containing a timestamp and one or more HMAC-SHA256 signatures in the format t=<unix>,v1=<hex>[,v1=<hex>].

Signing secrets are per-user and are obtained from the dashboard at /dashboard/webhooks.

During a secret rotation window, the header carries more than one v1 value; accept the payload if ANY of them matches, eliminating the need for coordinated cutovers.

Compute HMAC on raw request body bytes

The HMAC must be computed over the raw request body before any JSON parsing. Re-serialising a parsed object changes the bytes and the signature will never match.

# Python (Standard Library: hmac, hashlib, time)
import hmac
import hashlib
import time

def verify_webhook_signature(
    raw_body: bytes,
    signature_header: str,
    secret: str,
    tolerance_seconds: int = 300
) -> bool:
    """
    Verifies the X-DevUp-Signature header on an incoming webhook request.

    CRITICAL: `raw_body` MUST be the raw bytes from the request body before any JSON deserialization.
    Re-serialising the parsed object changes the bytes and the signature will never match.

    Header format: X-DevUp-Signature: t=<unix_timestamp>,v1=<hex_signature>[,v1=<hex_signature>]
    """
    if not signature_header or not secret:
        return False

    timestamp = None
    signatures = []

    for element in signature_header.split(","):
        parts = element.strip().split("=", 1)
        if len(parts) == 2:
            key, value = parts[0], parts[1]
            if key == "t":
                try:
                    timestamp = int(value)
                except ValueError:
                    return False
            elif key == "v1":
                signatures.append(value)

    if timestamp is None or not signatures:
        return False

    # Prevent replay attacks
    now = int(time.time())
    if abs(now - timestamp) > tolerance_seconds:
        return False

    # Compute expected HMAC over f"{timestamp}.{raw_body}"
    if isinstance(raw_body, str):
        raw_body = raw_body.encode("utf-8")

    payload_to_sign = f"{timestamp}.".encode("utf-8") + raw_body
    secret_bytes = secret.encode("utf-8")

    expected_signature = hmac.new(
        secret_bytes,
        payload_to_sign,
        hashlib.sha256
    ).hexdigest()

    # Accept if ANY v1 matches (timing-safe comparison).
    # Multiple v1 values appear during a 24h secret rotation window.
    return any(
        hmac.compare_digest(expected_signature, sig)
        for sig in signatures
    )
⚠️
Use crypto.timingSafeEqual in Node.js or hmac.compare_digest in Python instead of standard equality operators to prevent timing-based attacks.
⚠️

Deduplicate on X-DevUp-Delivery-Id, not the signature

Always key idempotency checks on the X-DevUp-Delivery-Id header. The delivery ID is generated once and is identical across every retry attempt. Because the signature and its t= timestamp are recomputed on each attempt, a consumer that keys idempotency on the signature will treat a legitimate retry as a new event and process the same result twice.

Delivery guarantees

When inference completes, the relay encrypts the result and queues it for delivery. A background worker dispatches deliveries every 60 seconds, meaning the first delivery attempt lands within about a minute of completion.

If your endpoint is unreachable or returns a retryable status (HTTP 408, 429, any 5xx, timeouts, or network failures), delivery is retried up to 8 times using exponential backoff (1m, 2m, 4m, 8m, 16m, capped at 30m, plus jitter), providing roughly one hour of retry coverage. Non-retryable client errors (any other 4xx status, such as 401 or 404) fail immediately without retry.

Each attempt has a request timeout of 5 seconds over HTTPS (redirects are not followed). Return 2xx quickly and process the payload asynchronously on your side.

Signing secrets are re-read at delivery time. If no active secret exists when the result is initially queued, adding one inside the retry window allows subsequent attempts to sign and deliver successfully. Stored results are encrypted at rest and purged 6 hours after being queued.

You can view and rotate your signing secrets at any time in the Webhooks Dashboard.