For a marketplace signup flow, keep verification delivery behind a small event ledger: authenticate the sending domain, record every provider response, and make suppression state authoritative before retrying. That decision reduces integration effort because the signup service only emits an event; it does not need to understand every bounce or complaint payload.
Short answer: verify the domain with SPF and DKIM before production traffic, give each verification event an idempotency key, and treat bounces and complaints as durable state transitions rather than transient API errors. The fastest diagnosis comes from correlating the signup event, provider message identifier, DNS result, and recipient feedback in one record.
What should a marketplace event-notification ledger record?
The verification link is a security artifact, not an ordinary marketing email. Store a token hash, expiry, account identifier, and a delivery event ID; never put the raw token in a log or in a provider metadata field that many operators can read. The delivery record should also retain the authenticated domain, selector, message ID, attempt number, and a redacted destination hash.
That ledger gives the team a clean boundary. The account service asks for verification.requested. A delivery worker renders the message and calls an email API. A webhook or polling consumer changes the event to accepted, delivered, bounced, or complained. The account service only needs the final policy decision: a valid link can be issued again, while a hard bounce or complaint needs a different path.
One short rule matters: accepted is not delivered.
Retries lie.
I once treated a provider's 202 response as proof that a test account would receive the link. The API call had succeeded, but the domain's DKIM selector was missing, so downstream filtering discarded the message. The useful evidence was not the HTTP status; it was the authentication result and the later event. Your mileage may vary by mailbox provider, but this distinction is stable enough to make it a schema invariant. In a marketplace, the same mistake can leave a newly created seller unable to finish onboarding while the support dashboard says "sent." That is why the ledger needs both the request-side response and the recipient-side evidence, with a correlation ID that survives queue retries, worker restarts, and a later resend. It also needs an explicit expiry check: a delivered message with an expired token is a security failure, not a deliverability success.
How do DKIM, SPF, domain verification, and API events fit together?
SPF authorizes the hosts permitted to send for a domain. DKIM signs the message so a receiving system can verify that selected headers and the body were not altered. Domain verification is the operational step that proves your service controls the DNS records needed for those checks. None of these guarantees inbox placement by itself.
For a new marketplace domain, publish the required SPF record, publish the DKIM public key under the supplied selector, and verify both through the sending service before enabling signup traffic. Keep old and new selectors during rotation. A selector change is a deployment: test it, observe it, then remove the old record after the longest message and cache window you support.
The API contract should expose a provider-neutral envelope even when providers disagree about field names. Here is the critical path in Python; the endpoint is deliberately an internal adapter, not a claimed vendor route.
from dataclasses import dataclass
from datetime import datetime, timezone
import hashlib
import requests
@dataclass
class VerificationEvent:
event_id: str
account_id: str
destination: str
token_hash: str
expires_at: datetime
def destination_hash(address: str) -> str:
return hashlib.sha256(address.strip().lower().encode()).hexdigest()
def send_verification(event: VerificationEvent, adapter_url: str, api_key: str) -> str:
payload = {
"event_id": event.event_id,
"template": "marketplace-account-verification",
"to_hash": destination_hash(event.destination),
"expires_at": event.expires_at.astimezone(timezone.utc).isoformat(),
}
response = requests.post(
f"{adapter_url}/messages",
json=payload,
headers={"Authorization": f"Bearer {api_key}"},
timeout=10,
)
response.raise_for_status()
return response.json()["message_id"]
The adapter owns provider-specific paths, authentication, and response parsing. It must make retries idempotent: reuse the same event ID, persist the returned message ID, and never create a second verification token merely because a network timeout happened after the provider accepted the first request. A timeout is ambiguous. Query the event ledger before sending again.
Which failure signals should stop a retry?
Suppression is a policy decision, not a convenience endpoint. A hard bounce usually means the address is permanently undeliverable; a complaint means the recipient marked the message as unwanted. Both should stop automatic retries for that destination until an explicit account-recovery process clears the state. A temporary deferral can be retried with bounded backoff, but the retry budget belongs in the ledger so two workers cannot multiply it.
| Signal | Ledger action | Next attempt |
|---|---|---|
| DNS authentication failure | Mark configuration error and alert | Do not retry the recipient |
| Permanent bounce | Add destination to suppression state | Require a new address or support flow |
| Complaint | Suppress immediately and preserve evidence | Do not send another verification email |
| Temporary deferral | Record provider reason and attempt count | Retry inside a short, bounded window |
| Accepted without delivery event | Keep pending | Reconcile from webhook or status API |
The table is intentionally conservative. Sending another verification message after a complaint can damage the domain's reputation and does not help account security. Conversely, deleting every pending event after one timeout hides a delivery that may already be in flight.
Where does API troubleshooting go wrong in production?
Teams often start at the HTTP client because that is where the failure is visible. The more useful sequence is chronological: confirm the signup event exists, check that the adapter used the expected authenticated domain, inspect the provider message ID, then compare webhook timing with the receiving mailbox's authentication result. Keep timestamps in UTC and retain the raw provider reason in a restricted store; normalize it into a small set of states for application logic.
A second trap is mixing verification and notification policies. A password-reset or signup link may have a short expiry and a strict one-recipient rule, while an order notification can tolerate a later retry. Give them different templates, suppression handling, and observability labels even when they share one API client.
The integration boundary is also a real trade-off. A direct SMTP implementation gives control over the connection, but the team inherits DNS, feedback processing, retry queues, and reputation operations. A hosted email API reduces that integration surface, yet the application must still own token security, idempotency, and the durable suppression decision. The smallest adapter is usually the one that keeps those responsibilities visible.
When is this design not suitable?
The catch is that an event ledger and authenticated domain do not solve every communication workload. This design is not suitable when the product needs bulk marketing segmentation, complex unsubscribe journeys, or a regulated archive with a mandated provider retention contract. Use a dedicated campaign system or an internally governed mail platform for those requirements, and keep account verification on a transactional path.
It is also a poor fit for a team that cannot operate DNS changes or consume delivery events. In that case, choose an integration with managed domain onboarding and an operational support process, accepting less control over the adapter. Do not pretend a simpler API removes the need to understand SPF, DKIM, suppression, bounces, and complaints; it only moves some mechanics behind a boundary.
For the marketplace scenario, I would ship the ledger, DNS verification checks, and a replayable event consumer before adding a second channel such as SMS. That ordering keeps the account-security path testable and makes a later channel choice an isolated decision rather than a rewrite of signup.
Top comments (0)