zhyper docs

Platform capabilities

Platforms do not agree on anything. One takes four images, another takes one. One accepts a 9:16 video and rejects 1:1. One has no analytics per post but plenty per location. Hard-coding those differences in your client means editing your client every time we ship one.

GET /v1/platforms is the machine-readable answer. It returns one entry per platform your key can reach, and every limit in it is the same value the validator and the publisher use — not a copy maintained alongside them.

export async function platformCatalog(zhyper: ZhyperClient): Promise<Catalog> {
  // Katalog ANAHTARIN modunu izler: test anahtari sandbox registry'sini,
  // canli anahtar uretim registry'sini gorur. Ikisi ayni platform kumesini
  // tasimak ZORUNDA DEGILDIR.
  return zhyper.request<Catalog>('listPlatforms', {});
}

The catalogue follows your key's mode. A test-mode key sees the sandbox registry, a live key sees the production one, and the two need not carry the same platforms — a platform can be complete in sandbox and still gated off in production.

Reading a capability

Each entry carries platform, capabilities, mediaDelivery, quotaModel, apiVersion and apiSunsetAt.

Inside capabilities, most fields are either an object of limits or the literal false. That false is doing real work, and it means different things at different depths:

export async function imageLimitsFor(
  zhyper: ZhyperClient,
  platform: string,
): Promise<{ supported: boolean; maxCount?: number; maxBytes?: number }> {
  const catalog = await platformCatalog(zhyper);
  const entry = catalog.find((row) => row.platform === platform);
  if (!entry) return { supported: false };

  // `publish` VE `image` ikisi de `false` OLABILIR ve bu iki AYRI sey soyler:
  // `publish: false` platformun bu API'den hic yayin kabul etmedigini,
  // `image: false` yayin kabul ettigini ama GORUNTU kabul etmedigini. Ikisini
  // ayni kovaya koyan bir istemci ikinci durumda yanlis hata gosterir.
  const publish = entry.capabilities.publish;
  if (publish === false) return { supported: false };
  const image = publish.image;
  if (image === false) return { supported: false };

  return { supported: true, maxCount: image.maxCount, maxBytes: image.maxBytes };
}
  • publish: false — the platform accepts no publishing through this API at all.
  • publish.image: false — it publishes, but not images.

A client that collapses those two shows the wrong error in the second case. The same distinction applies to video, document, carousel, inbox and ads.

What false promises

A capability set to false is a promise that the request will be rejected early — at validation, before anything is scheduled — rather than accepted and then failing at delivery.

This is worth stating because it was once not true. Slack declared image, video and document support while no media pipeline existed behind it: posts were accepted, queued, and died at delivery. That declaration is now false, so the rejection arrives at POST /v1/tools/validate/post and at post creation, with image_not_supported, video_not_supported or document_not_supported and param: "media".

If you want to know whether a platform will take your attachment, ask this endpoint or the validator — do not infer it from the fact that a post was accepted a year ago.

Analytics, and the companion field

analytics splits into post and account: the metric names available per published post, and per connected account. Either can be empty.

An empty post list does not mean the platform has no analytics:

export async function analyticsShapeFor(
  zhyper: ZhyperClient,
  platform: string,
): Promise<{ post: number; account: number; companion: string | null }> {
  const catalog = await platformCatalog(zhyper);
  const entry = catalog.find((row) => row.platform === platform);
  const analytics = entry?.capabilities.analytics;
  if (analytics === undefined || analytics === false) {
    return { post: 0, account: 0, companion: null };
  }

  // `companion` bir platformun metriklerinin GONDERI basina degil baska bir
  // granulariteden geldigini soyler. Bugun tek deger `location`: metrikler
  // isletme lokasyonu duzeyinde toplanir, tekil gonderi duzeyinde degil.
  // `post: []` + dolu bir `account` gorunce "analitigi yok" diye okuma.
  return {
    post: analytics.post.length,
    account: analytics.account.length,
    companion: analytics.companion?.granularity ?? null,
  };
}

When companion is present it names the granularity the metrics actually come from. Today the only value is location — Google Business aggregates at the business-location level, so there is no per-post number to give you, while account carries the full set of location metrics. Treating post: [] as "no analytics" would hide a surface that exists.

What this endpoint deliberately does not tell you

  • Per-account differences. Some platforms narrow a capability for a specific connected account — a Discord bot without an attachment permission, an X app without media scope. The catalogue describes the platform; accountCapability on the entry, and the account health surface, describe the account.
  • Your quota position. quotaModel says how the platform's quota is shaped (shared pool or per tenant), not how much of it you have left. That lives in /v1/usage.
  • Internal method names. The catalogue is a customer contract, so adapter internals are stripped from it on the way out even where the underlying declaration carries them.

Is a platform up right now?

GET /v1/platforms says what a platform can do. /v1/status says whether it is answering. It takes no key — it is the public status page — and returns one entry per platform in the live registry plus an overall roll-up.

Each entry carries status (operational, degraded, outage, unknown), since, and two fields describing how the circuit breaker behind that status is scoped:

  • scope: "platform" — one breaker shared by every tenant. That is the right unit for a shared API: when Instagram stops answering, it stops for everyone.
  • scope: "origin" — one breaker per instance host, because the server address comes from your connected account, so "the platform" is the wrong unit. Mastodon and Bluesky are the platforms scoped this way today — but read the field, do not memorise the list: each platform declares its own scoping, and this page is not where that declaration lives. One broken server used to hold every tenant's posts on that platform; it no longer does.
  • openOriginCircuits — how many of those per-instance breakers are open or half-open at checkedAt. It is 0 whenever scope is "platform".

openOriginCircuits never changes status. One Mastodon server going dark is not Mastodon going dark, and on a federated network the wider claim is never true — so the number is reported and the platform status stays where it is. Read it as a weather report, not as an alarm.

The count is a count and nothing else. Which server, whose account, and how many tenants sit behind it are not published here. Your own instance being circuit-broken shows up where you can act on it instead: the affected targets come back held on your posts.

unknown on an origin-scoped platform also covers "we could not count". If the breaker store cannot be read, the platform reports unknown rather than a reassuring 0.

On this page