HiAPI
Blog

Tutorial

Not receiving HiAPI callbacks? A diagnostic and repair guide for Webhook delivery issues

Oct 14, 2026
Not receiving HiAPI callbacks? A diagnostic and repair guide for Webhook delivery issues

When you submitted a task to POST https://api.hiapi.ai/v1/tasks, you included callback.url so that HiAPI would proactively POST a notification to you once the task finished. But although the task already shows success in GET /v1/tasks/{taskId}, your callback endpoint has not received a single request. In eight out of ten cases the problem is not on HiAPI's side, but rather whether the callback URL is reachable, whether the receiver returns 2xx as agreed, and whether polling and callbacks are racing each other. Below, we check item by item against the real callback contract and give repair steps you can apply immediately.

If your task never reaches a final state (data.status stays at queued/handling/archiving for a long time), that is a different kind of problem; see HiAPI task stuck or timed out: diagnosis and retry guide. This article only covers the scenario where the task has already finished but the callback notification is not received.

Symptoms

The typical manifestation is that everything on the task side is normal: GET /v1/tasks/{taskId} returns data.status as success (or fail), but the logs of your callback endpoint deployed on the public internet contain no POST records from HiAPI; or you receive them occasionally but often miss them; or the same taskId is delivered twice and your deduplication logic treats that as an anomaly.

First, establish a fact: a callback is only one way to get results, not the only channel. HiAPI's asynchronous tasks can always be actively polled with GET /v1/tasks/{taskId} to obtain the final state; callbacks (Webhooks) only save you from polling. So "callback not received" does not mean "result cannot be obtained". First use polling to confirm that the task itself is fine, then locate the delivery problem.

The real callback contract (align first, then troubleshoot)

When registering a callback, callback is a top-level field of POST /v1/tasks, at the same level as model and input; it cannot be put inside input:

{
  "model": "gpt-image-2",
  "input": { "prompt": "a calm blue data center" },
  "callback": {
    "url": "https://your-domain.com/hiapi/callback",
    "when": "final"
  }
}

A few points you must remember:

  • callback.url must be a publicly reachable http(s) URL; HiAPI initiates a POST to it from the public internet.
  • callback.when currently only accepts final: it is delivered once when the task reaches a final state, and both success and fail trigger it. There are no progress-based or staged callbacks.
  • The delivery method is POST + Content-Type: application/json; the request body is exactly the same as the data field of GET /v1/tasks/{taskId}, that is, the task detail object, including status, output, error, taskId, created, completed, etc.
  • When delivery fails (non-2xx / timeout / network error), it is retried at immediately → +1 minute → +5 minutes → +20 minutes, up to 4 times; after all 4 attempts fail, it is not retried again, and the task's own state is not affected.
  • Final-state callbacks use **at least once** semantics, not exactly once. The same taskId may be delivered multiple times, and the receiver must perform idempotent deduplication by taskId.

For contract details, refer to the official documentation: Creating tasks and the callback field, Asynchronous API overview (callback retries and idempotency).

Common causes (sorted by probability of occurrence)

1. The callback URL is not publicly accessible (most common)

It was written as http://localhost:3000/..., an internal IP, or a domain that resolves only in internal DNS. HiAPI cannot reach your local machine from the public internet, so it naturally cannot deliver. This is the easiest pitfall in local development.

2. The receiver does not return 2xx

The handler throws an exception, returns a 3xx redirect, is blocked as 401/403 by authentication middleware, or returns 5xx. HiAPI only considers HTTP 2xx as successful delivery; everything else enters retry, and after the retry window is exhausted, the symptom appears as "not received".

3. Processing timeout

The handler does heavy work first (downloading artifacts, transferring to object storage, writing to the database, sending messages) and only then returns 200. If it drags on too long, it is judged as timed out and triggers retries; when retries stack up, it looks like packet loss.

4. Putting callback inside input

It is a top-level field; putting it inside input will not be recognized as callback registration. Moreover, each model's input is a strict schema, and adding unknown keys usually causes a synchronous error at submission time: invalid input: <root>: additional properties 'callback' not allowed, so the task was never submitted successfully at all.

5. when set to a value other than final

Currently only final is supported; other values will be rejected.

6. Polling racing with callbacks

You enabled both polling and callback waiting. Polling obtains the final state first and closes the local task record; when the callback arrives, the corresponding record cannot be found, and it is treated as "not received".

7. Certificate or WAF interception

