मुख्य सामग्री पर जाएं

पार्टनर API संदर्भ

Society Speaks को प्रोग्राम्स रूप से इंटीग्रेट करें। JSON APIs के साथ अपने एम्बेड्स, स्नैपशॉट्स, और संपादकीय अंतर्दृष्टि को शक्तिशाली बनाएं जो न्यूज़रूम वर्कफ़्लो और डेवलपर गति के लिए बनाए गए हैं।

यदि आप यहाँ नए हैं: प्रकाशक प्रत्येक मतदान विषय के लिए एक चर्चा शुरू करते हैं — अक्सर एक लेख URL के साथ संरेखित, लेकिन आप इसके बजाय केवल अपना external_id हब या बिना स्थायी लिंक वाले उपकरणों के लिए उपयोग कर सकते हैं। कथन आपके ड्राफ़्ट करने के लिए हैं या उन्हें आपूर्ति किए गए प्रतिलिपि से ड्राफ़्ट किया जा सकता है; आपके होमपेज से कुछ भी अनुमान नहीं लगाया जाता है जब तक कि आपका एकीकरण इसके लिए नहीं पूछता। औपचारिक परिभाषाएँ साथ ही API फ़ील्ड मैपिंग: चर्चा बनाम कथन

त्वरित शुरुआत

curl "https://societyspeaks.io/api/discussions/by-article-url?url=https://example.com/article"

फिर embed_url के साथ एम्बेड करें।

डेवलपर्स के लिए

पूर्वानुमानित एंडपॉइंट्स, स्पष्ट त्रुटि कोड, और कॉपी/पेस्ट उदाहरण।

संपादकीय नेताओं के लिए

आप प्रत्येक प्रश्न चुनते हैं, यह आपकी कहानियों या आईडी से कैसे मैप करता है, और क्या पाठक-प्रस्तुत पंक्तियाँ एम्बेड में दिखाई देती हैं।

शासन के लिए

दर सीमाएं, एट्रिब्यूशन आवश्यकताएं, और सत्य का एक स्पष्ट स्रोत।

API का परीक्षण

  • Partner Portal: एक पोर्टल बनाएं Partner Portal:
  • इंटरैक्टिव खेल का मैदान: ओपन API प्लेग्राउंड Interactive playground:
  • Write endpoints (POST/PATCH /api/partner/...) केवल सर्वर-से-सर्वर हैं और आपके बैकएंड या curl/Postman से परीक्षण किए जाने चाहिए, ब्राउज़र Swagger से नहीं।
  • कमांड लाइन से: नीचे प्रत्येक खंड में curl उदाहरणों का उपयोग करें, आवश्यकतानुसार बेस URL और पैरामीटर को बदलते हुए।

Newsroom वर्कफ़्लो त्वरित मानचित्र: CMS मैपिंग के लिए external_id, सिस्टम सिंक के लिए webhook कॉलबैक, और एक्सेस कंट्रोल के लिए Partner Portal भूमिकाएं।

प्रमाणीकरण और वातावरण

रिहर्सल के समय परीक्षण कुंजियों का उपयोग करें; बिलिंग सक्रिय होने के बाद लाइव कुंजियों पर स्विच करें। कुंजियाँ Partner Portal में बनाई जाती हैं।

टेस्ट कुंजी: sspk_test_...
लाइव कुंजी: sspk_live_... (बिलिंग के बाद सक्रिय)

प्रत्येक hostname जहाँ आप iframe होस्ट करते हैं, DNS सत्यापन के बाद Domains के तहत दिखना चाहिए — रिहर्सल (test) और production (live) के लिए अलग से। सामान्य समाचार साइटें दोनों www. और short domain पंजीकृत करती हैं यदि पाठक दोनों का उपयोग करते हैं।

पाठक हमेशा Society Speaks के साथ सामान्य एन्क्रिप्टेड वेब ट्रैफिक पर इंटरैक्ट करते हैं; आपकी होस्टिंग टीम को हमारे public web address (BASE_URL हमारी ओर से) को मेल खिलाना चाहिए कि ब्राउज़र क्या expect करते हैं।

लेखन संचालन के लिए केवल सर्वर-से-सर्वर

चर्चा बनाएं और पार्टनर प्रबंधन रूट आपके सर्वर से कॉल किए जाने चाहिए, क्लाइंट-साइड JavaScript से नहीं (Origin हेडर वाले अनुरोध 403 के साथ अस्वीकृत होते हैं)। केवल-पढ़ने के लिए एंडपॉइंट (Lookup, Snapshot, oEmbed) API कुंजी के बिना ब्राउज़र से काम करते हैं। वेब ऐप से प्रमाणित अनुरोधों के लिए, अपने बैकेंड के माध्यम से प्रॉक्सी करें ताकि API कुंजियां गुप्त रहें।

HTTPS आवश्यक है

सभी API अनुरोध HTTPS पर किए जाने चाहिए और उत्पादन में आपके edge/proxy लेयर पर लागू किए जाने चाहिए। कभी भी अपनी API कुंजी को लॉग न करें या URL में शामिल न करें — हमेशा इसे X-API-Key हेडर में भेजें।

क्लाइंट लाइब्रेरीज

