Verifying Callbacks
Your job target is a URL on the public internet, so anything can POST to it. Every dispatch the platform makes carries a signed token proving it came from us — verify it before acting on the request.
What arrives
POST /your/endpoint HTTP/1.1
Authorization: Bearer <jwt>
Idempotency-Key: sched:<schedule_id>:<scheduled_time_unix_ms>
Content-Type: application/json
<your schedule's payload, verbatim>
The body is exactly the payload you configured — there is no envelope wrapping it. Identity
does not travel in plaintext headers: there are no X-Tenant-Id or X-Schedule-Id
headers to read. Everything the platform asserts about the call is inside the signed token,
which is the point — a header can be forged, a signature cannot.
Getting the public key
The platform publishes its signing keys as a standard JWKS, unauthenticated:
https://api.travila.ai/.well-known/jwks.json
with the usual discovery document alongside it:
https://api.travila.ai/.well-known/openid-configuration
Tokens are signed RS256, and each carries a kid in its header identifying which key
signed it. Use a JWT library that fetches the JWKS and selects by kid — keys rotate, so
do not pin a single one. Cache the JWKS (the endpoint sends Cache-Control: max-age=300)
rather than fetching it on every request.
There is no shared secret and no private key on your side. Verification is public-key only.
Claims
{
"iss": "https://api.travila.ai",
"sub": "<the schedule owner>",
"aud": "your-host.example.com/your/endpoint",
"iat": 1754500000,
"exp": 1754500300,
"jti": "sched:sch_abc123:1754500000000",
"trigger": "scheduled",
"schedule_id": "sch_abc123",
"tenant_id": "your-tenant",
"project_id": "default"
}
| Claim | Check |
|---|---|
iss | Must equal https://api.travila.ai |
aud | Must match your own host + path — see below |
exp / iat | Standard expiry and issued-at validation |
jti | Use for replay protection — see below |
trigger | scheduled for a scheduler dispatch |
schedule_id | Which schedule fired; use it to route |
tenant_id, project_id | Tenancy of the schedule |
An act.sub claim appears when the dispatch acts on behalf of another subject.
iss was https://api.yocaso.dev before the platform moved to api.travila.ai. There is only
ever one issuer, so a receiver still pinned to the old value rejects every callback — update
it. Nothing else changes: both hostnames serve the same API and the same JWKS, and the signing
keys are untouched.
Audience
aud is your target URL reduced to host + path — no scheme, no query string, and the
scheme's default port dropped. A target of https://api.example.com/hooks/daily-digest
produces:
api.example.com/hooks/daily-digest
Verify against the value you expect rather than accepting any audience. This is what stops a token minted for one of your endpoints being replayed against another.
Replay protection
jti is sched:<schedule_id>:<scheduled_time_unix_ms> — deterministic for a given firing.
Retries of the same firing reuse the same jti, so it is a stable key: record the ones
you have processed and reject repeats within a window comfortably longer than the retry
window.
The Idempotency-Key header carries the same value, so you can dedupe on either. Prefer
jti if you want the guarantee to rest on the signature.
Worked example
import jwt # PyJWT
from jwt import PyJWKClient
ISSUER = "https://api.travila.ai"
JWKS = PyJWKClient(f"{ISSUER}/.well-known/jwks.json", cache_keys=True)
AUDIENCE = "api.example.com/hooks/daily-digest" # your host + path
def handle(request):
auth = request.headers.get("Authorization", "")
if not auth.startswith("Bearer "):
return 401, "missing bearer token"
token = auth[len("Bearer "):]
try:
claims = jwt.decode(
token,
JWKS.get_signing_key_from_jwt(token).key,
algorithms=["RS256"],
issuer=ISSUER,
audience=AUDIENCE,
)
except jwt.PyJWTError as e:
return 401, f"invalid token: {e}"
if already_processed(claims["jti"]):
return 200, "duplicate, ignored"
mark_processed(claims["jti"])
run_job(claims["schedule_id"], request.json)
return 200, "ok"
Do the signature check first, and return 401 without side effects when it fails. An
endpoint that acts on the payload and verifies afterwards is an endpoint anyone can trigger.
Responding
| Your response | What the platform does |
|---|---|
2xx | Records a successful execution |
4xx / 5xx | Records a failure and retries per the retry policy |
Return quickly — acknowledge, then do slow work out of band. Repeated failures eventually auto-pause the job.