0% completed
Notification System: API Design
On This Page
Step 4: API Specifications
Step 4: API Specifications
Step 3 gave us the load the service has to carry. Now we write down what a tenant actually calls. Three endpoints cover the whole product. One sends a notification, one reports what happened to it, and one changes a user's preferences.
1. Send Notification
POST /v1/notifications- Headers:
Authorization: Bearer <tenant-key> - Request Body:
{ "user_id": "u_12345", "template_id": "order_shipped_v1", "channels": ["push", "email"], "priority": "transactional", "idempotency_key": "uuid-gen-by-client", "data": { "order_id": "999", "name": "Alice" } }
- Response:
202 Accepted
{ "notification_id": "notif_abc123", "channels": ["push", "email"] }
- Error:
429 Too Many Requests(quota exceeded).
Four fields decide how the rest of the design behaves.
channels is a list, and it is optional. Step 2 asks for delivery on one or several channels. A single channel field could not express that. Sending the list explicitly is the common case. Leaving it out means "use whatever this user has enabled". That is what a caller wants when it does not know, or care, how the person prefers to be reached. Either way, the Preference Service in Step 5 makes the final decision. A channel the user has switched off is dropped from the list before anything is queued.
priority names urgency, not a channel. Step 2 says channels are never ranked against each other, and that still holds. Email is not served ahead of SMS. Urgency is a separate question, and the system has to answer it. The accepted values are transactional, default and bulk. Step 7 gives each of them its own queue per channel. So a one-time passcode is never stuck behind a ten million message campaign.
idempotency_key comes from the caller. An idempotency key is a value that makes a repeated request safe to send again. The caller creates one value per real-world event and reuses it on every retry of that event. Delivery here is at least once, meaning a message may go out more than once but is never lost. Without the key, a caller that retries after a timeout would send the same message twice. The service stores the key with its result for 24 hours. A repeat gets the original notification_id back, and nothing new is queued.
202 Accepted, not 200 OK. The response means the request is stored and queued. It does not mean anybody has been notified. Delivery happens later and can still fail. Returning 200 would imply a promise this endpoint cannot make.
2. Get Status
GET /v1/notifications/{notification_id}- Response:
{ "status": "partial", "channels": { "push": { "status": "delivered", "updated_at": "..." }, "email": { "status": "failed", "reason": "hard_bounce" } } }
Status is reported per channel, because one notification can succeed on push and fail on email. A single top-level status would have to pick one of them and hide the other. The failed one is usually the one the caller needs to see. Note what delivered means for push: the provider accepted the message for the device. It does not mean the person has read it.
3. Update Preferences
PUT /v1/users/{user_id}/preferences- Body:
{ "categories": { "marketing": false, "shipping": true } }
Opting out is a PUT on the user's own preferences, not a flag on a notification. So it applies to every future send from every caller. A tenant cannot override it by setting a field on a request.
Notice what the three endpoints have in common. The caller states what it wants. The service, not the caller, decides what actually goes out.
Next: Step 5, where the services behind that API are laid out.
Reading Progress
0%
On This Page
Step 4: API Specifications