# Frontend integration guide: notification click actions **Audience**: the team building the Gooyal client app(s) that receive Gotify push notifications. This is everything you need to start building tap-to-navigate, independent of the backend repos. ## The one thing to build When a push notification is tapped, read a URL out of the notification's payload and navigate to it. That's the entire client-side contract — one handler, used by every notification type below, present or future. ## Where the URL lives Gotify delivers a JSON payload per message. The URL — when the notification is clickable at all — is under a reserved key, per [Gotify's own convention](https://gotify.net/docs/pushmsg#extras): ```json { "title": "بیلبورد شما تایید شد.", "message": "بیلبورد گربه (تست) تایید شد.", "priority": 5, "extras": { "ads::ad::approve": true, "ad_uuid": "8fb638c2-e6a4-4baf-aefe-83dab78fb5bd", "client::notification": { "click": { "url": "https://app.gooyal.ir/ads/8fb638c2-e6a4-4baf-aefe-83dab78fb5bd" } } } } ``` Pull `extras["client::notification"]["click"]["url"]`. If it's present, tapping navigates there. **If it's absent, do nothing on tap** — see next section. ## `click_url` is optional — most notifications are not clickable Do not assume every notification carries a URL. The backend field this comes from is explicitly optional (nullable end-to-end, not just "sometimes empty string") specifically because not every notification type has a sensible destination. Any notification without a `client::notification.click.url` key should render and behave exactly as a non-interactive notification — no error, no dead tap target, just no navigation. You may also see other keys under `extras` alongside (or instead of) `client::notification` — e.g. chat currently sends `conversation_uuid` and `post_id` directly, not yet through this mechanism (see "Not yet migrated" below). Ignore keys you don't recognize; don't treat an unfamiliar key as an error. ## URLs are admin-configured, not hard-coded — nothing for you to build here The actual destination string is no longer computed in backend code. Each notification type has a **code**, and the notifications service resolves that code against an admin-managed table (`NotificationLink`: `code`, `url_template`, `is_active`) at send time. `url_template` supports a `{object_id}` placeholder, e.g. `https://app.gooyal.ir/ads/{object_id}` or `gooyal://ads/{object_id}` — whichever scheme your app actually uses. This means: **your routing scheme is a config decision, not a code change**. Whoever manages the notifications service's admin panel enters one row per code below with your actual URL format, and every notification of that type immediately starts carrying it. If a code has no row yet (or its row is marked inactive), that notification simply has no `click_url` — same as any other non-clickable notification, no error. Practically, this doesn't change anything about what you build — you still just read `extras["client::notification"]["click"]["url"]` and navigate if it's present. It changes who's responsible for the URL *string itself*: not a backend deploy, just an admin panel entry. If you want a different value than what's currently configured for any code, ask whoever owns that panel to update it. ## What's clickable today, by code All of these are live in the `advertising` service: ### Billboards & content | Code | Notification text (fa) | |---|---| | `billboard.viewed` | شخصی شروع به مشاهده بیلبورد شما کرد. | | `billboard.created` | بیلبود شما با موفقیت ساخته شد. | | `billboard.approved` | بیلبورد شما تایید شد. | | `billboard.rejected` | بیلبود شما رد شد. | | `billboard.commented` | یک نفر برای بیلبورد شما نظر گذاشت! | | `billboard.replied` | یک نفر به نظر شما پاسخ داد! | | `billboard.content_bought` | درآمد جدید دارید! یک نفر محتوای شما را خرید. | | `billboard.content_supported` | یک نفر از محتوای شما حمایت کرد! | | `billboard.pin_expiring` | زمان پین رو به اتمامه! | | `billboard.pin_expired` | پین شما منقضی شد! | | `billboard.credit_low` | اعتبار پین رو به اتمامه! | | `billboard.credit_exhausted` | اعتبار پین شما به پایان رسید! | Every code above sends the ad's `uuid` as `click_object_id`, so a `url_template` of `https://app.gooyal.ir/ads/{object_id}` (or your app's real equivalent) resolves correctly for all twelve. They're still separate codes rather than one shared one, on purpose: `billboard.pin_expiring` and `billboard.credit_low` are genuinely different situations (time running out vs. budget running out) even though they'd point at the same screen today — giving each its own code means that can diverge later (e.g. deep-linking straight to an "extend time" vs. "top up credit" action) without any code change, just a new admin row. ### Escrow deals | Code | Trigger | |---|---| | `escrow.request_deal` | Buyer paid, seller needs to review | | `escrow.cancel_deal` | Buyer canceled before seller responded | | `escrow.reject_deal` | Seller declined the deal | | `escrow.approve_deal` | Seller confirmed the deal | | `escrow.cancel_deal_by_seller` | Seller canceled an active deal | | `escrow.request_cancel` | Buyer requested cancellation | | `escrow.approve_cancel` | Seller accepted the cancellation | | `escrow.reject_cancel` | Seller declined the cancellation | | `escrow.confirm_by_seller` | Seller marked it delivered | | `escrow.confirm_by_buyer` | Buyer confirmed receipt — deal completed | | `escrow.request_judge` | A dispute was opened | | `escrow.cancel_judge` | Buyer withdrew their dispute | | `escrow.approve_judge` | Dispute resolved for the buyer | | `escrow.reject_judge` | Dispute resolved for the seller | | `escrow.timeout` | Deal expired without a response | All fifteen send the escrow deal's `uuid` as `click_object_id`. Same one-code-per-situation reasoning as billboards — they all currently resolve to the same escrow-detail destination, but that's an admin config choice, not a hard-coded one, so it's free to diverge later. ## Not yet migrated to this mechanism - **chat**: sends `extras = {"conversation_uuid": ..., "post_id": ...}` directly, without a code or `click_url`. If your client already has custom handling for these two keys (built before this mechanism existed), it keeps working — this doc doesn't change that. Let us know if you'd rather chat send a code too. - **promotions**: integrated with the notifications service but not currently sending any notification with real content (`extras={}` today) — nothing to build against yet.