MishkanMishkan

API

Publish on Mishkan from outside the app.

Updated 2026-08-23

A Mishkan API key lets a studio, a script or a server read the feed and publish on it in your name. It is a second door into your own identity — not a service account: a key obeys every rule you obey, from your verification level to the Mem a post costs and the people you have blocked.

Everything below is served from https://api.mishkan.tech. Responses are JSON; errors carry an HTTP status and a single "error" string.

Getting a key

Keys are minted inside the app: Profile → API keys → +. Creating one requires level 4 (formal verification), the same bar as a business sub-account — a key acts in your name outside the app, so we hand one out only to an account an admin has identified. Listing and revoking work at any level.

  • Name — for your own bookkeeping («Studio Midbar»).
  • Publish as — your personal account, or one of your business sub-accounts. A key pinned to a business can publish ONLY as that business; naming another identity in a request is refused.
  • Allow publishing — off, the key can only read.

The secret (msk_live_…) is shown once. The server keeps only its SHA-256, so it cannot be recovered — a lost key is revoked and replaced. You may hold up to 20 live keys, each with an optional expiry of up to ten years.

Authentication

Send the key as a bearer token. The X-API-Key header is accepted too, for clients that reserve Authorization for themselves.

curl https://api.mishkan.tech/api/users/me \
  -H "Authorization: Bearer $MISHKAN_KEY"

An unknown, revoked or expired key all answer the same 401 — deliberately, so probing tells an attacker nothing.

Scopes

A key carries posts:read, posts:write, notifications:write, or any combination. Everything else — messages, Mem, verification, administration, and key management itself — stays reserved to a signed-in session. A leaked key can neither mint another one, nor read your conversations, nor spend your balance.

  • posts:read — read the feed, a post, your own posts, your businesses.
  • posts:write — publish and delete.
  • notifications:write — notify the key's owner, and only them. It is never granted by default: ask for it when you create the key, or turn it on later from the key's row in the app.

Endpoints

RouteScopeLevel required
GET /api/users/meposts:read
GET /api/businessesposts:read
GET /api/citiesposts:read
GET /api/posts/mineposts:read
GET /api/feed?kind=visual|textposts:read1 (verified email)
GET /api/posts/{id}posts:read1 (verified email)
POST /api/postsposts:write3 to publish on the public feed
POST /api/posts/visualposts:write3 to publish on the public feed
POST /api/posts/youtubeposts:write3 to publish on the public feed
DELETE /api/posts/{id}posts:writeauthor or admin
POST /api/notificationsnotifications:write— (notifies its own owner)

Publishing into a group's own feed (conversation_id) replaces the level gate with membership and the group's posting policy, and pays the group's own price to its owner.

curl "https://api.mishkan.tech/api/feed?kind=text&limit=20" \
  -H "Authorization: Bearer $MISHKAN_KEY"

Publishing

A post on the public feed costs 2 מ and requires mezuzah verification (level 3). The charge is refunded if the post fails to save.

POST /api/posts publishes text and links, as JSON:

FieldMeaning
contentup to 4000 characters
link_urloptional absolute http(s) URL, unfurled and shown as a card
languagefr | en | he — the feed's language filter
citycity slug — the feed's city filter
business_idoptional; must match the key's identity when it is pinned
conversation_idoptional; publishes into that group's feed instead of the public one
curl -X POST https://api.mishkan.tech/api/posts \
  -H "Authorization: Bearer $MISHKAN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content":"Chag sameach","language":"en"}'

POST /api/posts/visual publishes a reel or a carousel as multipart/form-data: 1 to 10 media files, in form order, up to 30 MB for the whole request. The caption goes in content (1000 characters), and the same optional fields apply.

curl -X POST https://api.mishkan.tech/api/posts/visual \
  -H "Authorization: Bearer $MISHKAN_KEY" \
  -F "content=Kabbalat Shabbat, 18:30" \
  -F "media=@reel.mp4;type=video/mp4"

POST /api/posts/youtube publishes a reel backed by a YouTube video. url is the video; link_url is a separate “learn more” button, so fill both when you want the two.

curl -X POST https://api.mishkan.tech/api/posts/youtube \
  -H "Authorization: Bearer $MISHKAN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://youtu.be/abc123","link_url":"https://example.org"}'

Notifications

An integration that stops in the night — out of credit, expired token, quota reached — usually tells nobody: the error lands in a scheduler's logs, and you find out three days later, wondering why nothing was published. With notifications:write, a key writes into its owner's notification centre and the push goes out like any other.

A key notifies its OWNER, and nobody else. The recipient is not a field of the request — it IS the key's identity, so there is nothing to falsify to reach someone else. This channel cannot become a megaphone.

FieldMeaning
titlerequired, up to 120 characters
bodyup to 500 characters
sourcewho is speaking (60 characters). A key pinned to a business defaults to that business's name
levelinfo | warning | error — drives the icon, never the wording
urloptional http(s) address the notification opens
dedupe_keycollapses repeats of the same alert for 6 h
curl -X POST https://api.mishkan.tech/api/notifications \
  -H "Authorization: Bearer $MISHKAN_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Autopilot stopped",
    "body": "Anthropic credit exhausted — nothing published since 03:12.",
    "source": "TorahWithLLM",
    "level": "error",
    "url": "https://console.anthropic.com/settings/billing",
    "dedupe_key": "anthropic-credit"
  }'

202 on delivery. A deduped repeat answers 200 with deduped: true and the id of the notification already sent — the request succeeded and nothing was sent:

{ "delivered": true, "remaining_today": 59 }
{ "id": "…", "delivered": false, "deduped": true, "remaining_today": 59 }

dedupe_key is what makes an alert loop bearable: a pilot retrying every ten minutes with no credit would otherwise send the same line 144 times a day. Under it sits a hard floor of 60 notifications per account per 24 h, after which the route answers 429; remaining_today comes back on every call so you can back off first.

Title and body are shown exactly as sent, in every language: the server does not know what your integration meant, and guessing it in three languages would be inventing. A key pinned to a business notifies under that identity — the alert shows up in the app under that account, not in the personal centre — and dedupe_key is scoped to it too, so two businesses using the same key describe two different failures.

Limits

LimitValue
Reads~60 per minute, burst 30
Writes~20 per minute, burst 5
Per IP address~300 per minute, burst 60
Request size30 MB (App Engine caps at 32 MB)
Media per post10
Live keys per account20

Errors

{ "error": "missing the posts:write scope" }
StatusWhat it means
401 invalid API keyunknown, revoked or expired — the three are answered alike
403 missing the posts:write scopethe key is read-only
403 verification level 3 requiredthe account is not mezuzah-verified, which the public feed requires
403 can only publish as the business account it is bound tothe request named an identity other than the key's
402not enough מ to pay for the post
429rate limited — back off and retry
429 at most 60 notifications per daythe daily notification floor — use dedupe_key

Revoking

Swipe the key (iOS) or tap the bin (Android), or call DELETE /api/api-keys/{id} with a session token. The effect is immediate. Posts already published stay online: revoking a credential is not withdrawing what was published with it.