v0.3.0

Python और Node.js के लिए ड्रॉप-इन हेल्पर क्लाइंट। प्रत्येक एक एकल स्वतंत्र फ़ाइल है — कोई बिल्ड स्टेप नहीं, requests (केवल Python) से परे कोई transitive निर्भरता नहीं। केवल सर्वर-साइड उपयोग — कभी भी अपनी API कुंजी ब्राउज़र कोड में उजागर न करें।

1. अपनी परियोजना में जोड़ें

फ़ाइल डाउनलोड करें और इसे अपने एप्लिकेशन कोड के साथ रखें।

societyspeaks_partner.py डाउनलोड करें
# Place societyspeaks_partner.py anywhere in your project, then:
from societyspeaks_partner import SocietyspeaksPartnerClient, PartnerApiError

2. एक निर्भरता इंस्टॉल करें

pip install requests

3. शुरू करें और अपनी पहली कॉल करें

from societyspeaks_partner import SocietyspeaksPartnerClient, PartnerApiError

client = SocietyspeaksPartnerClient(
    base_url="https://societyspeaks.io",
    api_key="sspk_live_your_key_here",  # never expose in browser code
)

try:
    # Look up an existing discussion by article URL
    result = client.lookup_by_article_url("https://yoursite.com/article")
    print(result["embed_url"])
except PartnerApiError as e:
    if e.status_code == 404:
        # Discussion doesn't exist yet — create one
        discussion = client.create_discussion(
            title="Should cities ban single-use plastics?",
            article_url="https://yoursite.com/article",
            excerpt="A new UN report finds that single-use plastic bans...",
            embed_statement_submissions_enabled=False,
        )
        print(discussion["embed_url"])
    else:
        raise
▶ पूर्ण स्रोत देखें societyspeaks_partner.py
"""Lightweight Society Speaks Partner API client.

Intended as a reference wrapper for partners integrating from backend services.
"""

from __future__ import annotations

import hashlib
import hmac as _hmac
import time
import uuid
from typing import Any, Dict, List, Optional

import requests

SDK_VERSION = "0.3.0"


class PartnerApiError(Exception):
    def __init__(self, status_code: int, error: str, message: str, retry_after: Optional[int] = None):
        super().__init__(f"{status_code} {error}: {message}")
        self.status_code = status_code
        self.error = error
        self.message = message
        self.retry_after = retry_after


