# Social

URL: https://docs.hanzo.ai/docs/openapi/social

> Posting to every social account you own, now or on a schedule.

Also for this capability: **API** · [CLI](https://docs.hanzo.ai/docs/cli/social) · [MCP](https://docs.hanzo.ai/docs/mcp-tools/social) · [SDKs](https://docs.hanzo.ai/docs/sdks)

Posting to every social account you own, now or on a schedule.

> [All API references →](https://docs.hanzo.ai/docs/openapi) · [Get started →](https://docs.hanzo.ai/docs/guides)

|                |                                        |
| -------------- | -------------------------------------- |
| **Base URL**   | `https://api.hanzo.ai`                 |
| **Operations** | 13                                     |
| **Auth**       | `Authorization: Bearer $HANZO_API_KEY` |

## Specification

> **HIP-1153 · Social — Publishing to Connected Channels** — Draft · [read the specification →](https://github.com/hanzoai/hips/blob/7ed737180339cd05b5f2935eafe7f7b83d2449a2/HIPs/hip-1153-social-channel-publishing.md)

`/v1/social` is posting to every social account an org owns, now or on a
schedule. Two entities: an Account is a connected channel (X, Facebook,
Instagram, LinkedIn, TikTok, YouTube, Threads) and a Post is content published or
scheduled to one — scheduling is not a third entity, it is a Post with a future
`scheduleAt`. It is implemented in `hanzoai/cloud` at `apps/social`, the
in-process fold of the standalone social stack, and its defining honesty is that
the publish edge fails closed: it reports exactly which provider credentials are
missing and never fakes success.

### Motivation

Social publishing had three homes: the standalone pods, a second path in the
content app reaching the same upstream over HTTP, and a third scheduled-post
store in marketing with no publisher wired at all. This fold — which owns the
accounts, the scheduler and the publish edge — is the one
(`apps/social/social.go`). Ground truth for the fail-closed default: no
deployment carries the per-provider OAuth-app credentials the live orchestrator
needs and no account access tokens exist, so there was no publishing capability
to preserve, only one to enable (`apps/social/publish.go`).

### Specification

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as
in RFC 2119.

#### The store

One SQLite file through the fleet's one opener (`sqlpool.Open("social", dir)`,
`apps/social/store.go:34`) holds every org's accounts and posts; tenant isolation
is the `org` column, enforced on every query. Post lifecycle is four
user-settable states (draft, scheduled, published, failed) plus a transient
`publishing` claim state that is NEVER user-settable: `ClaimForPublish` is the
guard that stops two publishers double-posting the same row, and stuck claims are
recovered at mount.

#### The address

Thirteen operations under `/v1/social`, all of them typed ops
(`apps/social/social.go:176-190`, models in `apps/social/typed.go`): the account
collection and item, the post collection and item,
`POST /v1/social/posts/{id}/publish`, a per-org summary and a `providers` read
reporting publish-readiness per network with the exact credential names still
missing. Nothing on this surface is wire-bound — every operation answers a value
its own handler assembled — so the debt this section recorded is paid rather
than deferred, and the ledger that would hold an exception is empty with the
count pinned at thirteen (`apps/social/typed_wire_test.go:122`, `:185`).

What is published is the store's own row: `socialAccount` and `socialPost` are
the types the store scans into rather than a view copied beside them, because a
second copy is a second thing to keep true and its drift is silent. What typing
adds is the REQUEST half a row cannot state — a create accepts no `id`,
`createdAt` or `updatedAt`, since the server mints all three, and an account's
provider token is neither accepted nor returned here at all
(`apps/social/typed.go:113-122`). The names carry the product because a Go type
name becomes a schema name in the one fleet document, where two apps may not
mean different things by one name (`openapi/weave.go:112`): the request bodies
are `socialAccountBody` and `socialPostBody`, never a bare `Account` or `Post`.

Where a handler assembled a map, field order is load-bearing: encoding/json
writes a map's keys sorted and a struct in declaration order, so `socialSummary`
declares its four alphabetically and the summary's bytes did not move
(`TestSummaryBytesDidNotMove`, `apps/social/typed_wire_test.go:283`).

#### Publishing

One path pushes a post out (`publish.go`): on explicit publish, on create when
scheduled for now-or-earlier, and on the scheduler tick when a scheduled time
arrives. A post fans out to its channel's connected accounts through the
Publisher edge, selected once at mount. The default Publisher MUST fail closed —
a publish with missing provider credentials answers 503 naming what is absent,
and MUST NOT mark the post published. The provider vocabulary is one ordered
list from which validation, ordering and credential-checking all derive.

#### Tenancy

The org is `principal.Org` — minted from the validated bearer owner (HIP-0026) —
never a client-supplied header, and it is the mandatory predicate on every store
query.

#### Money, events, observability, stage

It is free — the surface declares `cloud.Free` (`plugin/social/main.go`). It
publishes nothing on the bus; the scheduler delivers posts to networks, not
events to webhooks. It emits nothing beyond the request span every route gets.
The stage is `beta`: a vertical application whose per-account OAuth connect flow
and native provider push are the declared remaining gap.

#### Upstream

It derives from the standalone hanzoai/social stack — its own lineage, not a
third-party fork. What survives in HEAD is the model (integration → Account,
post-now-or-schedule → Post), the provider vocabulary, and the orchestrator's
exact credential names; the pods' HTTP surface is replaced by this in-process
fold. No third-party OSS is embedded; the networks are reached as remote APIs.

### Rationale

The alternative to a fail-closed publisher is a stub that returns success, which
is the worst possible product: a customer schedules a campaign and nothing is
delivered anywhere, silently. Reporting the missing credentials by name makes
the 503 an installation instruction. The claim state exists because the same
post is reachable from three triggers; without a claim, the scheduler and an
explicit publish racing is a double post on a customer's public channel.

### Security Considerations

The stored account rows will carry per-account OAuth tokens once the connect flow
lands, which makes the org predicate the boundary between one tenant's audience
and another's: the wrong implementation posts one org's content through another
org's accounts — public, attributable damage. The other wrong implementation is
accepting `publishing` from a request, which lets a caller wedge or replay the
claim guard; the state vocabulary therefore excludes it from every write path.

## Four surfaces

| Surface  | Reaches this capability as                     | Coverage                                                                   |
| -------- | ---------------------------------------------- | -------------------------------------------------------------------------- |
| **REST** | `social` at its own prefix                     | 13 operations                                                              |
| **CLI**  | `hanzo social …`                               | 13 of 13                                                                   |
| **SDK**  | `SocialApi` in every published client          | 13 methods                                                                 |
| **MCP**  | tool `social` on `https://api.hanzo.ai/v1/mcp` | 13 operations, 1 under the document's own id — ask `describe` for the rest |

## Quickstart

```bash
export HANZO_API_KEY=sk-...   # console.hanzo.ai → API keys
```

Then the first call — a read that needs nothing but the key. `GET /v1/social/posts`, operation `get_social_posts`:

**CLI**

```bash
hanzo social posts list
```

**SDK**

**.ts**

```ts
import { Configuration, SocialApi } from 'hanzoai';

const api = new SocialApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.getSocialPosts();
```

**.py**

```python
from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import SocialApi

client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = SocialApi(client).get_social_posts()
```

**.go**

```go
cfg := hanzoai.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := hanzoai.NewAPIClient(cfg)

resp, _, err := client.SocialAPI.GetSocialPosts(context.Background()).Execute()
if err != nil {
	return err
}
```

**.rs**

```rust
use hanzo_client::apis::{configuration::Configuration, social_api};

let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();

let result = social_api::get_social_posts(&cfg, Default::default()).await?;
```

**.java**

```java
import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.SocialApi;

ApiClient client = new ApiClient();
client.setBearerToken(System.getenv("HANZO_API_KEY"));

var result = new SocialApi(client).getSocialPosts();
```

**HTTP**

```bash
curl https://api.hanzo.ai/v1/social/posts \
  -H "Authorization: Bearer $HANZO_API_KEY"
```

**MCP**

MCP reaches **social** through the `social` tool, which names its 13 operations with its own verbs — this one among them, under a name only MCP declares. `describe` explains any of them:

```bash
curl -X POST https://api.hanzo.ai/v1/mcp \
  -H "Content-Type: application/json" \
  -d '{
       "jsonrpc": "2.0",
       "id": 1,
       "method": "tools/call",
       "params": {
         "name": "describe",
         "arguments": {
           "op": "list_social_accounts"
         }
       }
     }'
```

Answers `200` with `object` — ok.

## Endpoints

| Endpoint                                                                                                          | What it does                                                                                                                                                                                       |
| ----------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`GET /v1/social/accounts/{id}`](https://docs.hanzo.ai/docs/openapi/social/get-social-accounts-by-id)             | Returns one of the org's connected accounts by id — its network, handle, status and timestamps — or 404.                                                                                           |
| [`PUT /v1/social/accounts/{id}`](https://docs.hanzo.ai/docs/openapi/social/put-social-accounts-by-id)             | Replaces the account's network, handle and status with what the body carries, and answers with the stored row.                                                                                     |
| [`DELETE /v1/social/accounts/{id}`](https://docs.hanzo.ai/docs/openapi/social/delete-social-accounts-by-id)       | Removes one connected account from the org and answers 204 with no body; an id that is not there is 404.                                                                                           |
| [`GET /v1/social/accounts`](https://docs.hanzo.ai/docs/openapi/social/get-social-accounts)                        | Returns the org's connected accounts — each one's id, network, handle, status and timestamps, most-recently-updated first.                                                                         |
| [`POST /v1/social/accounts`](https://docs.hanzo.ai/docs/openapi/social/post-social-accounts)                      | Records a social account for the org and answers 201 with the stored row, including the generated id later calls address it by.                                                                    |
| [`POST /v1/social/posts/{id}/publish`](https://docs.hanzo.ai/docs/openapi/social/post-social-posts-by-id-publish) | Publishes the post immediately to the connected accounts on its channel and answers with the updated row, carrying the account and external id it published under.                                 |
| [`GET /v1/social/posts/{id}`](https://docs.hanzo.ai/docs/openapi/social/get-social-posts-by-id)                   | Returns one of the org's posts by id, with its current status, scheduled time, media and — once it has published — the account and external id it published under.                                 |
| [`PUT /v1/social/posts/{id}`](https://docs.hanzo.ai/docs/openapi/social/put-social-posts-by-id)                   | Replaces the post's content, channel, status, scheduled time and media with what the body carries, and answers with the stored row.                                                                |
| [`DELETE /v1/social/posts/{id}`](https://docs.hanzo.ai/docs/openapi/social/delete-social-posts-by-id)             | Removes one post from the org and answers 204 with no body; an id that is not there is 404.                                                                                                        |
| [`GET /v1/social/posts`](https://docs.hanzo.ai/docs/openapi/social/get-social-posts)                              | Returns the org's posts — content, channel, status, scheduled time, media and timestamps — most-recently-updated first.                                                                            |
| [`POST /v1/social/posts`](https://docs.hanzo.ai/docs/openapi/social/post-social-posts)                            | Stores a post for the org and answers 201 with the stored row.                                                                                                                                     |
| [`GET /v1/social/providers`](https://docs.hanzo.ai/docs/openapi/social/get-social-providers)                      | Reports each supported network's publish-readiness: whether this deployment holds the OAuth application credentials for it and, when it does not, exactly which environment variables are missing. |
| [`GET /v1/social/summary`](https://docs.hanzo.ai/docs/openapi/social/get-social-summary)                          | Returns four counts for the caller's org: total posts, how many are scheduled, how many have published, and how many accounts are connected.                                                       |

---

[All Hanzo APIs](https://docs.hanzo.ai/docs/openapi) · [Interactive reference](https://docs.hanzo.ai/docs/openapi/reference)
