Sign in with Apple Sends Four Notifications, Not Three

Apple’s developer announcement about Sign in with Apple server-to-server notifications lists three things your endpoint will receive: changes in email forwarding preferences, account deletions in your app, and permanent Apple Account deletions.3 The API documentation defines four distinct event types.1

An implementation written from the announcement handles three branches and silently drops a fourth event. The announcement collapses email-enabled and email-disabled into a single bullet about forwarding preferences. They arrive as separate notifications with separate type values, and code that switches on type without a default case ignores whichever one the author forgot.

Two of the four events also mean more than their names suggest, and one of them changes your app’s authentication state.

TL;DR

Sign in with Apple delivers four server-to-server notification types: email-enabled, email-disabled, consent-revoked, and account-deleted.1 The payload arrives as a JWS wrapped inside a JSON object under a payload key, signed by Apple’s private key, and must be validated using the algorithm named in the header’s alg parameter before you act on it.1 consent-revoked invalidates the user’s credentials, making it an authentication event rather than a preference change. Native apps receive no client-side callback when an Apple Account is deleted, so the server notification is the only signal.1 Apple documents no retry or delivery semantics.

The Four Event Types

Each notification carries a type value inside the events claim.1

type What happened
email-enabled The user enabled email forwarding to their personal address using Hide My Email
email-disabled The user disabled email forwarding
consent-revoked The user revoked consent for your app, and their credentials became invalid
account-deleted The user requested permanent deletion of their Apple Account

The two email events are the ones the announcement merges. They matter independently: email-disabled means mail you send to the relay address stops reaching the user, and email-enabled means it starts again. Treating them as one “preferences changed” event forces you to go ask what the current state is, which the notification already told you.

Two Events That Are Not What They Look Like

consent-revoked is an authentication event. Apple’s description is that the user “revokes consent for your app to use their Apple Account and their credentials become invalid.”1 Not deprecated, not pending expiry. Invalid.

An app that logs this alongside the email events and updates a preferences row will keep presenting a signed-in interface backed by credentials that no longer authenticate. The user sees their account until the next token refresh fails, and then sees something worse. The correct handling is to end the session and route to re-authentication, in the same code path you would use for a revoked OAuth grant.

account-deleted may be the only notice you get. Apple states that when a user permanently deletes their Apple Account, Sign in with Apple invalidates all user tokens and disables email forwarding for all associated apps, and that for native apps the system does not send a client-side callback.1

That sentence is the strongest argument for running an endpoint at all. An iOS-only team with no server-to-server endpoint has no mechanism to learn the account is gone. The record persists, the relay address stops working, and any deletion obligation you have goes unmet because nothing told you there was anything to delete.

Registering the Endpoint

Configuration happens in Certificates, Identifiers & Profiles: select Identifiers, choose your App ID, enable the Sign in with Apple service, click Configure, and provide the endpoint URL.2

The constraints are worth reading before you design around them.2

  • One URL per Sign in with Apple app grouping and key. Not one per app.
  • Registrable only on a primary App ID.
  • The URL must be an absolute URI with scheme, host, and path: https://example.com/path/to/endpoint
  • TLS 1.2 or later is required to receive notifications.

Apple’s API documentation adds that you may use the same URL for multiple developer teams and apps.1 Read alongside the one-URL-per-grouping rule, the sensible reading is that a single service can receive everything while each grouping registers its own pointer to it. Apple does not spell out the interaction, so treat a shared endpoint as workable rather than blessed, and make sure your handler can tell which app a notification concerns before you rely on it.

The TLS 1.2 floor connects to a broader shift. OS 27 began enforcing stricter TLS requirements on management traffic, with the same 1.2 minimum plus ATS-compliant ciphersuites and certificates. An endpoint that satisfies Apple’s requirement today is not automatically ATS-compliant, and the direction of travel is toward stricter rather than looser.

Reading the Payload

The delivery arrives as an HTTP POST whose body is a JSON object, with the signed token inside it:1

{
    "payload": "<SERVER_TO_SERVER_NOTIFICATION_JWS>"
}

The JWS is wrapped, not raw. Parse the JSON, pull out payload, then validate. An implementation that hands the entire request body to a JWS verifier fails on the first notification, and the failure presents as a malformed token rather than as a wrapping mistake, which sends you looking in the wrong place.

Validation comes before interpretation. The payload is cryptographically signed by Apple’s private key in JSON Web Signature format, and Apple’s instruction is to examine the JWS and use the algorithm specified in the header’s alg parameter to validate the signature.1 Only after the signature checks out do you read the events claim and branch on type.

Two habits worth keeping from general JWS practice: never trust an alg value that would let a caller downgrade verification, and confirm the token’s issuer and audience match what you expect rather than accepting any well-formed Apple-signed token.

The decoded shape

Once validated, a decoded consent-revoked notification looks like this:1