class SocietyspeaksPartnerClient:
    def __init__(self, base_url: str, api_key: str, timeout: int = 15, max_retries: int = 2):
        self.base_url = base_url.rstrip("/")
        self.api_key = api_key
        self.timeout = timeout
        self.max_retries = max_retries

    @property
    def sdk_version(self) -> str:
        return SDK_VERSION

    @staticmethod
    def verify_webhook_signature(
        raw_body: bytes,
        signature_header: str,
        timestamp_header: str,
        secret: str,
        tolerance_seconds: int = 300,
    ) -> bool:
        """Verify an incoming webhook request signature.

        Always call this before processing any webhook payload.

        Args:
            raw_body: The raw request body **bytes** — read before parsing JSON.
            signature_header: Value of the ``X-SocietySpeaks-Signature`` header.
            timestamp_header: Value of the ``X-SocietySpeaks-Timestamp`` header.
            secret: The signing secret issued when the webhook endpoint was created.
            tolerance_seconds: Reject requests older than this many seconds to prevent
                replay attacks. Defaults to 300 (5 minutes).

        Raises:
            ValueError: If the timestamp is missing, malformed, or outside tolerance.

        Returns:
            ``True`` if the signature is valid, ``False`` otherwise.
        """
        try:
            ts = int(timestamp_header)
        except (TypeError, ValueError):
            raise ValueError("Invalid or missing X-SocietySpeaks-Timestamp header.")

        age = abs(int(time.time()) - ts)
        if age > tolerance_seconds:
            raise ValueError(
                f"Webhook timestamp is {age}s old (tolerance: {tolerance_seconds}s). "
                "Possible replay attack — reject this request."
            )

        if isinstance(raw_body, str):
            raw_body = raw_body.encode("utf-8")

        signed_payload = f"{ts}.".encode() + raw_body
        expected = "sha256=" + _hmac.new(
            secret.encode("utf-8"),
            signed_payload,
            hashlib.sha256,
        ).hexdigest()
        return _hmac.compare_digest(expected, signature_header)

    @staticmethod
    def _retry_after_seconds(response: requests.Response) -> Optional[int]:
        raw = response.headers.get("Retry-After")
        if not raw:
            return None
        try:
            return max(0, int(raw))
        except (TypeError, ValueError):
            return None

    def _request(
        self,
        method: str,
        path: str,
        *,
        json_body: Optional[Dict[str, Any]] = None,
        params: Optional[Dict[str, Any]] = None,
        extra_headers: Optional[Dict[str, str]] = None,
    ):
        url = f"{self.base_url}{path}"
        headers = {
            "X-API-Key": self.api_key,
            "Content-Type": "application/json",
        }
        if extra_headers:
            headers.update(extra_headers)
        attempt = 0
        resp: Optional[requests.Response] = None
        while True:
            attempt += 1
            try:
                resp = requests.request(
                    method=method,
                    url=url,
                    headers=headers,
                    json=json_body,
                    params=params,
                    timeout=self.timeout,
                )
            except requests.RequestException as exc:
                if attempt > self.max_retries:
                    raise PartnerApiError(0, "network_error", str(exc)) from exc
                time.sleep(0.5 * attempt)
                continue
            if resp.status_code < 500 or attempt > self.max_retries:
                break
            time.sleep(0.5 * attempt)

        if resp is None:
            raise PartnerApiError(0, "network_error", "Request failed before receiving a response.")

        if 200 <= resp.status_code < 300:
            if resp.headers.get("Content-Type", "").startswith("application/json"):
                return resp.json()
            return resp.text

        body = {}
        try:
            body = resp.json() or {}
        except Exception:
            body = {}
        raise PartnerApiError(
            status_code=resp.status_code,
            error=body.get("error", "request_failed"),
            message=body.get("message", "Request failed."),
            retry_after=self._retry_after_seconds(resp),
        )

    def lookup_by_article_url(self, url: str):
        return self._request("GET", "/api/discussions/by-article-url", params={"url": url})

    def create_discussion(
        self,
        *,
        title: str,
        article_url: Optional[str] = None,
        external_id: Optional[str] = None,
        excerpt: Optional[str] = None,
        seed_statements: Optional[list] = None,
        source_name: Optional[str] = None,
        idempotency_key: Optional[str] = None,
        embed_statement_submissions_enabled: Optional[bool] = None,
    ):
        if not article_url and not external_id:
            raise ValueError("Provide at least one identifier: article_url or external_id.")
        if not excerpt and not seed_statements:
            raise ValueError("Provide excerpt or seed_statements.")
        if embed_statement_submissions_enabled is not None and not isinstance(embed_statement_submissions_enabled, bool):
            raise ValueError("embed_statement_submissions_enabled must be a boolean when provided.")

        payload = {
            "title": title,
            "article_url": article_url,
            "external_id": external_id,
            "excerpt": excerpt,
            "seed_statements": seed_statements,
            "source_name": source_name,
            "embed_statement_submissions_enabled": embed_statement_submissions_enabled,
        }
        payload = {k: v for k, v in payload.items() if v is not None}

        return self._request(
            "POST",
            "/api/partner/discussions",
            json_body=payload,
            extra_headers={"Idempotency-Key": idempotency_key or f"idem_{uuid.uuid4().hex}"},
        )

    def get_discussion_by_external_id(self, external_id: str, env: Optional[str] = None):
        params = {"external_id": external_id}
        if env:
            params["env"] = env
        return self._request("GET", "/api/partner/discussions/by-external-id", params=params)

    def list_discussions(self, *, env: str = "all", page: int = 1, per_page: int = 30):
        return self._request(
            "GET",
            "/api/partner/discussions",
            params={"env": env, "page": page, "per_page": per_page},
        )

    def patch_discussion(
        self,
        discussion_id: int,
        *,
        is_closed: Optional[bool] = None,
        integrity_mode: Optional[bool] = None,
        embed_statement_submissions_enabled: Optional[bool] = None,
    ):
        payload: Dict[str, Any] = {}
        if is_closed is not None:
            if not isinstance(is_closed, bool):
                raise ValueError("is_closed must be a boolean when provided.")
            payload["is_closed"] = is_closed
        if integrity_mode is not None:
            if not isinstance(integrity_mode, bool):
                raise ValueError("integrity_mode must be a boolean when provided.")
            payload["integrity_mode"] = integrity_mode
        if embed_statement_submissions_enabled is not None:
            if not isinstance(embed_statement_submissions_enabled, bool):
                raise ValueError("embed_statement_submissions_enabled must be a boolean when provided.")
            payload["embed_statement_submissions_enabled"] = embed_statement_submissions_enabled
        if not payload:
            raise ValueError("Provide at least one field to patch.")
        return self._request("PATCH", f"/api/partner/discussions/{discussion_id}", json_body=payload)

    def list_webhooks(self):
        return self._request("GET", "/api/partner/webhooks")

    def create_webhook(self, *, url: str, event_types: List[str]):
        return self._request(
            "POST",
            "/api/partner/webhooks",
            json_body={"url": url, "event_types": event_types},
        )

    def update_webhook(
        self,
        endpoint_id: int,
        *,
        status: Optional[str] = None,
        event_types: Optional[List[str]] = None,
    ):
        payload: Dict[str, Any] = {}
        if status is not None:
            payload["status"] = status
        if event_types is not None:
            payload["event_types"] = event_types
        if not payload:
            raise ValueError("Provide status and/or event_types to update.")
        return self._request("PATCH", f"/api/partner/webhooks/{endpoint_id}", json_body=payload)

    def delete_webhook(self, endpoint_id: int):
        return self._request("DELETE", f"/api/partner/webhooks/{endpoint_id}")

    def rotate_webhook_secret(self, endpoint_id: int):
        return self._request("POST", f"/api/partner/webhooks/{endpoint_id}/rotate-secret")

    def export_usage(self, *, days: int = 30, env: str = "all", page: int = 1, per_page: int = 100):
        return self._request(
            "GET",
            "/api/partner/analytics/usage-export",
            params={"days": days, "env": env, "format": "json", "page": page, "per_page": per_page},
        )

