पार्टनर 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 करें याexcerptprovide करें जो 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) लेख URL द्वारा चर्चा खोजें
- 2) यदि कोई मौजूद नहीं है, तो discussion shell बनाएँ (optionally seed statements उसी call में)
- 3)
discussion_idstore करें औरembed_urluse करें; यदि आपको 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"
import requests
resp = requests.get(
"https://societyspeaks.io/api/discussions/by-article-url",
params={"url": "https://example.com/article"}
)
data = resp.json()
embed_url = data["embed_url"]
const resp = await fetch(
`https://societyspeaks.io/api/discussions/by-article-url?` +
new URLSearchParams({ url: "https://example.com/article" })
);
const data = await resp.json();
const embedUrl = data.embed_url;
सफलता प्रतिक्रिया (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"
}'
import requests
resp = requests.post(
"https://societyspeaks.io/api/partner/discussions",
headers={
"X-API-Key": "your_api_key",
"Content-Type": "application/json",
},
json={
"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",
},
)
data = resp.json()
discussion_id = data["discussion_id"]
embed_url = data["embed_url"]
const resp = await fetch("https://societyspeaks.io/api/partner/discussions", {
method: "POST",
headers: {
"X-API-Key": "your_api_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
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",
}),
});
const data = await resp.json();
const { discussion_id, embed_url } = data;
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-Event | Event type, जैसे discussion.created |
| X-SocietySpeaks-Event-Id | Unique event UUID — deduplication के लिए use करें |
| X-SocietySpeaks-Timestamp | Unix seconds (UTC) में जब event भेजा गया था |
| X-SocietySpeaks-Signature | HMAC-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
// Express example — adapt raw-body reading to your framework
const express = require("express");
const { SocietyspeaksPartnerClient } = require("./societyspeaks_partner");
const app = express();
const WEBHOOK_SECRET = process.env.SS_WEBHOOK_SECRET; // store in env, never hardcode
// Use express.raw() so the body stays as a Buffer before any parsing
app.post(
"/webhooks/societyspeaks",
express.raw({ type: "application/json" }),
(req, res) => {
const rawBody = req.body; // Buffer — do NOT call JSON.parse before verifying
let valid;
try {
valid = SocietyspeaksPartnerClient.verifyWebhookSignature(
rawBody,
req.headers["x-societyspeaks-signature"] ?? "",
req.headers["x-societyspeaks-timestamp"] ?? "",
WEBHOOK_SECRET,
300, // reject events older than 5 minutes
);
} catch (err) {
// Timestamp missing, malformed, or outside the tolerance window
return res.status(400).send(err.message);
}
if (!valid) return res.status(401).send("Invalid webhook signature.");
const event = JSON.parse(rawBody);
const eventType = req.headers["x-societyspeaks-event"];
const eventId = req.headers["x-societyspeaks-event-id"];
// Deduplicate using eventId (store processed IDs in your DB or cache)
if (alreadyProcessed(eventId)) return res.sendStatus(200);
if (eventType === "discussion.created") {
handleDiscussionCreated(event.data);
}
res.sendStatus(200);
}
);
Manual verification (SDK के बिना)
अगर आप हमारे SDK का use नहीं कर रहे हैं, तो ये steps बिल्कुल follow करें:
- JSON parsing से पहले raw request body को bytes के रूप में पढ़ें।
X-SocietySpeaks-Timestampको एक integer के रूप में parse करें। अगर यह absent है या 300 seconds से पुराना है, तो HTTP 400 के साथ reject करें।- Signed payload build करें:
{timestamp}+.+ raw body bytes। HMAC-SHA256(key=signing_secret, msg=signed_payload)को compute करें और hex-encode करें।- Expected signature string बनाने के लिए
sha256=को prepend करें। 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"
}
प्रश्न हैं? हमसे संपर्क करें या भागीदार हब.