Read your organisation's drives, stories and recorded totals, check certificates, and get told when things happen. It never carries personal data about volunteers.
Version 1 · JSON over HTTPS · 120 requests a minute per key · Webhooks signed with HMAC-SHA256
Getting started
The SocioStory API lets an NGO, a CSR team or a college connect its own website, CRM or reports to SocioStory. Read your drives with their seats and the hours volunteers recorded, list your published stories, check any SocioStory certificate, and have SocioStory tell your server when something happens.
Your organisation’s owners and admins make keys and webhooks in the dashboard, under Organisation settings → Developers & API. It’s free.
Base address
https://sociostory.org/api/v1
Format
JSON over HTTPS. Times are in UTC (ISO 8601); dates like 2026-09-26 are India time.
Keys
One per place that reads from SocioStory, each with its own permissions. Keep them on your server.
Limits
120 requests a minute for each key.
Personal data
None about volunteers: no names, contact details, rosters or certificate IDs.
Send your key in the Authorization header of every request. Keys start with ssk_. We keep only a fingerprint of each key, so we can’t show it to you again: if you lose one, revoke it and make another.
Never put a key in a web page or an app people download: anyone could read it there. The API doesn’t answer requests from browsers on other sites (it sends no CORS headers), so call it from your server.
Endpoints
Every endpoint answers GET requests. What each key can read depends on the permissions you gave it:
Endpoint
Permission
What it returns
GET /api/v1
none
What the API offers (no key needed).
GET /api/v1/organisation
any key
Your organisation, and the key's name and permissions.
GET /api/v1/drives
drives:read
Your drives, newest first. ?when=upcoming | past | all, ?from= and ?to= (dates), ?limit=, ?cursor=
GET /api/v1/drives/{id}
drives:read
One of your drives.
GET /api/v1/stories
stories:read
Your organisation's published stories, newest first. ?limit=, ?cursor=
GET /api/v1/certificates/{id}
certificates:read
Checks any SocioStory certificate by its ID, as its public page does.
The permissions you can give a key:
Permission
What it allows
drives:read
Drives: Your drives, their seats and their recorded totals.
stories:read
Stories: Your organisation's published stories.
certificates:read
Certificate checks: Check any SocioStory certificate by its ID, as its public page does.
A drive comes with its seats and, once SocioStory has confirmed it, what was recorded: the volunteers who attended and their hours, captured by QR check-in and your coordinator’s sign-off, net of any later correction, and the certificates issued. What your organisation counts itself, such as the people a drive reached or saplings still alive six months later, is under reported, with who reported it.
SocioStory doesn’t verify organisations’ work, compliance or finances. Please describe these figures the same way wherever you show them: “recorded” and “reported by”, never “verified”.
Drafts, drives waiting for SocioStory’s review and drives SocioStory has paused aren’t in the API.
GET /api/v1/stories lists the stories SocioStory has published for your organisation: title, standfirst, kind, cause, dates, the cover image’s address, the series and tags, and any numbers from a drive an editor attached. Stories written by CSR teams and companies carry "partner_update": true, as the site labels them. Bylines aren’t included.
Checking a certificate
GET /api/v1/certificates/{id} checks any SocioStory certificate: a course, a drive, a milestone or a letter of recommendation. Give the ID as it’s printed (any case, with or without dashes). It returns exactly what the certificate’s public page shows to anyone with its ID: whether it’s valid, revoked or replaced, what it’s for, and the name printed on it, unless the holder has withheld their name from public pages.
It also returns the signed record, so you can check the signature yourself with our public keys at /.well-known/sociostory-keys.json (Ed25519, over the canonical JSON of the payload).
Lists return up to limit items (20 unless you ask, at most 100) and a next_cursor. Pass it back as ?cursor= for the next page; when it’s null, you have everything. A cursor keeps its place even when new items arrive in between.
Errors
Errors come back as JSON with a code you can act on, a message in plain words and a link to this page: { "error": { "code": "invalid_key", "message": "…", "docs": "…" } }
Status
Code
What to do
400
bad_request
A parameter isn't right. The message says which.
401
invalid_key
The key is missing, wrong, revoked or expired.
403
insufficient_scope
The key doesn't have the permission this endpoint needs.
403
organisation_suspended
Your organisation is suspended on SocioStory; keys work again when it's reinstated.
404
not_found
No such item for your organisation (or no such endpoint).
405
method_not_allowed
The API only answers GET requests.
429
rate_limited
Too many requests. Wait for the seconds in the Retry-After header.
500
server_error
Something went wrong on our side. Try again in a minute.
Rate limits
Each key can make 120 requests a minute. Every answer says how many are left in X-RateLimit-Remaining; past the limit you get a 429 with Retry-After. Requests with a wrong key are limited too.
Webhooks
Add an endpoint (an https address on your server) in Developers & API, choose the events it should get, and SocioStory sends it a signed POST request with a JSON body whenever one happens:
Event
When
drive.published
One of your drives goes live, straight away or after SocioStory's review.
drive.completed
SocioStory confirms a drive: its hours are recorded and certificates issued.
drive.cancelled
Your team cancels a drive.
certificates.issued
Certificates are issued for one of your drives, or a letter your organisation signed. A count, never names.
story.published
SocioStory's editors publish one of your organisation's stories.
Every event has an id; if your server ever sees the same one twice, it can safely ignore the repeat. Certificates are sent as a count: never the volunteers’ names or certificate IDs.
Check every delivery before you trust it. SocioStory-Signature holds a timestamp (t) and one or more HMAC-SHA256 signatures (v1) of the timestamp, a full stop and the raw body, made with your endpoint’s signing secret. Compute the same over the body exactly as it arrived, compare in constant time, and reject timestamps more than five minutes old. After you make a new secret, deliveries carry a signature with each secret for 24 hours.
Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the request body exactly as it arrived (a string, before JSON.parse).
// secret: the whsec_… secret you copied when you added the endpoint.
export function isFromSocioStory(rawBody, header, secret, toleranceSeconds = 300) {
const parts = String(header ?? "").split(",").map((p) => p.trim().split("="));
const t = Number(parts.find(([k]) => k === "t")?.[1]);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
return parts
.filter(([k]) => k === "v1")
.some(([, sig]) => {
const given = Buffer.from(sig, "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
});
}
Python
import hashlib, hmac, time
def is_from_sociostory(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = [p.strip().split("=", 1) for p in (header or "").split(",")]
t = next((int(v) for k, v in parts if k == "t" and v.isdigit()), 0)
if not t or abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, v) for k, v in parts if k == "v1")
Retries and failures
Answer with any 2xx status within 10 seconds. Anything else counts as a failure (we don’t follow redirects), and we try again after 1 minute, 5 minutes, 30 minutes, 2 hours, 8 hours and 24 hours. If 5 deliveries in a row fail after every retry, we switch the endpoint off and tell your organisation’s admins. The delivery log in the dashboard keeps each attempt for 30 days, and any delivery can be sent again from there.
To try your endpoint, use Send a test event on its page: it sends a ping event straight away and shows what your server answered.
What's never in the API
SocioStory keeps volunteers’ details private, so the API and webhooks never carry:
volunteers’ names, emails, phone numbers, birth dates or colleges;
drive rosters, attendance by person, or certificate IDs;
messages, CVs, job applications, or anything from other organisations.
Your team still sees your own drives’ rosters in the dashboard, as before. Certificate checks show only what a certificate’s public page shows to someone who already has its ID.
Versions and changes
This is version 1 (/api/v1; every answer carries SocioStory-Api-Version: 1). We may add fields and events to it at any time, so ignore what you don’t recognise. Anything that would break your code comes as a new version, announced to every organisation with a key at least three months ahead.
Help
Questions, or something not working as described? Write to us through the contact details on our About page, with the time of the request and the SocioStory-Delivery ID or the endpoint you called. Never send us your key.