आधार URL

https://societyspeaks.io/api

चर्चा बनाम कथन

हम Partner Portal, API responses, और support materials में इन शब्दों का सुसंगत तरीके से उपयोग करते हैं — इसे एक बार देख लें और बाकी docs पूर्वानुमानित रहते हैं।

चर्चा
एक पाठक-सामना करने वाले voting experience के लिए छाता: headline title, यह आपकी साइट से कैसे जुड़ता है (article_url और/या external_id), partner environment (test/live), एक embed/consensus URLs, और इसके तहत combined participation। API के माध्यम से discussion बनाने से पहले यह container स्थापित होता है।
कथन
उस छाते के अंदर व्यक्तिगत prompts — प्रत्येक अपनी own agree / disagree / unsure counts एकत्र करता है। इन्हें seed_statements के साथ creation के दौरान populate करें या excerpt provide करें जो AI seeds को fuel करे; बाद में portal में इन्हें add/edit करें; optionally anonymous readers को अधिक propose करने की अनुमति दें जब embed submissions उस discussion के लिए toggled हों।

सामान्य वर्कफ़्लो

सामान्य pattern है "एक discussion प्रत्येक published URL के लिए," लेकिन यह एक choice है, requirement नहीं। आप article pages के लिए discussions बना सकते हैं, एक CMS id attach कर सकते हैं public article के बिना, या दोनों को mix कर सकते हैं ताकि reporting आपके stack में सुसंगत रहे।

identifiers की परवाह किए बिना, split को याद रखें: lookup/create discussion_id return करता है; statements को अलग से model किया जाता है और embed, portal statement tools, और snapshot payloads के माध्यम से surfaced होते हैं।

प्रोग्रामेटिक embed (अनुशंसित)

  1. 1) लेख URL द्वारा चर्चा खोजें
  2. 2) यदि कोई मौजूद नहीं है, तो discussion shell बनाएँ (optionally seed statements उसी call में)
  3. 3) discussion_id store करें और embed_url use करें; यदि आपको tweaks की ज़रूरत हो तो portal में statements review करें
# 1) Lookup
curl "https://societyspeaks.io/api/discussions/by-article-url?url=https://example.com/article" \
  -H "X-API-Key: sspk_test_..."

# 2) If 404, create
curl -X POST "https://societyspeaks.io/api/partner/discussions" \
  -H "X-API-Key: sspk_test_..." \
  -H "Content-Type: application/json" \
  -d '{"article_url":"https://example.com/article","title":"Example title","excerpt":"..."}'

मैनुअल embed (त्वरित शुरुआत)

एकबारी iframes बनाने के लिए Embed Generator का उपयोग करें। यह समान APIs पर निर्भर करता है और समान discussion_id और embed_url लौटाता है।

सरल लॉन्च चेकलिस्ट

Editors और producers नीचे दिए गए section को skim कर सकते हैं; IT के साथ engineering notes share करें।

Editorial & product teams के लिए

  • Vocabulary cheat sheet: एक discussion = एक voting topic plus its embed wrapper; कई statements नीचे बैठते हैं — वे lines जिनमें पाठक agree / disagree / unsure के साथ उत्तर देते हैं। लंबी व्याख्या →
  • आप decide करते हैं कि प्रत्येक voting topic को क्या कहा जाए और पाठक पहले क्या देखें — article tie-in optional है; live blogs, hubs, और special projects एक stable id का उपयोग कर सकते हैं story URL के बजाय।
  • हर web address जहाँ snippet load हो सकता है, वह Partner Portal में approved होना चाहिए — रिहर्सल sites और live sites अलग हैं।
  • यदि आपके पाठक दोनों www. और bare domain names type करते हैं, तो दो approvals register करें।
  • Engineering से short tracking tag (?ref=…) add करने के लिए कहें ताकि reports attributable रहें। Tags का अर्थ क्या है →
  • Optional: automatic height के लिए पूछें ताकि लंबे articles को clip न किया जाए (आपकी ओर से एक छोटी script)। यह कैसे काम करता है →
  • Manual checking के बजाय automatic "something changed" pings को अपने systems को prefer करें। Webhooks overview →

Engineering & hosting के लिए

  • परीक्षण API keys को केवल रिहर्सल domains के साथ match करें; live keys केवल verified production domains के साथ।
  • Rehearsal (test) embeds production में हमेशा को एक resolvable parent page की ज़रूरत होती है browser से — PARTNER_EMBED_REQUIRE_PARENT_ORIGIN के बिना भी।
  • Optional hardening env: PARTNER_EMBED_REQUIRE_PARENT_ORIGIN=true (production, live partner embeds only) iframe HTML को block करता है जब हम parent page को Origin या Referer से resolve नहीं कर सकते — stricter, और uncommon browsers या privacy settings को प्रभावित कर सकता है।
  • Embed से inline reader submissions EMBED_STATEMENT_SUBMIT_RATE_LIMIT_ACTOR और EMBED_STATEMENT_SUBMIT_RATE_LIMIT_IP को configuration में honour करते हैं (plus per-person limits)।

