Subscribe and unsubscribe
POST /contacts/{address}/unsubscribe
POST /contacts/{address}/subscribe
Both take an optional body, and both return the contact.
Two layers, and they are not the same thing
No body is the whole list. It sets the global opt-out, and no broadcast reaches them again whatever its topic.
POST /contacts/alice%40example.com/unsubscribe
{ "reason": "preferences" }
A topic is one topic. Everything else they agreed to stands.
POST /contacts/alice%40example.com/unsubscribe
{ "topic": "weekly-digest" }
Topics are named, not numbered — your code already says
"product-updates", and an id would be a lookup you have to store.
404 not_found names the topic when there is no such one.
A contact who has never answered about a topic inherits that topic’s default, so
absence of an answer is not absence of consent. Their stated answers are in
topics on retrieve.
reason
manual (the default) or preferences, on an unsubscribe.
complaint is refused with 422 validation_error: it is written from the
mailbox provider’s own report, and a sender declaring one on a contact’s behalf
would be fabricating the fact that costs them most.
Subscribing back
POST /contacts/{address}/subscribe clears the global opt-out, or opts them into
a named topic.
It is refused for a contact whose unsubscribe_reason is complaint
(422 failed_precondition). That address is on the suppression list too, so mail
would not go out even with the flag flipped — answering OK would tell you a
contact is reachable when they are not. Re-consent after a spam report is yours
to obtain, not ours to record.