Get called when a job finishes
Add webhookUrl to a request and the API POSTs the job's final state to that URL once the job reaches succeeded, failed or cancelled. Polling stays available; the webhook saves you the poll loop.
Deliveries follow the Standard Webhooks specification, so you verify them with the official Standard Webhooks library for your language instead of writing signature code yourself.
Ask for a callback
webhookUrl goes in the body of POST /jobs/{site} or POST /records/{site}, next to the source's own fields. It must be a public http or https URL; a URL that points at a private or internal address is never called.
curl -X POST https://api.urakimon.com/jobs/amazon \
-H "x-api-key: $URAKIMON_API_KEY" \
-H "content-type: application/json" \
-d '{"mode":"product","asins":["B0H1KXX5TL","B0F5HPCLGB","B0GG9BY46M"],"country":"US","webhookUrl":"https://example.com/urakimon/finished"}'What arrives
One POST with a JSON body and the three Standard Webhooks headers:
POST /urakimon/finished HTTP/1.1
content-type: application/json
webhook-id: b8e2f4a1-6c3d-4e9b-a7f2-1d5c8e0b3a69
webhook-timestamp: 1758393836
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4={ "deliveryId": "b8e2f4a1-6c3d-4e9b-a7f2-1d5c8e0b3a69", "id": "0f0c4a18-7a5e-4d2a-9f4c-6a1b9d0e2c31", "site": "amazon", "status": "succeeded", "count": 1840, "error": null, "finishedAt": 1758393836400}Example data. The same seven fields arrive on every delivery.
webhook-idis the delivery id, the same value asdeliveryIdin the body. Every attempt of one delivery repeats it.webhook-timestampis when this attempt was signed, in unix seconds.webhook-signatureisv1,followed by a base64 HMAC-SHA256 of<webhook-id>.<webhook-timestamp>.<body>.
id is the job id, count the records it wrote and error the reason when status is not succeeded. Read the records with GET /jobs/{id}/records; the body carries the job's state, not its records. Answer with any 2xx as soon as you have stored the delivery, and do the work afterwards.
Verify the signature
Every delivery is signed with the key's own signing secret, a whsec_... value shown next to the key at https://api.urakimon.com/account. The secret belongs to the key, so rotating the key's API secret does not change it. Pass it to the library as it is shown, including the whsec_ prefix.
The library recomputes the signature over the body exactly as received, compares it in constant time, and refuses a webhook-timestamp more than 300 seconds from your clock. Each sample below returns the parsed delivery only when both checks pass.
Node
import { Webhook } from "standardwebhooks";
// npm install standardwebhooks
// rawBody: the request body exactly as received (a string or Buffer), before JSON.parse.
// headers: the request headers; webhook-id, webhook-timestamp and webhook-signature are read.
// secret: the key's signing secret (whsec_...), from the account page, loaded from your configuration.
// Returns the parsed delivery, or throws when the signature or the timestamp does not check out.
function verifyUrakimonDelivery(rawBody, headers, secret) {
return new Webhook(secret).verify(rawBody, headers);
}Python
# pip install standardwebhooks
import os
from standardwebhooks.webhooks import Webhook, WebhookVerificationError
# The key's signing secret (whsec_...), from the account page.
webhook = Webhook(os.environ["URAKIMON_WEBHOOK_SECRET"])
def verify_urakimon_delivery(raw_body: bytes, headers: dict) -> dict:
"""raw_body: the request body bytes exactly as received, before json.loads.
Returns the parsed delivery; raises WebhookVerificationError when it does not check out."""
return webhook.verify(raw_body, headers)Go
// go get github.com/standard-webhooks/standard-webhooks/libraries
import (
"io"
"net/http"
"os"
standardwebhooks "github.com/standard-webhooks/standard-webhooks/libraries/go"
)
func urakimonDelivery(w http.ResponseWriter, r *http.Request) {
// The key's signing secret (whsec_...), from the account page.
wh, err := standardwebhooks.NewWebhook(os.Getenv("URAKIMON_WEBHOOK_SECRET"))
if err != nil {
http.Error(w, "misconfigured", http.StatusInternalServerError)
return
}
body, err := io.ReadAll(r.Body) // the raw bytes, before any JSON decoding
if err != nil || wh.Verify(body, r.Header) != nil {
http.Error(w, "invalid signature", http.StatusBadRequest)
return
}
// Store the delivery (dedupe on r.Header.Get("webhook-id")), then answer 2xx.
w.WriteHeader(http.StatusNoContent)
}Ruby
# gem install standardwebhooks
require "standardwebhooks"
# The key's signing secret (whsec_...), from the account page.
WEBHOOK = StandardWebhooks::Webhook.new(ENV.fetch("URAKIMON_WEBHOOK_SECRET"))
# raw_body: the request body exactly as received. Returns the parsed delivery;
# raises StandardWebhooks::WebhookVerificationError when it does not check out.
def verify_urakimon_delivery(raw_body, headers)
WEBHOOK.verify(raw_body, {
"webhook-id" => headers["webhook-id"],
"webhook-timestamp" => headers["webhook-timestamp"],
"webhook-signature" => headers["webhook-signature"],
})
endPHP
<?php
// composer require standard-webhooks/standard-webhooks
use StandardWebhooks\Webhook;
// The key's signing secret (whsec_...), from the account page.
$webhook = new Webhook(getenv('URAKIMON_WEBHOOK_SECRET'));
// The raw body, before json_decode. verify() returns the parsed delivery and throws when it does not check out.
$payload = file_get_contents('php://input');
$delivery = $webhook->verify($payload, [
'webhook-id' => $_SERVER['HTTP_WEBHOOK_ID'] ?? '',
'webhook-timestamp' => $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '',
'webhook-signature' => $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '',
]);Watch out. Read the raw bytes before your framework parses the JSON. A body that was parsed and re-serialised will not match the signature, even when the data is unchanged.
Libraries for C#, Elixir, Java and Rust are listed in the Standard Webhooks repository. They all take the same secret and the same three headers.
Retries and what ends them
| Your endpoint answers | What happens |
|---|---|
2xx | Delivered. No further attempt. |
3xx | Ended. The redirect is not followed and there is no retry, so point webhookUrl at the final URL. |
410 Gone | Ended. Your endpoint asked us to stop; there is no retry. |
| Any other status, or a network error | Retried with the same webhook-id and a fresh signature. Retry-After on your answer is honoured. |
| No answer | An attempt that has not been answered after 30 seconds counts as failed and is retried. |
A delivery gets up to 8 attempts: the first as soon as the job finishes, then each retry 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours, 10 hours after the one before, with a little random spread so retries from many jobs do not arrive at once. Deliveries are kept across restarts of the API, so a retry is never lost to a deploy.
A delivery that is still not accepted after its last attempt, or 48 hours after the job finished, has failed. Failed deliveries are listed at https://api.urakimon.com/account with the reason, and the job itself is unaffected: GET /jobs/{id} still reports it.
Duplicates and replay
Delivery is at least once. If your endpoint stored a delivery but the answer never reached us (a timeout, a dropped connection, a restart on either side), the same delivery arrives again with the same webhook-id. Store the webhook-id of each delivery you process and skip one you have seen; then each job reaches you exactly once.
The timestamp check bounds replay: a captured delivery cannot be presented again more than 300 seconds after it was signed, and every retry is signed afresh. Keep your receiver's clock synchronised, or a correct delivery fails the check.
Rotate the signing secret
Roll a key's signing secret at https://api.urakimon.com/account. For 24 hours afterwards webhook-signature carries two signatures separated by a space, one made with the new secret and one with the previous one. The libraries accept a delivery when any signature matches, so deploy the new secret to your receiver at any point in that window and no delivery is refused.
Limits
- A key can trigger up to 1,000 deliveries per UTC day. Jobs beyond that still run and settle; they are not delivered.
- After 20 failed attempts in a row to one host, deliveries to that host pause for 1 minute, then one is tried. Each further failure doubles the pause, up to 1 hour; the first accepted delivery ends it. Paused deliveries keep their place and their remaining attempts.
Next step
Make retried submissions safe with the idempotency-key header, so a network error on POST /jobs/{site} never starts a second job.