लेख URL द्वारा लुकअप

अपने article के URL से एक discussion खोजें। Lookup discussion metadata return करता है (discussion_id, embed_url, आदि); statements जिन पर पाठक vote करते हैं वह उस discussion के embed और snapshot payloads के साथ आते हैं।

GET /api/discussions/by-article-url

क्वेरी पैरामीटर

url आवश्यक लेख URL (URL-एन्कोडित)
ref वैकल्पिक विश्लेषण के लिए पार्टनर संदर्भ

उदाहरण अनुरोध

curl "https://societyspeaks.io/api/discussions/by-article-url?url=https://example.com/article"

सफलता प्रतिक्रिया (200)

{
  "discussion_id": 123,
  "slug": "should-we-reform-housing-policy",
  "title": "Should we reform housing policy?",
  "embed_url": "https://societyspeaks.io/discussions/123/embed",
  "consensus_url": "https://societyspeaks.io/discussions/123/should-we.../consensus",
  "snapshot_url": "https://societyspeaks.io/api/discussions/123/snapshot",
  "source": "rss",
  "env": "live"
}

त्रुटि प्रतिक्रियाएं

400 missing_url - URL पैरामीटर आवश्यक है
400 invalid_url - URL को पार्स नहीं किया जा सकता
403 partner_disabled - इस पार्टनर संदर्भ के लिए Embed और API एक्सेस रद्द कर दिया गया
404 no_discussion - इस URL के लिए कोई चर्चा नहीं
429 rate_limited - बहुत सारे अनुरोध; बाद में पुनः प्रयास करें

चर्चा स्नैपशॉट प्राप्त करें

भागीदारी गणना और मेटाडेटा प्राप्त करें। विश्लेषण सामग्री शामिल नहीं है।

GET /api/discussions/{discussion_id}/snapshot

पथ पैरामीटर

discussion_id आवश्यक चर्चा ID

उदाहरण अनुरोध

curl "https://societyspeaks.io/api/discussions/123/snapshot"

सफलता प्रतिक्रिया (200)

{
  "discussion_id": 123,
  "discussion_title": "Should we reform housing policy?",
  "participant_count": 847,
  "statement_count": 12,
  "has_analysis": true,
  "opinion_groups": 3,
  "analyzed_at": "2026-02-05T10:30:00Z",
  "consensus_url": "https://societyspeaks.io/discussions/123/.../consensus",
  "teaser_text": "Housing reform debate reveals surprising common ground"
}

जब विश्लेषण तैयार न हो

{
  "discussion_id": 123,
  "discussion_title": "Should we reform housing policy?",
  "participant_count": 23,
  "statement_count": 5,
  "has_analysis": false,
  "consensus_url": "https://societyspeaks.io/discussions/123/.../consensus"
}

त्रुटि प्रतिक्रियाएं

403 partner_disabled - इस पार्टनर संदर्भ के लिए Embed और API एक्सेस रद्द कर दिया गया
403 forbidden - परीक्षण चर्चाओं के लिए वैध परीक्षण API कुंजी आवश्यक है
404 discussion_not_found - चर्चा मौजूद नहीं है
429 rate_limited - बहुत सारे अनुरोध; बाद में पुनः प्रयास करें

एम्बेड URL पैरामीटर

URL पैरामीटर के साथ एम्बेड दिखावट को कस्टमाइज़ करें।

एम्बेड Society Speaks मूल चर्चाओं के लिए उपलब्ध हैं।

