Skip to content

For developers

The SocioStory API

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.

Make a key

Authentication

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.

Shell
curl -H "Authorization: Bearer ssk_…" \
  https://sociostory.org/api/v1/organisation

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:

EndpointPermissionWhat it returns
GET /api/v1noneWhat the API offers (no key needed).
GET /api/v1/organisationany keyYour organisation, and the key's name and permissions.
GET /api/v1/drivesdrives:readYour drives, newest first. ?when=upcoming | past | all, ?from= and ?to= (dates), ?limit=, ?cursor=
GET /api/v1/drives/{id}drives:readOne of your drives.
GET /api/v1/storiesstories:readYour organisation's published stories, newest first. ?limit=, ?cursor=
GET /api/v1/certificates/{id}certificates:readChecks any SocioStory certificate by its ID, as its public page does.

The permissions you can give a key:

PermissionWhat it allows
drives:readDrives: Your drives, their seats and their recorded totals.
stories:readStories: Your organisation's published stories.
certificates:readCertificate checks: Check any SocioStory certificate by its ID, as its public page does.
Shell
curl -H "Authorization: Bearer ssk_…" \
  "https://sociostory.org/api/v1/drives?when=upcoming&limit=10"

What a drive record says

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.

JSON
{
  "data": [
    {
      "id": "k3v9x2m7p4q8r1s5t6u0w2y4a",
      "slug": "lake-clean-up-bhopal",
      "url": "https://sociostory.org/drives/lake-clean-up-bhopal",
      "title": "Lake clean-up",
      "summary": "Clearing plastic from the lake's edge before the monsoon.",
      "type": "cleanup",
      "type_label": "Clean-up",
      "cause": "Environment",
      "status": "completed",
      "starts_at": "2026-09-13T01:30:00.000Z",
      "ends_at": "2026-09-13T04:30:00.000Z",
      "published_at": "2026-09-01T06:00:00.000Z",
      "place": { "venue": "Boat Club gate", "city": "Bhopal", "district": "Bhopal", "state": "Madhya Pradesh", "pincode": "462001" },
      "seats": { "total": 20, "filled": 18, "waitlisted": 3 },
      "recorded": { "volunteers": 16, "hours": 48, "certificates": 16, "confirmed_at": "2026-09-14T09:12:00.000Z" },
      "reported": {
        "people_reached": null,
        "followups": [{ "metric": "bags of waste collected", "value": 64, "out_of": null, "counted_on": "2026-09-13", "reported_by": "Your NGO" }],
        "reported_by": "Your NGO"
      },
      "tags": { "schedule_vii": ["iv"], "sdgs": [6, 14], "aspirational_district": false },
      "organisation": { "id": "q2m8…", "slug": "your-ngo", "name": "Your NGO" }
    }
  ],
  "next_cursor": null
}

Stories

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).

Shell
curl -H "Authorization: Bearer ssk_…" \
  https://sociostory.org/api/v1/certificates/7KQ2-M9XD-4TPR-W6HJ-3N5B-8CFA-QZ

Pages of results

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": "…" } }

StatusCodeWhat to do
400bad_requestA parameter isn't right. The message says which.
401invalid_keyThe key is missing, wrong, revoked or expired.
403insufficient_scopeThe key doesn't have the permission this endpoint needs.
403organisation_suspendedYour organisation is suspended on SocioStory; keys work again when it's reinstated.
404not_foundNo such item for your organisation (or no such endpoint).
405method_not_allowedThe API only answers GET requests.
429rate_limitedToo many requests. Wait for the seconds in the Retry-After header.
500server_errorSomething 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:

EventWhen
drive.publishedOne of your drives goes live, straight away or after SocioStory's review.
drive.completedSocioStory confirms a drive: its hours are recorded and certificates issued.
drive.cancelledYour team cancels a drive.
certificates.issuedCertificates are issued for one of your drives, or a letter your organisation signed. A count, never names.
story.publishedSocioStory'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.

HTTP
POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: SocioStory-Webhooks/1 (+https://sociostory.org/developers)
SocioStory-Event: drive.completed
SocioStory-Delivery: 7f3k2m9x4p8q1r5s6t0v2w4y
SocioStory-Signature: t=1790000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

{
  "id": "evt_7kq2m9xd4tprw6hj3n5b8cfaqz",
  "type": "drive.completed",
  "created_at": "2026-09-14T09:12:05.000Z",
  "api_version": 1,
  "organisation": { "id": "q2m8…", "slug": "your-ngo", "name": "Your NGO" },
  "data": { "drive": { "id": "k3v9…", "title": "Lake clean-up", "status": "completed", "recorded": { "volunteers": 16, "hours": 48, "certificates": 16 } } }
}

Checking the signature

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.