The callback URL uses a self-signed or expired certificate, or the WAF blocks the anonymous POST that has no standard UA and none of your own authentication headers as suspicious traffic.

Repair steps

  1. First confirm that the task itself is fine. Use GET /v1/tasks/{taskId} to check data.status; if it is success/fail, the task has reached a final state and the problem is in the delivery stage; if it is still in a non-final state, it belongs to another kind of problem (see the stuck-task troubleshooting at the beginning).
  2. Change the callback URL to a public https domain. From an external machine, run curl -i -X POST https://your-domain.com/hiapi/callback -d '{}' to confirm that it is reachable and returns 2xx. For localhost debugging, use a public tunnel to map your local port to the public internet.
  3. First record the callback reliably, then return 2xx quickly. Upon receiving the request, first write the callback content to the database or a persistent queue (this step must be fast), and only return 2xx after the write succeeds; put heavy work such as downloading and transferring into background asynchronous processing. Do not return 200 before persisting: if the process crashes afterward, the callback is lost, and HiAPI will not deliver it again after receiving 2xx. At the same time, exempt this path from authentication middleware, because HiAPI's callback will not carry your own authentication headers.
  4. Verify the location and value of callback. Confirm that it is at the top level of the request body (at the same level as model and input), with url as a public http(s) address and when as final.
  5. Perform idempotent deduplication by taskId. When a callback is received, first check whether this taskId has already been processed locally; if not, persist it / trigger subsequent actions. Under at-least-once semantics, duplicate arrivals are normal; do not discard duplicates as anomalies.
  6. Add a fallback polling mechanism. For taskIds that have not received a callback beyond the expected duration, actively call GET /v1/tasks/{taskId} to check the final state. Callbacks are an optimization; polling is the baseline; use the same idempotency logic for both to converge.
  7. Transfer artifacts as soon as possible. The output[].url in the callback body is a temporary link (storage is temp, expireAt is a Unix time, expiring in about 7 days). Download or transfer it to your own storage immediately after receiving it; do not store the hotlink directly.

Minimal verification example

First leave callbacks aside and use "submit + poll" to prove that your key and integration itself work (use the bare model id):

# 1. Submit a task
curl -s -X POST https://api.hiapi.ai/v1/tasks \
  -H "Authorization: Bearer $HIAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-image-2","input":{"prompt":"a calm blue data center"}}'
# → {"code":200,"message":"success","data":{"taskId":"tk-hiapi-..."}}

# 2. Poll the final state with the returned taskId
curl -s https://api.hiapi.ai/v1/tasks/tk-hiapi-xxxx \
  -H "Authorization: Bearer $HIAPI_API_KEY"
# → data.status goes from queued/handling/archiving to success (including output) or fail (including error)

If submission returns HTTP 401 + {"error":{"code":"permission_denied","type":"hiapi_error","request_id":"..."}}, the API key is incorrect; check it in the console and retry. After confirming that the integration is fine, add the top-level callback to the POST body, and check the receiver item by item according to the repair steps above.

FAQ

The callback has never been received, but the task actually succeeded; can I still get the result? Yes. At any time you can use GET /v1/tasks/{taskId} to read data.status and data.output. A lost callback does not prevent you from actively retrieving the result.

How many times are callbacks retried, and at what intervals? On non-2xx / timeout / network error, it is retried at immediately → +1 minute → +5 minutes → +20 minutes, up to 4 times. After all 4 attempts fail, it is not retried again, and it does not affect the task's own success/fail state.

Is it normal to receive two callbacks for the same task? Yes. Final-state callbacks have "at least once" semantics; just deduplicate idempotently by taskId. Do not treat duplicate delivery as an error.

How do I receive callbacks during local development? HiAPI cannot reach localhost or internal network addresses. Either use a public tunnel to map the local port to the public internet, or simply use polling during the local stage and switch to callbacks after going live.

What fields are in the callback request body? They are exactly the same as the data of GET /v1/tasks/{taskId}, including taskId, status, output (the artifact array on success), error (the code and message on failure), created, completed, etc. See the task detail documentation for details.

Will I be charged extra if I do not receive the callback? No. A failed task itself is not charged, and reserved quota is returned; whether the callback is delivered successfully also does not change the task's billing result. Billing is calculated only based on whether the task ultimately generates successfully.

Related articles

View all articles →

Start building with persistent AI artifacts

Get your API key and call image, video and audio models through one task-based API.

Read the API docs