https://societyspeaks.io/discussions/{discussion_id}/embed
पैरामीटर विवरण उदाहरण
theme प्रीसेट थीम: default, dark, editorial, minimal, bold, muted theme=editorial
primary प्राथमिक रंग (# के बिना हेक्स) primary=1e40af
bg पृष्ठभूमि रंग (# के बिना हेक्स) bg=f9fafb
font अनुमत सूची से फॉन्ट परिवार font=Georgia
ref विश्लेषण के लिए पार्टनर संदर्भ ref=observer

अनुमत फॉन्ट: system-ui, Georgia, Inter, Lato, Open Sans, Source Serif Pro

एम्बेड: सत्र ट्रैकिंग और गोपनीयता

एम्बेड पेज लोड के दौरान गुमनाम वोट को डीडुप्लिकेट करने के लिए प्रथम-पक्ष कुकी का उपयोग करता है। जब एम्बेड एक क्रॉस-डोमेन iframe में लोड होता है (भागीदार साइटों के लिए सामान्य स्थिति), तो सख्त गोपनीयता सेटिंग वाले ब्राउज़र इस कुकी को तृतीय-पक्ष कुकी के रूप में अवरुद्ध कर सकते हैं।

अपने partner API के माध्यम से बनाए गए Discussions partner-scoped हैं: embed page parent site origin को check करता है (Origin या Referer का उपयोग करते हुए जब आवश्यक हो) आपके verified domains के विरुद्ध matching test/live environment के लिए। Native public discussions उस framing gate के अधीन नहीं हैं।

कोई कार्रवाई आवश्यक नहीं है। जब कुकी उपलब्ध नहीं होती है, तो एम्बेड स्वचालित रूप से localStorage-आधारित सत्र पहचानकर्ता (embed_fingerprint) पर फॉलबैक करता है। वोट डीडुप्लिकेशन दोनों मोड में काम करता है।

यह कैसे काम करता है:

  • एम्बेड एक यादृच्छिक प्रतिभागी ID उत्पन्न करता है और इसे एम्बेड origin के अंतर्गत localStorage में संग्रहीत करता है।
  • यह ID प्रत्येक वोट के साथ embed_fingerprint के रूप में भेजा जाता है सर्वर-पक्ष डीडुप्लिकेशन के लिए।
  • यदि उपयोगकर्ता साइट डेटा हटाता है या निजी ब्राउज़िंग का उपयोग करता है, तो एक नया सत्र शुरू होता है (वे फिर से वोट कर सकते हैं)।
  • Safari, Firefox, और Brave इस फॉलबैक के माध्यम से पूरी तरह समर्थित हैं।

PostMessage इवेंट्स

एम्बेड postMessage के माध्यम से पैरेंट frame के साथ संचार करता है।

societyspeaks:embed:loaded

भेजा जाता है जब एम्बेड लोडिंग समाप्त करता है।

{
  "type": "societyspeaks:embed:loaded",
  "discussionId": 123,
  "statementCount": 5
}

societyspeaks:embed:resize

भेजा जाता है जब एम्बेड सामग्री की ऊंचाई बदलती है। iframe को पुनः आकार देने के लिए उपयोग करें।

{
  "type": "societyspeaks:embed:resize",
  "discussionId": 123,
  "height": 450
}

उदाहरण: iframe को स्वचालित रूप से पुनः आकार दें

window.addEventListener('message', (event) => {
  if (event.data.type === 'societyspeaks:embed:resize') {
    const iframe = document.querySelector('iframe[src*="societyspeaks"]');
    if (iframe) iframe.style.height = event.data.height + 'px';
  }
});

oEmbed प्रदाता

ऐसे प्लेटफॉर्म में Society Speaks चर्चाओं को स्वचालित रूप से एम्बेड करने को सक्षम करें जो oEmbed को समर्थन करते हैं।

GET /api/oembed

क्वेरी पैरामीटर

url आवश्यक Society Speaks चर्चा URL
maxwidth वैकल्पिक अधिकतम एम्बेड चौड़ाई
maxheight वैकल्पिक अधिकतम एम्बेड ऊंचाई
format वैकल्पिक प्रतिक्रिया प्रारूप (केवल "json" समर्थित है)

उदाहरण अनुरोध

curl "https://societyspeaks.io/api/oembed?url=https://societyspeaks.io/discussions/123/my-discussion"

सफलता प्रतिक्रिया (200)

{
  "type": "rich",
  "version": "1.0",
  "title": "Should we reform housing policy?",
  "provider_name": "Society Speaks",
  "provider_url": "https://societyspeaks.io",
  "html": "<iframe src=\"https://societyspeaks.io/discussions/123/embed\" ...></iframe>",
  "width": 600,
  "height": 400,
  "cache_age": 3600
}

ऑटो-डिस्कवरी: चर्चा पृष्ठों में compatible प्लेटफॉर्म द्वारा स्वचालित oEmbed डिस्कवरी के लिए <link rel="alternate" type="application/json+oembed"> टैग शामिल होता है।

चर्चा बनाएं (प्रमाणित)

एक specific story के लिए एक discussion बनाएँ, या किसी भी editorial context के लिए जिसे आप एक stable id के साथ name कर सकते हैं। article_url supply करें जब आपके पास एक public link हो; external_id का उपयोग करें जब आपके पास न हो (या दोनों use करें traceability के लिए)। API key की आवश्यकता है।

यह endpoint हमेशा discussion पहले बनाता है। Statements को bundle करें same request में seed_statements के माध्यम से या excerpt supply करें ताकि हम seeds generate कर सकें; अन्यथा बाद में Partner Portal से statements add करें। पढ़ें कि discussion vs statements fields के लिए कैसे map होते हैं →

POST /api/partner/discussions

हेडर

X-API-Key आवश्यक आपकी भागीदार API कुंजी
Content-Type आवश्यक application/json

अनुरोध निकाय

article_url वैकल्पिक कैनोनिकल लेख URL (यदि प्रदान किया गया हो तो सामान्यीकृत)
external_id वैकल्पिक स्थिर भागीदार-परिभाषित ID (यदि article_url छोड़ा गया हो तो आवश्यक)
title आवश्यक चर्चा शीर्षक (अधिकतम 200 वर्ण)
excerpt सशर्त AI-generated statements के लिए article excerpt
seed_statements सशर्त {content, position} statements का array
source_name वैकल्पिक Attribution के लिए आपका publication का नाम
embed_statement_submissions_enabled वैकल्पिक इस discussion के लिए embed में inline reader submissions की अनुमति दें (default false)

Notes: कम से कम एक identifier (article_url या external_id) और excerpt या seed_statements दोनों में से कोई एक provide करें।

Recommendation: अधिकांश article pages के लिए embed_statement_submissions_enabled को false रखें, और इसे केवल उन discussions के लिए enable करें जहां आपकी टीम embed से inline statement intake चाहती है।

उदाहरण अनुरोध

curl -X POST "https://societyspeaks.io/api/partner/discussions" \
  -H "X-API-Key: your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: idem_$(openssl rand -hex 16)" \
  -d '{
    "article_url": "https://example.com/article/urban-cars",
    "title": "Should cities ban cars from downtown areas?",
    "excerpt": "A new study shows that car-free zones improve air quality...",
    "source_name": "Example News"
  }'

Success Response (201)

{
  "discussion_id": 456,
  "slug": "should-cities-ban-cars-from-downtown-areas",
  "title": "Should cities ban cars from downtown areas?",
  "partner_article_url": "https://example.com/article/urban-cars",
  "external_id": "observer-cms-12345",
  "embed_statement_submissions_enabled": false,
  "embed_url": "https://societyspeaks.io/discussions/456/embed",
  "consensus_url": "https://societyspeaks.io/discussions/456/.../consensus",
  "snapshot_url": "https://societyspeaks.io/api/discussions/456/snapshot",
  "source": "partner",
  "env": "live",
  "statement_count": 5
}

त्रुटि प्रतिक्रियाएं

401 invalid_api_key - Missing or invalid API key
400 missing_identifier या missing_content - (article_url या external_id) और excerpt/seed_statements की जरूरत है
409 discussion_exists - URL के लिए discussion पहले से मौजूद है

Discussion policy को update करें (Authenticated)

Discussions को close/reopen करने, integrity mode को toggle करने, और inline embed submissions को control करने के लिए इसका use करें।

curl -X PATCH "https://societyspeaks.io/api/partner/discussions/456" \
  -H "X-API-Key: your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "is_closed": false,
    "integrity_mode": true,
    "embed_statement_submissions_enabled": true
  }'

