Skip to main content

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"
}
ClaimCheck
issMust equal https://api.travila.ai
audMust match your own host + path — see below
exp / iatStandard expiry and issued-at validation
jtiUse for replay protection — see below
triggerscheduled for a scheduler dispatch
schedule_idWhich schedule fired; use it to route
tenant_id, project_idTenancy of the schedule

An act.sub claim appears when the dispatch acts on behalf of another subject.

The issuer changed

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"
Verify before you act

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 responseWhat the platform does
2xxRecords a successful execution
4xx / 5xxRecords 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.