Callbacks
Creating a form and filling from context are asynchronous. Instead of
polling for the result, you can have Emboss call you:
pass a callback_url when you start a job, and Emboss POSTs the result to that
URL the moment the job finishes — whether it succeeds or fails.
Callbacks don't replace polling — they're an alternative. The result also stays fetchable from the usual endpoints, so a missed callback is never fatal.
Request a callback
Pass callback_url when you start any async job. It must be a public https
URL (private, loopback, and non-https URLs are rejected with 400).
curl -X POST https://api.getemboss.ai/forms/with-context \
-H "Authorization: Bearer sk_live_yourkey" \
-F "file=@form.pdf" \
-F "context=@notes.txt" \
-F "callback_url=https://your-app.example.com/emboss/webhook"
The same callback_url field works on POST /forms and
POST /forms/{form_id}/with-context.
What Emboss sends
When the job reaches a terminal state, Emboss makes one POST to your URL with a
JSON body. Context-fill jobs (/forms/with-context,
/forms/{form_id}/with-context) send job.* events:
{
"event": "job.ready",
"status": "ready",
"job_id": "6c47f7f5-f921-4698-910f-95dd7d81310b",
"session_id": "9b1c…",
"report": {
"filled": [{ "id": "full_name", "confidence": "high" }],
"dropped": [{ "id": "ssn", "value": "…", "reason": "not an allowed option" }]
}
}
{
"event": "job.failed",
"status": "failed",
"job_id": "6c47f7f5-f921-4698-910f-95dd7d81310b",
"error": { "code": "context_fill_failed", "message": "…" }
}
Creating a form (POST /forms) sends form.* events with a form_id:
{ "event": "form.ready", "status": "ready", "form_id": "…" }
{ "event": "form.failed", "status": "failed", "form_id": "…",
"error": { "code": "form_processing_failed", "message": "…" } }
Verify the signature
Each callback carries an X-Emboss-Signature header: sha256= followed by the
HMAC-SHA256 of the raw request body, keyed by your callback signing secret.
Compute the same HMAC over the raw bytes you received and compare in constant
time — before parsing the JSON.
import crypto from "crypto";
function verify(rawBody, signature, secret) {
const expected =
"sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signature || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
import hmac, hashlib
def verify(raw_body: bytes, signature: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature or "")
Verify against the raw body bytes, not a re-serialized object — re-encoding JSON can change bytes and break the signature.
Your signing secret is configured on your Emboss account. If signing isn't
enabled, callbacks arrive without an X-Emboss-Signature header — treat a
missing signature as a configuration signal, not a valid unsigned request.
Fetch the result
The callback tells you the job is done; fetch the artifact the same way the poll
flow does. For a context fill, use the session_id from the payload:
curl https://api.getemboss.ai/sessions/9b1c…/pdf \
-H "Authorization: Bearer sk_live_yourkey" -o filled.pdf
For a created form, download the fillable PDF with the form_id:
GET /forms/{form_id}/fillable.
Delivery & reliability
Delivery is best-effort: Emboss retries a few times with backoff on a timeout or 5xx, then gives up. A callback failure never affects the job itself — the result is always still available from the GET endpoints above, so you can fall back to polling if a callback is ever missed.