अधिकांश article pages के लिए recommended default: "embed_statement_submissions_enabled": false। Selected high-engagement discussions के लिए true enable करें (उदाहरण के लिए, live blogs या campaign hubs)।

Webhook callbacks (Authenticated)

Polling से बचने के लिए signed lifecycle callbacks configure करें। Portal-issued key के साथ server-to-server calls का use करें।

GET /api/partner/webhooks - webhook endpoints को list करें

POST /api/partner/webhooks - endpoint create करें और signing secret एक बार receive करें

PATCH /api/partner/webhooks/{endpoint_id} - pause/resume करें और event subscriptions को update करें

POST /api/partner/webhooks/{endpoint_id}/rotate-secret - signing secret को rotate करें

DELETE /api/partner/webhooks/{endpoint_id} - endpoint को delete करें

Request headers

हर webhook delivery में ये headers शामिल होते हैं:

Header विवरण
X-SocietySpeaks-EventEvent type, जैसे discussion.created
X-SocietySpeaks-Event-IdUnique event UUID — deduplication के लिए use करें
X-SocietySpeaks-TimestampUnix seconds (UTC) में जब event भेजा गया था
X-SocietySpeaks-SignatureHMAC-SHA256 signature — हमेशा इसे verify करें

हमेशा webhook signatures को verify करें

Verification के बिना, कोई भी party जो आपके endpoint URL को जानती है, forged events भेज सकती है। JSON parse करने से पहले raw body को पढ़ें, फिर अपने signing secret का use करके HMAC-SHA256 signature को verify करें। Replay attacks को prevent करने के लिए 5 minutes से पुरानी timestamps वाली requests को reject करें।

Signature format

Signed payload है {timestamp}.{raw_body} (Unix seconds, एक literal dot, फिर raw UTF-8 request body)। X-SocietySpeaks-Signature header है sha256=<hex_digest>

Verification example

# Flask example — adapt raw-body reading to your framework
from flask import Flask, request, abort
from societyspeaks_partner import SocietyspeaksPartnerClient
import os

app = Flask(__name__)
WEBHOOK_SECRET = os.environ["SS_WEBHOOK_SECRET"]  # store in env, never hardcode

@app.route("/webhooks/societyspeaks", methods=["POST"])
def handle_webhook():
    raw_body = request.get_data()  # read raw bytes BEFORE any JSON parsing

    try:
        valid = SocietyspeaksPartnerClient.verify_webhook_signature(
            raw_body=raw_body,
            signature_header=request.headers.get("X-SocietySpeaks-Signature", ""),
            timestamp_header=request.headers.get("X-SocietySpeaks-Timestamp", ""),
            secret=WEBHOOK_SECRET,
            tolerance_seconds=300,  # reject events older than 5 minutes
        )
    except ValueError as exc:
        # Timestamp missing, malformed, or outside the tolerance window
        abort(400, str(exc))

    if not valid:
        abort(401, "Invalid webhook signature.")

    event      = request.get_json()
    event_type = request.headers.get("X-SocietySpeaks-Event")
    event_id   = request.headers.get("X-SocietySpeaks-Event-Id")

    # Deduplicate using event_id (store processed IDs in your DB or cache)
    if already_processed(event_id):
        return "", 200

    if event_type == "discussion.created":
        handle_discussion_created(event["data"])

    return "", 200