{
    "iss": "https://appleid.apple.com",
    "aud": "com.mytest.app",
    "iat": 1508184845,
    "jti": "abede...67890",
    "events": {
        "type": "consent-revoked",
        "sub": "820417.faa325acbc78e1be1668ba852d492d8a.0219",
        "event_time": 1508184845
    }
}

account-deleted carries the same fields with a different type. The email events add two more, email and is_private_email.

Three details in that shape will cost you time if you meet them by surprise.

events is an object, not an array. The name is plural and the value is a single event. Code written against the name rather than the shape iterates a dictionary and gets keys.

is_private_email is a string. Apple’s examples show "true" in quotes, not the JSON boolean true. A strict decoder mapping it to a Bool fails, and a permissive one that treats any non-empty string as truthy gets the right answer for the wrong reason and then gets "false" wrong.

sub is the stable user identifier, the same value you received at sign-in, and it is how you find the account this notification concerns. aud is your client identifier, which is what lets a shared endpoint route notifications for several apps.

A note on Apple’s own examples: the two email payloads are missing a comma between "is_private_email": "true" and "event_time". Copy either block into a JSON parser and it will reject the document. The structure is right, the punctuation is not, and the reader who pastes it into a test fixture loses ten minutes to a syntax error that is not theirs.

Apple’s terminology also drifts. The prose describes a payload in JSON Web Signature format, while the wrapper example names the value SERVER_TO_SERVER_NOTIFICATION_JWT.1 Same object; a signed JWT is a JWS with a JSON payload. Worth knowing when you search their documentation and find both terms.

A Handler, End to End

The shape of a correct handler follows from the constraints above. In Python, with PyJWT:

import json

import jwt
from jwt import PyJWKClient

# Apple publishes its signing keys as a JWKS. Cache the client;
# it fetches and caches keys rather than hitting Apple per request.
JWKS = PyJWKClient("https://appleid.apple.com/auth/keys")
CLIENT_ID = "com.mytest.app"   # your aud value

def handle_notification(request_body: bytes):
    # 1. The JWS is wrapped in JSON under "payload", not the raw body.
    wrapper = json.loads(request_body)
    token = wrapper["payload"]

    # 2. Resolve the signing key by the token's kid, then verify.
    #    Pin the algorithm. Never read alg from the token to decide.
    signing_key = JWKS.get_signing_key_from_jwt(token)
    claims = jwt.decode(
        token,
        signing_key.key,
        algorithms=["RS256"],
        audience=CLIENT_ID,
        issuer="https://appleid.apple.com",
    )

    # 3. Only now is anything trustworthy.
    event = claims["events"]          # an object, not a list
    apple_user_id = event["sub"]      # stable identifier from sign-in

    match event["type"]:
        case "consent-revoked" | "account-deleted":
            end_all_sessions(apple_user_id)
            mark_account_unlinked(apple_user_id)
        case "email-disabled":
            set_email_forwarding(apple_user_id, enabled=False)
        case "email-enabled":
            set_email_forwarding(apple_user_id, enabled=True)
        case other:
            log_unknown_event(other)   # do not fail silently

A few of those lines are load-bearing.

Pin the algorithm. Apple’s live JWKS currently publishes three RSA keys, all RS256 with use: sig and distinct kid values. Passing algorithms=["RS256"] rather than reading alg out of the token closes the classic downgrade path where an attacker supplies a token claiming alg: none.

Resolve by kid, not by taking the first key. Three keys are live simultaneously, which is what key rotation looks like from outside. A handler that grabs keys[0] works until Apple rotates and then fails for a subset of tokens, which is a miserable thing to debug.

Verify aud and iss. Without them you accept any validly-signed Apple token, including one minted for a different app.

Keep a default branch. A fifth event type would otherwise vanish. That is precisely how an implementation written from Apple’s three-item announcement drops email-enabled or email-disabled today.

What Apple Does Not Document

Neither the API documentation nor the account help page says anything about delivery guarantees. Searching both for retry, redelivery, acknowledgment, status codes, timeouts, and idempotency returns nothing.

So the following are unanswered by Apple’s documentation as of this writing:

  • Whether a failed delivery is retried, and how many times
  • Over what window retries occur, if they do
  • What status code your endpoint should return to signal success
  • Whether the same notification can arrive twice

That absence has a design consequence, and here I am reasoning past what Apple states rather than reporting it. An endpoint whose delivery semantics are unspecified cannot be treated as an authoritative event stream. The defensive posture is to treat each notification as a hint that something changed, and to reconcile against your own records rather than applying the event blindly. Make handlers idempotent, because you cannot rule out duplicates. Do not build a flow whose correctness depends on having received every notification, because you cannot confirm you did.

If your endpoint is down for an hour, you do not know from the documentation whether you lost an hour of account deletions or whether they are queued somewhere. Design as though you lost them.

There is also no documented way to test it

