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
| Route | Scope | Level required |
|---|---|---|
| GET /api/users/me | posts:read | — |
| GET /api/businesses | posts:read | — |
| GET /api/cities | posts:read | — |
| GET /api/posts/mine | posts:read | — |
| GET /api/feed?kind=visual|text | posts:read | 1 (verified email) |
| GET /api/posts/{id} | posts:read | 1 (verified email) |
| POST /api/posts | posts:write | 3 to publish on the public feed |
| POST /api/posts/visual | posts:write | 3 to publish on the public feed |
| POST /api/posts/youtube | posts:write | 3 to publish on the public feed |
| DELETE /api/posts/{id} | posts:write | author or admin |
| POST /api/notifications | notifications: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:
| Field | Meaning |
|---|---|
| content | up to 4000 characters |
| link_url | optional absolute http(s) URL, unfurled and shown as a card |
| language | fr | en | he — the feed's language filter |
| city | city slug — the feed's city filter |
| business_id | optional; must match the key's identity when it is pinned |
| conversation_id | optional; 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.
| Field | Meaning |
|---|---|
| title | required, up to 120 characters |
| body | up to 500 characters |
| source | who is speaking (60 characters). A key pinned to a business defaults to that business's name |
| level | info | warning | error — drives the icon, never the wording |
| url | optional http(s) address the notification opens |
| dedupe_key | collapses 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
| Limit | Value |
|---|---|
| Reads | ~60 per minute, burst 30 |
| Writes | ~20 per minute, burst 5 |
| Per IP address | ~300 per minute, burst 60 |
| Request size | 30 MB (App Engine caps at 32 MB) |
| Media per post | 10 |
| Live keys per account | 20 |
Errors
{ "error": "missing the posts:write scope" }| Status | What it means |
|---|---|
| 401 invalid API key | unknown, revoked or expired — the three are answered alike |
| 403 missing the posts:write scope | the key is read-only |
| 403 verification level 3 required | the account is not mezuzah-verified, which the public feed requires |
| 403 can only publish as the business account it is bound to | the request named an identity other than the key's |
| 402 | not enough מ to pay for the post |
| 429 | rate limited — back off and retry |
| 429 at most 60 notifications per day | the 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.