Manual verification (SDK के बिना)

अगर आप हमारे SDK का use नहीं कर रहे हैं, तो ये steps बिल्कुल follow करें:

  1. JSON parsing से पहले raw request body को bytes के रूप में पढ़ें।
  2. X-SocietySpeaks-Timestamp को एक integer के रूप में parse करें। अगर यह absent है या 300 seconds से पुराना है, तो HTTP 400 के साथ reject करें।
  3. Signed payload build करें: {timestamp} + . + raw body bytes।
  4. HMAC-SHA256(key=signing_secret, msg=signed_payload) को compute करें और hex-encode करें।
  5. Expected signature string बनाने के लिए sha256= को prepend करें।
  6. X-SocietySpeaks-Signature के साथ एक constant-time comparison का use करके compare करें — Python में hmac.compare_digest, Node में crypto.timingSafeEqual। एक plain string equality check timing information leak करती है।

गोपनीय प्रबंधन

हस्ताक्षर गोपनीयता एंडपॉइंट बनाते समय एक बार लौटाई जाती है — इसे पर्यावरण चर या गोपनीयता प्रबंधक में सुरक्षित रूप से संग्रहीत करें, कभी भी स्रोत कोड में नहीं। यह पोर्टल में कभी फिर से दिखाया नहीं जाता है। POST /api/partner/webhooks/{endpoint_id}/rotate-secret का उपयोग करके नई गोपनीयता जारी करें; पुरानी को निष्क्रिय करने से पहले रोलओवर अवधि के दौरान संक्षेप में पुरानी और नई दोनों हस्ताक्षरों को स्वीकार करें।

विश्लेषण निर्यात (सत्यापित)

BI पाइपलाइन के लिए JSON या CSV में उपयोग घटनाएं निर्यात करें।

curl "https://societyspeaks.io/api/partner/analytics/usage-export?days=30&env=all&format=json&page=1&per_page=100" \
  -H "X-API-Key: your_api_key"

सीधी फ़ाइल अंतर्ग्रहण के लिए format=csv का उपयोग करें, या API उपभोक्ताओं के लिए पृष्ठांकित format=json का उपयोग करें।

लागत और दुरुपयोग सुरक्षा

कौन किसके लिए भुगतान करता है: Lookup, snapshot, oEmbed, और embed पृष्ठ हमारे LLM का उपयोग नहीं करते। वे दुरुपयोग को रोकने के लिए IP (और वैकल्पिक ref) प्रति दर-सीमित हैं। चर्चा बनाएँ एकमात्र एंडपॉइंट है जो AI-जनित बीज कथनों (OpenAI/Anthropic) को ट्रिगर कर सकता है; यह हमारी API कुंजियों का उपयोग करता है, आपकी नहीं।

भागीदार हमारी द्वारा जारी कुंजियों का उपयोग करते हैं। केवल वैध X-API-Key (हमारी अनुमति सूची से) वाले अनुरोध चर्चाएं बना सकते हैं। हम create flow के लिए भागीदार-प्रदत्त LLM कुंजियों का उपयोग नहीं करते। यदि कुंजी का दुरुपयोग किया जाता है, तो हम इसे रद्द कर देते हैं।

चर्चा बनाएँ प्रति घंटे प्रति API कुंजी 30 अनुरोधों तक सीमित है। कुंजी नियंत्रण के साथ मिलकर, यह create endpoint से LLM और डेटाबेस लागत को सीमित करता है।

दर सीमाएं

एंडपॉइंट सीमा
आर्टिकल URL द्वारा Lookup 60 req / min / IP
स्नैपशॉट प्राप्त करें 120 req / min / IP
वोट सबमिशन (डिफ़ॉल्ट) 30 req / min / IP
वोट सबमिशन (अखंडता मोड) 10 req / min / discussion
कथन निर्माण (सत्यापित) 10 प्रति घंटा
कथन निर्माण (अनाम) 5 प्रति घंटा
कथन को फ़्लैग करें (embed से) 10 req / min / IP
Embed से inline statement (partner-scoped discussions) Default 25 / reader / hour + IP ceiling; env देखें
चर्चा बनाएँ 30 req / hr / key

प्रतिक्रियाओं में 429 Too Many Requests और एक Retry-After शीर्षलेख शामिल है जो अगले अनुरोध तक सेकंड में इंगित करता है।

अखंडता मोड: कुछ चर्चाओं में अखंडता सुरक्षा के लिए सक्षम कठोर वोट दर सीमाएं हैं। embed इसे पारदर्शी रूप से संभालता है — उपयोगकर्ताओं को "कृपया धीमा करें" संदेश दिखाई देता है।

Embed inline statements: Flask-Limiter EMBED_STATEMENT_SUBMIT_RATE_LIMIT_ACTOR और EMBED_STATEMENT_SUBMIT_RATE_LIMIT_IP का उपयोग करता है (देखें config.py)। Per-user caps भी apply होते हैं statements के store होने से पहले।

त्रुटि प्रारूप

सभी त्रुटियां सुसंगत प्रारूप के साथ JSON लौटाती हैं।

{
  "error": "error_code",
  "message": "Human-readable description of the error"
}