Searching both pages for sandbox, simulate, and trigger returns nothing. Apple documents no mechanism for firing a notification on demand.

Which leaves an awkward loop. The events that matter most, consent-revoked and account-deleted, are produced by a user revoking access to your app or deleting their Apple Account. Verifying your handler against a real account-deleted means someone deletes an Apple Account. That is not a test you run twice.

The workable substitute is to split the problem. Construct the decoded payloads yourself from the shapes above and unit-test the branching, the idempotency, and the reconciliation logic against them. Separately, test the transport and signature path with a real notification you can actually generate: revoking consent for a test Apple Account is recoverable in a way that deleting one is not, and it exercises the wrapper parsing, the kid lookup, and the signature check end to end.

Whatever you do, confirm the endpoint is reachable and returns promptly before you register it. An endpoint registered and never verified is how a team discovers, months later, that every notification since launch went to a URL behind an expired certificate.

A Requirement That Is Already Live

The reason this surfaced in developer news at all: since January 1, 2026, developers based in the Republic of Korea must provide a server-to-server notification endpoint when registering a new Services ID or updating an existing one, in order to associate a website with an app using Sign in with Apple.3 Apple announced it on October 9, 2025.

The requirement is narrow, and it has been in force for months rather than being something to prepare for. Its interest is directional. Apple has begun making the endpoint mandatory in at least one jurisdiction, for reasons that generalize: giving people control over personal data they have shared, and making account deletion actually propagate. Nothing about that logic is specific to Korea.

If you are building the endpoint anyway, build it before a regulator makes it your deadline.

Key Takeaways

For backend engineers: - Handle four types, not three. email-enabled and email-disabled arrive separately. - Parse the JSON body and extract payload before handing anything to a JWS verifier. - Validate the signature using the algorithm in the header’s alg before reading the events claim. - Make handlers idempotent and reconcile against your own records. Apple documents no delivery guarantees.

For iOS teams: - consent-revoked invalidates credentials. Treat it as a session-ending auth event, not a preferences update. - Native apps get no client-side callback on Apple Account deletion. Without an endpoint you never learn it happened.

For anyone weighing whether to bother: - The endpoint is already mandatory for Korea-based developers as of January 2026, and the rationale generalizes.

FAQ

How many notification types are there?

Four: email-enabled, email-disabled, consent-revoked, and account-deleted.1 Apple’s developer news announcement describes three, merging the two email events into one bullet about forwarding preferences.3

The user’s credentials become invalid.1 Treat it the way you would a revoked OAuth grant: end the session and send the user to re-authentication rather than updating a preference and continuing.

Do I need an endpoint if I only ship a native iOS app?

Apple states that for native apps the system does not send a client-side callback when an Apple Account is permanently deleted.1 Without a server-to-server endpoint there is no mechanism that informs you.

What should my endpoint return, and what if it is down?

Apple does not document status code expectations, retry behavior, or whether duplicate deliveries occur. Design for at-least-once or possibly at-most-once delivery, make handlers idempotent, and reconcile against your own records rather than assuming every notification arrived.

Can one endpoint serve multiple apps?

Apple’s documentation says you may use the same URL for multiple developer teams and apps.1 Registration is one URL per Sign in with Apple app grouping and key, on a primary App ID.2 A shared service works, provided your handler can determine which app each notification concerns.

Sources


  1. Apple, “Processing changes for Sign in with Apple accounts.” Source for the four event types (email-enabled, email-disabled, consent-revoked, account-deleted), the JWS payload format and the instruction to validate using the header’s alg parameter, the {"payload": "<JWS>"} wrapping, the statement that revoked consent makes credentials invalid, the note that native apps receive no client-side callback on account deletion, the TLS 1.2 server requirement, and the allowance to use one URL across multiple teams and apps. Retrieved 2026-08-02. 

  2. Apple, “Enabling server-to-server notifications.” Source for the registration path through Certificates, Identifiers & Profiles, the one-URL-per-app-grouping-and-key rule, the primary App ID restriction, the absolute URI requirement, and the TLS 1.2 requirement. Retrieved 2026-08-02. 

  3. Apple Developer News, “New requirement for apps using Sign in with Apple for account creation,” October 9, 2025. Source for the Korea requirement effective January 1, 2026, and for the three-item summary of what the endpoint receives. 

相關文章

The Fork Bomb Saved Us

The LiteLLM attacker made one implementation mistake. That mistake was the only reason 47,000 installs got caught in 46 …

7 分鐘閱讀

The Repo Shouldn't Get to Vote on Its Own Trust

Two Claude Code trust dialog bypass CVEs in 37 days reveal a load-order failure. One invariant fixes it: interpret no wo…

12 分鐘閱讀

What I Refuse To Write About

A blog cluster's voice comes from what it refuses to publish, not what it ships. Categorical, pattern, and interesting r…

11 分鐘閱讀