Men of Hunger Partner API
The Men of Hunger (MOH) Partner API lets an approved application connect a member's account with OAuth 2.0 / OpenID Connect and read the public information that member allows. It is read-only: there are no partner endpoints to publish, reply, react, follow, upload, or delete MOH content. Members create on MOH itself.
Access is by approval. If the MOH team has given your account developer access, create up to three apps yourself under Settings › Developer apps on menofhunger.com (one for testing and one for production is a good split). Otherwise, ask the MOH team for credentials. Members see your account's handle on the consent screen, so build from the account your users know. Using the API means agreeing to the API terms.
Every endpoint, parameter and response shape is listed in the API reference, with machine-readable OpenAPI at api.menofhunger.com/partner/openapi.json.
Quickstart
- Create a test app under Settings › Developer apps (or ask the MOH team for one) to get a client ID, client secret and webhook secret. Register exact HTTPS callback/logout URLs and, if you want webhooks, a public HTTPS webhook URL. Test and production use different clients. Keep secrets on your server.
- Discover
${issuer}/.well-known/openid-configuration. The production issuer is the API origin followed by/oauth; use the assigned issuer rather than guessing a host. The resource identifier is the same origin followed by/v1/partner. - Redirect the member to the discovered authorization endpoint with
response_type=code, your registeredredirect_uri,client_id,scope, unpredictablestate,nonce, and PKCEcode_challengewithcode_challenge_method=S256. Useprompt=consentwhen requesting offline access. Store state, nonce, and the verifier in a short-lived server-side session bound to the initiating browser. Never put the verifier or client secret in a browser URL. - The member signs in or completes signup, explicitly selects their person/page, and approves scopes. Validate state on return. A denial ends this attempt; show a retry button without automatically restarting authorization.
- Exchange the code at the discovered token endpoint using HTTP Basic client authentication, the exact redirect URI and PKCE verifier. Include
resource=<API origin>/v1/partnerin code exchange and refresh requests for partner reads. Validate ID tokens with a maintained OIDC library, including issuer, audience, signature, expiry, and nonce. Download the runnable TypeScript example and its setup instructions. - Call
GET <resource>/mewithAuthorization: Bearer <access_token>. Store its immutableidas the selected MOH account. FetchGET <resource>/users/{username}/posts, thenGET <resource>/posts/{id}. Treat usernames as display values, not identity keys. - Verify webhook signatures on raw bytes, persist event IDs, then acknowledge quickly with a 2xx response. Process asynchronously.
- To disconnect read access, use the discovered revocation endpoint with client authentication and the refresh token. Members can also revoke in MOH's Connected apps settings. Delete cached personal/social data when access ends. Retain only identifiers you need for required records.
Example identity response:
{"data":{"id":"moh_account_id","username":"example","name":"Example","bio":null,"avatarUrl":null,"canonicalUrl":"https://menofhunger.com/u/example","accountKind":"person","createdAt":"2026-09-30T12:00:00.000Z","counts":{"followers":4,"following":8}}}
If a client secret may have leaked, rotate it under Settings › Developer apps (or ask MOH if the team issued your app). The new secret is issued once and the old one stops working immediately, so deploy the new value promptly. Webhook secrets rotate with an overlap period (see Webhooks).
Identity, pages, and consent
OIDC sub identifies the human who authenticated. /me.data.id identifies the selected resource account, which can be a page. Never substitute one for the other. Page-only authorization can omit openid profile and use OAuth without requesting the human's profile. When using OIDC, use UserInfo for human profile claims. Resource tokens are audience-bound to the partner API and are not UserInfo tokens; request a separate token without resource if UserInfo is needed.
The initiating operator's authority is checked at consent, code redemption, refresh, and every resource access. A removed operator's grant stops working; a current operator must reauthorize. Connecting is free.
| Scope | Access |
|---|---|
openid |
Human OIDC sign-in identity |
profile |
Human public OIDC profile claims |
offline_access |
Refresh token, subject to consent |
account:read |
Selected account and public profiles |
verification:read |
Selected account's current verification category and timestamp |
content:read |
Public posts, articles, comments and content search |
social:read |
Selected account's followers/following, and friend.created / friend.removed webhooks |
webhooks:read |
Subscribed events, additionally gated by the event's read scope |
Effective scopes are the intersection of the application's registered scopes and the member's consent. Verification status is none, manual, or identity; it is an MOH status, not identity evidence or a guarantee about a member's conduct. Evidence and documents are never included.
Public resource API
All paths below are relative to /v1/partner.
| Method and path | Scope |
|---|---|
GET /me, GET /users/{username} |
account:read |
GET /me/verification |
verification:read |
GET /me/followers, GET /me/following |
social:read |
GET /users/{username}/posts, GET /users/{username}/articles |
content:read |
GET /posts/{id}, GET /articles/{id} |
content:read |
GET /posts/{id}/comments, GET /articles/{id}/comments |
content:read |
GET /search?q=example&type=users |
account:read |
GET /search?q=example&type=posts or type=articles |
content:read |
Single responses use { data }; lists use { data: [], pagination: { nextCursor } }. Pass cursor unchanged. Default limit is 20; maximum 100, or 50 for search. A null next cursor ends the list. There is no bulk export. An inaccessible resource returns 404. Public reads do not record views or mark notifications read.
Objects contain immutable IDs, canonical MOH URLs, status: "published", timestamps, allowlisted author profiles and public media URLs/alt text. Post bodies are plain text; article bodies are allowlisted HTML. Article comments include articleId and parentId. Missing optional fields and explicit nulls must be handled. Do not infer visibility, verification evidence, email, or operator identity from omitted fields.
The public ceiling applies even when the member can see more inside MOH. Drafts, scheduled content, restricted content, groups, Board shells, quoted/reposted objects, blocked/banned accounts, deleted content, messages and personal notifications are excluded. Counts and pagination are filtered for the connected account. The engagement object reports visible boosts/comments and recorded distinct signed-in viewers (uniqueViewers), plus reaction records on articles. It excludes blocked/banned accounts; it is not an audience or reach estimate. Advanced analytics and reach are not part of this API.
The production reference is api.menofhunger.com/partner/docs; machine-readable OpenAPI is api.menofhunger.com/partner/openapi.json. It exposes partner routes only, independently of internal/admin documentation. OAuth responses and errors follow OAuth/OIDC standards; resource errors use MOH's { meta: { status, errors: [{ code, message, reason }], requestId? } } envelope.
Token and retry rules
Authorization codes expire after five minutes and are single-use. Access tokens are opaque and expire after 15 minutes. Refresh tokens rotate, expire after 30 days of inactivity, and cannot extend a grant beyond 180 days.
Serialize refresh operations per grant in your shared store. For the same authenticated client, an identical refresh request replayed within ten seconds returns the original encrypted-cached result. Concurrent work still in progress may return 429 with Retry-After: 1. Retry the identical request after waiting. Reuse after the window revokes the token family; obtain new consent. Changed scope/resource parameters are not an identical replay. Persist each successful rotated token atomically before releasing your refresh lock. Never run independent refreshes in two workers.
| Limit per minute | Default |
|---|---|
| Reads per client/account | 120 |
| Reads per client | 1,200 |
| Search per client/account | 30 |
| Token calls per client | 600 |
| Refresh calls per grant | 60 |
These are default allocations, shared across API instances; ask MOH if your integration needs more. Resource responses carry X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (Unix seconds). The headers describe the tightest checked budget. A 429 includes Retry-After in seconds. Add jitter and honor it. Partner requests do not consume first-party posting quotas. UserInfo shares the client/account read allocation. Authenticated introspection and revocation share a separate 600 calls/client/minute allocation and can inspect or revoke only that client’s tokens. Unauthenticated abuse has separate IP protection.
Webhooks
MOH administrators configure destinations and subscriptions initially. Supported event names: profile.updated, verification.updated, post.updated, post.removed, article.updated, article.removed, comment.updated, comment.removed, mention.created, follow.created, follow.removed, friend.created, friend.removed, connection.revoked.
Events concern connected accounts, not a global firehose. Each attempt rechecks consent, current authority and public visibility, then builds a fresh allowlisted payload. Removal events contain identifiers, not deleted content. Revocation immediately stops ordinary delivery; one minimal terminal revocation event is allowed. Store deletions as tombstones, and discard outdated versions.
Public content only. Friends-only posts, profile Wall posts, and Wall comments and replies are never delivered: no post.*, comment.* or mention.created event is emitted for them, and no read endpoint returns them. A post that becomes friends-only is reported as post.removed (identifier only).
friend.created and friend.removed need social:read and mirror follow.*. They are sent once per member of the friendship, for the connected account, with resource.kind friendship and resource.id the other member's account id. data is {"user":<profile preview>,"friend":<profile preview>,"occurredAt":"<ISO date>"}. friend falls back to {"id":"<account id>"} when that member is blocked, banned or otherwise not visible to the connected account (a block also ends a friendship). Friends lists, the Top 5, and friends-only content are never included. Friends exist only between verified members: when either member is unverified the friendship is dormant, so friend.removed is sent when a friendship goes dormant (a member loses verification) and friend.created when it becomes active again, exactly as for a new or ended friendship.
{"id":"event_id","type":"post.updated","origin":"menofhunger","createdAt":"2026-09-30T12:00:00.000Z","version":"123","accountId":"moh_account_id","resource":{"kind":"post","id":"post_id"},"data":{"id":"post_id"}}
data above is abbreviated; content events carry the current public object. Version is a decimal string, compared numerically per account/resource; gaps are normal. A webhook is a notification to update your cached view, not a publishing instruction.
X-MOH-Signature: t=<Unix seconds>,v1=<hex HMAC> signs the raw UTF-8 string <timestamp>.<raw body> using HMAC-SHA256. X-MOH-Event-Id must match the signed body's id. Accept any valid v1 during secret rotation; compare in constant time. Reject timestamps more than five minutes away. Deduplicate event IDs durably; retries are signed with a fresh timestamp. Treat lower versions as stale. Persist the event before acknowledging.
Delivery is at least once, with a ten-second request timeout and exponential retries bounded to 72 hours. Diagnostics are retained for 30 days. Redirects and private-network destinations are rejected, and DNS is pinned for each attempt. Ask MOH for replay or delivery inspection; keep your handler idempotent.