Unauthorized
ERR-P01-MAIL-0401 - Unauthorized
Section titled “ERR-P01-MAIL-0401 - Unauthorized”Summary
Section titled “Summary”The X-Service-Key presented on an /_internal/* route is missing or wrong.
When this is raised
Section titled “When this is raised”- A request hits
/_internal/notifywithout anX-Service-Keyheader, or with a value that does not match the mail Worker’sSERVICE_KEYsecret. The comparison is constant-time. - Also raised when the mail Worker has no
SERVICE_KEYconfigured at all - the route fails closed rather than admitting unauthenticated callers. - Note: bearer-authenticated
POST /mails/GET /mailsfailures emit identity codes (ERR-P01-IDT-*), not this one - the JWT is verified against the identity service.ERR-P01-MAIL-0401is strictly the service-key (PSK) gate on the cross-Worker internal surface.
What to do
Section titled “What to do”- Send the
X-Service-Keyheader equal to the sharedSERVICE_KEY. This is the same PSK that gatesapps/api’s/_admin/*routes. - Internal callers should reach mail via the service binding, which injects
the key; a
401here usually means a misconfigured binding or a rotated key not yet propagated. - This route is not for external clients - end-user agents use the bearer
/mailssurface instead.
Server-side cause
Section titled “Server-side cause”- Raised by:
apps/mail/src/internal/notify.ts- theinternalRouter.use('*', ...)guard, which calls the localunauthorized(c, ...)helper. - Guard:
if (!expected) ...(noSERVICE_KEY) andif (!timingSafeEqual(presented, expected)) ...(mismatch), both returning401withcode: MAIL_ERR.unauthorized.
Example response
Section titled “Example response”{ "type": "https://citizenry.id/errors/ERR-P01-MAIL-0401", "title": "Unauthorized", "status": 401, "code": "ERR-P01-MAIL-0401", "message": "X-Service-Key missing or invalid", "method": "POST", "instance": "/_internal/notify", "request_url": "https://mail.citizenry.id/_internal/notify", "timestamp": "2026-05-17T07:00:00.000Z"}Related codes
Section titled “Related codes”ERR-P01-MAIL-0400- malformed JSON on the same/_internal/notifyroute, checked after this guard passes.
Changelog
Section titled “Changelog”- 0.1.0 - introduced.