Teams

Invite people by email with a role and, if you want, only some brands; switch between workspaces.

View as MarkdownOpenAPIBase URL https://postwire.io

Workspaces. Your account is a workspace: its posts, brands, connected accounts, queue, approvals, activity log, API keys, X credits and the plan's limits. Invite people by email; each one signs in with their own PostWire account (emailed code, Google or Apple) and can belong to several workspaces. The dashboard has a workspace switcher; through the API, a dashboard session acts in another workspace with the header X-PostWire-Workspace: <workspace id>. An API key belongs to the workspace it was created in and can never switch (403 workspace_forbidden).

Roles. Owner: billing, members, deleting the workspace, everything. Admin: everything except billing and ownership. Editor: writes, schedules and publishes, within each brand's approval rules. Contributor: drafts and schedules, but every post waits for approval (202 pending_approval, whatever the brand's rules). Approver: only the approvals inbox — previews, approve, reject. Viewer: read-only. Editors, contributors, approvers and viewers can be limited to some brands: they see and act only on those (the others answer 404, as if they did not exist).

Every request is checked on the server. One choke point decides, for every route, which roles may call it: 403 role_forbidden (the role can't), brand_restricted (a screen that covers every brand), seat_read_only (past the plan's seats). An API key or a connected AI app (Claude, ChatGPT) acts with the CURRENT role of the member who created it: a Viewer's or an Approver's key can't publish, and a removed member's keys and apps stop at once. The activity log names the member behind every action.

Invitations. A signed link that works once, only for the invited address, for 7 days; it can be canceled or sent again (a new link). Inviting, changing roles, removing and transferring need a person in the dashboard: an API key or an AI connector, even an admin's, gets 403 team_requires_human.

Seats. Free 1 (the owner), Starter 2, Pro 5, Agency 25, Scale 100 — members plus open invitations. Client approvers who only approve from an emailed link are not seats. On a downgrade nobody is removed: the latest members past the new plan's seats become read-only until a seat is free.

The team of this workspace: members, roles, open invitations, seats

GET/api/team

Members in seat order (owner first, then by joining) with their role, brand list and read_only (past the plan's seats after a downgrade: they stay, read-only). Owners and admins also get the open invitations; contributors, approvers and viewers see only themselves. `seats` = { plan_seats, used (members + open invitations), free, next_plan }. Seats: Free 1, Starter 2, Pro 5, Agency 25, Scale 100.

Needs an API key: Authorization: Bearer pw_live_….

Responses

StatusMeans
200{ teams, workspace: { id, name, display_name, plan }, you: { role, effective_role, read_only, brand_ids, can_manage, can_edit }, seats, roles: [{ id, label, does, takes_brands }], members: [{ account_id, email, role, brand_ids, joined_at, read_only, you }], invites: [{ id, email, role, brand_ids, expires_at, expired, invited_by }], brands }
curl
curl "https://postwire.io/api/team" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
Node
const res = await fetch("https://postwire.io/api/team", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}` },
});
console.log(res.status, await res.json());
Python
import os, requests

r = requests.get(
    "https://postwire.io/api/team",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
)
print(r.status_code, r.json())

Example response

JSON
{
  "teams": true,
  "workspace": {
    "id": "9d3c…",
    "name": "Bakery Agency",
    "plan": "pro"
  },
  "you": {
    "role": "owner",
    "effective_role": "owner",
    "can_manage": true
  },
  "seats": {
    "plan_seats": 5,
    "used": 3,
    "free": 2
  },
  "members": [
    {
      "account_id": "9d3c…",
      "email": "ana@bakery.agency",
      "role": "owner"
    },
    {
      "account_id": "51be…",
      "email": "leo@bakery.agency",
      "role": "editor",
      "brand_ids": null
    }
  ],
  "invites": [
    {
      "id": "c0f2…",
      "email": "maria@client.com",
      "role": "approver",
      "brand_ids": [
        "b7e1…"
      ],
      "expires_at": "2026-10-12T09:00:00Z"
    }
  ]
}

Abridged: it also returns the roles with what each one does and the brands (for the brand picker).

Rename the workspace

PATCH/api/team

Owner or admin. Only a person signed in to the dashboard (its session key plus X-PostWire-Source: dashboard): an API key or an AI connector gets 403 team_requires_human, even an admin's.

Needs an API key: Authorization: Bearer pw_live_….

Body (JSON)

FieldTypeDescription
namestringUp to 80 characters.

Responses

StatusMeans
200{ ok, name }
403role_forbidden, team_requires_human
curl
curl -X PATCH "https://postwire.io/api/team" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Bakery Agency"
}'
Node
const res = await fetch("https://postwire.io/api/team", {
  method: "PATCH",
  headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    "name": "Bakery Agency"
  }),
});
console.log(res.status, await res.json());
Python
import os, requests

r = requests.patch(
    "https://postwire.io/api/team",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
    json={
        "name": "Bakery Agency",
    },
)
print(r.status_code, r.json())

Invite someone by email

POST/api/team/invites

Owner or admin. Only a person signed in to the dashboard (its session key plus X-PostWire-Source: dashboard): an API key or an AI connector gets 403 team_requires_human, even an admin's. The invitation is a signed link that works once, only for that address, for 7 days; the person signs in or up with an emailed code, Google or Apple and joins. The response also carries invite_url, to share yourself. Seats count members plus open invitations: past the plan's seats the answer is 403 seat_limit with the next plan's checkout link. At most 30 invitations a day per workspace. Inviting an address again replaces its open invitation.

Needs an API key: Authorization: Bearer pw_live_….

Body (JSON)

FieldTypeDescription
email requiredstring (email)
role requiredstringadmin: everything except billing and ownership. editor: write, schedule, publish within approval rules. contributor: drafts and schedules, every post waits for approval. approver: only the approvals inbox. viewer: read-only. One of: admin, editor, contributor, approver, viewer.
brand_idsarray of string (uuid)Only these brands of the workspace (editor, contributor, approver, viewer). Empty or omitted = every brand.

Responses

StatusMeans
200{ ok, invite: { id, email, role, brand_ids, expires_at }, invite_url, email: { sent, note? }, message }
400bad_email, bad_role, bad_brands
403seat_limit (with upgrade), role_forbidden, team_requires_human
409already_member
503teams_unavailable
curl
curl -X POST "https://postwire.io/api/team/invites" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "maria@client.com",
  "role": "approver",
  "brand_ids": [
    "b7e1c4a0-5f7d-4a52-9e0a-2f1e0c9d8a11"
  ]
}'
Node
const res = await fetch("https://postwire.io/api/team/invites", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    "email": "maria@client.com",
    "role": "approver",
    "brand_ids": [
      "b7e1c4a0-5f7d-4a52-9e0a-2f1e0c9d8a11"
    ]
  }),
});
console.log(res.status, await res.json());
Python
import os, requests

r = requests.post(
    "https://postwire.io/api/team/invites",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
    json={
        "email": "maria@client.com",
        "role": "approver",
        "brand_ids": ["b7e1c4a0-5f7d-4a52-9e0a-2f1e0c9d8a11"],
    },
)
print(r.status_code, r.json())

Example response

JSON
{
  "ok": true,
  "invite": {
    "id": "c0f2…",
    "email": "maria@client.com",
    "role": "approver",
    "expires_at": "2026-10-12T09:00:00Z"
  },
  "invite_url": "https://postwire.io/join/#t=inv1.eyJ3…",
  "email": {
    "sent": true
  },
  "message": "Invitation sent to maria@client.com."
}

Needs the dashboard's own session (a person): an API key gets 403 team_requires_human.

Send an invitation again (a new link; the old one stops working)

POST/api/team/invites/{id}/resend

Owner or admin. Only a person signed in to the dashboard (its session key plus X-PostWire-Source: dashboard): an API key or an AI connector gets 403 team_requires_human, even an admin's. Once an hour per invitation.

Needs an API key: Authorization: Bearer pw_live_….

Parameters

FieldTypeDescription
id requiredpath, string (uuid)

Responses

StatusMeans
200{ ok, invite, invite_url, email: { sent }, expires_at }
404not_found
curl
curl -X POST "https://postwire.io/api/team/invites/c0f2a9e4-1b7d-4f0e-8a51-3c6d2e9b7f10/resend" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
Node
const res = await fetch("https://postwire.io/api/team/invites/c0f2a9e4-1b7d-4f0e-8a51-3c6d2e9b7f10/resend", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}` },
});
console.log(res.status, await res.json());
Python
import os, requests

r = requests.post(
    "https://postwire.io/api/team/invites/c0f2a9e4-1b7d-4f0e-8a51-3c6d2e9b7f10/resend",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
)
print(r.status_code, r.json())

Cancel an invitation

DELETE/api/team/invites/{id}

Owner or admin. Only a person signed in to the dashboard (its session key plus X-PostWire-Source: dashboard): an API key or an AI connector gets 403 team_requires_human, even an admin's. Its link stops working and the seat is free again.

Needs an API key: Authorization: Bearer pw_live_….

Parameters

FieldTypeDescription
id requiredpath, string (uuid)

Responses

StatusMeans
200{ ok }
404not_found
curl
curl -X DELETE "https://postwire.io/api/team/invites/c0f2a9e4-1b7d-4f0e-8a51-3c6d2e9b7f10" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
Node
const res = await fetch("https://postwire.io/api/team/invites/c0f2a9e4-1b7d-4f0e-8a51-3c6d2e9b7f10", {
  method: "DELETE",
  headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}` },
});
console.log(res.status, await res.json());
Python
import os, requests

r = requests.delete(
    "https://postwire.io/api/team/invites/c0f2a9e4-1b7d-4f0e-8a51-3c6d2e9b7f10",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
)
print(r.status_code, r.json())

Change a member's role or brand list

PATCH/api/team/members/{id}

Owner or admin; never the owner (ownership moves with POST /api/team/transfer). Only a person signed in to the dashboard (its session key plus X-PostWire-Source: dashboard): an API key or an AI connector gets 403 team_requires_human, even an admin's. brand_ids applies to editor, contributor, approver and viewer; [] = every brand.

Needs an API key: Authorization: Bearer pw_live_….

Parameters

FieldTypeDescription
id requiredpath, string (uuid)The member's account_id

Body (JSON)

FieldTypeDescription
rolestringadmin: everything except billing and ownership. editor: write, schedule, publish within approval rules. contributor: drafts and schedules, every post waits for approval. approver: only the approvals inbox. viewer: read-only. One of: admin, editor, contributor, approver, viewer.
brand_idsarray of string (uuid)Only these brands of the workspace (editor, contributor, approver, viewer). Empty or omitted = every brand.

Responses

StatusMeans
200{ ok, member: { account_id, role, brand_ids } }
409owner
curl
curl -X PATCH "https://postwire.io/api/team/members/51be0c3d-9a4f-4e21-b6c8-7d2f1a0e9c34" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "role": "contributor",
  "brand_ids": []
}'
Node
const res = await fetch("https://postwire.io/api/team/members/51be0c3d-9a4f-4e21-b6c8-7d2f1a0e9c34", {
  method: "PATCH",
  headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    "role": "contributor",
    "brand_ids": []
  }),
});
console.log(res.status, await res.json());
Python
import os, requests

r = requests.patch(
    "https://postwire.io/api/team/members/51be0c3d-9a4f-4e21-b6c8-7d2f1a0e9c34",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
    json={
        "role": "contributor",
        "brand_ids": [],
    },
)
print(r.status_code, r.json())

Remove a member

DELETE/api/team/members/{id}

Owner or admin. Only a person signed in to the dashboard (its session key plus X-PostWire-Source: dashboard): an API key or an AI connector gets 403 team_requires_human, even an admin's. The API keys and AI apps the member created in this workspace are revoked at once, and they get an email. Never the owner, and never the person who created the workspace (their sign-in is the workspace's account: they can be given any role instead).

Needs an API key: Authorization: Bearer pw_live_….

Parameters

FieldTypeDescription
id requiredpath, string (uuid)The member's account_id

Responses

StatusMeans
200{ ok, removed, keys_revoked, apps_disconnected, email: { sent } }
409owner, founder
curl
curl -X DELETE "https://postwire.io/api/team/members/51be0c3d-9a4f-4e21-b6c8-7d2f1a0e9c34" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
Node
const res = await fetch("https://postwire.io/api/team/members/51be0c3d-9a4f-4e21-b6c8-7d2f1a0e9c34", {
  method: "DELETE",
  headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}` },
});
console.log(res.status, await res.json());
Python
import os, requests

r = requests.delete(
    "https://postwire.io/api/team/members/51be0c3d-9a4f-4e21-b6c8-7d2f1a0e9c34",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
)
print(r.status_code, r.json())

Example response

JSON
{
  "ok": true,
  "removed": "51be…",
  "keys_revoked": 1,
  "apps_disconnected": 0,
  "email": {
    "sent": true
  }
}

Leave this workspace

POST/api/team/leave

Any member but the owner (transfer first) and the founder. Only a person signed in to the dashboard (its session key plus X-PostWire-Source: dashboard): an API key or an AI connector gets 403 team_requires_human, even an admin's. Your own PostWire account is not touched.

Needs an API key: Authorization: Bearer pw_live_….

Responses

StatusMeans
200{ ok }
409owner, founder
curl
curl -X POST "https://postwire.io/api/team/leave" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
Node
const res = await fetch("https://postwire.io/api/team/leave", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}` },
});
console.log(res.status, await res.json());
Python
import os, requests

r = requests.post(
    "https://postwire.io/api/team/leave",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
)
print(r.status_code, r.json())

Transfer ownership to a member

POST/api/team/transfer

Owner only. Only a person signed in to the dashboard (its session key plus X-PostWire-Source: dashboard): an API key or an AI connector gets 403 team_requires_human, even an admin's. The member becomes the owner (billing, members, deleting the workspace); you become an admin. Type their email in confirm_email.

Needs an API key: Authorization: Bearer pw_live_….

Body (JSON)

FieldTypeDescription
account_id requiredstring (uuid)
confirm_email requiredstring (email)

Responses

StatusMeans
200{ ok, owner, you: { role: admin } }
400confirmation_required
403role_forbidden
curl
curl -X POST "https://postwire.io/api/team/transfer" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "account_id": "51be0c3d-9a4f-4e21-b6c8-7d2f1a0e9c34",
  "confirm_email": "leo@bakery.agency"
}'
Node
const res = await fetch("https://postwire.io/api/team/transfer", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    "account_id": "51be0c3d-9a4f-4e21-b6c8-7d2f1a0e9c34",
    "confirm_email": "leo@bakery.agency"
  }),
});
console.log(res.status, await res.json());
Python
import os, requests

r = requests.post(
    "https://postwire.io/api/team/transfer",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
    json={
        "account_id": "51be0c3d-9a4f-4e21-b6c8-7d2f1a0e9c34",
        "confirm_email": "leo@bakery.agency",
    },
)
print(r.status_code, r.json())

The workspaces you belong to (the switcher)

GET/api/workspaces

Your own workspace first, then every team you joined, each with your role there. To act in one, the dashboard sends its id in the X-PostWire-Workspace header with its session key. An API key belongs to the workspace it was created in and never switches (403 workspace_forbidden).

Needs an API key: Authorization: Bearer pw_live_….

Responses

StatusMeans
200{ teams, workspaces: [{ id, name, display_name, role, plan, own, members, read_only }] }
curl
curl "https://postwire.io/api/workspaces" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
Node
const res = await fetch("https://postwire.io/api/workspaces", {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}` },
});
console.log(res.status, await res.json());
Python
import os, requests

r = requests.get(
    "https://postwire.io/api/workspaces",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
)
print(r.status_code, r.json())

Example response

JSON
{
  "teams": true,
  "workspaces": [
    {
      "id": "51be…",
      "display_name": "Your workspace",
      "role": "owner",
      "plan": "free",
      "own": true
    },
    {
      "id": "9d3c…",
      "display_name": "Bakery Agency",
      "role": "editor",
      "plan": "pro",
      "own": false
    }
  ]
}

What an invitation is (the /join/ page)

POST/api/invites/view

No API key needed: the signed link is the credential. Never returns the link; the invited address is masked.

No API key needed.

Body (JSON)

FieldTypeDescription
t requiredstring

Responses

StatusMeans
200{ state: open|used|revoked|expired, workspace, invited_by, role, role_label, does, brands, email (masked), expires_at, you? }
400invite_invalid
410invite_expired
curl
curl -X POST "https://postwire.io/api/invites/view" \
  -H "Content-Type: application/json" \
  -d '{
  "t": "inv1.eyJ3…"
}'
Node
const res = await fetch("https://postwire.io/api/invites/view", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    "t": "inv1.eyJ3…"
  }),
});
console.log(res.status, await res.json());
Python
import os, requests

r = requests.post(
    "https://postwire.io/api/invites/view",
    json={
        "t": "inv1.eyJ3…",
    },
)
print(r.status_code, r.json())

Example response

JSON
{
  "state": "open",
  "workspace": "Bakery Agency",
  "invited_by": "ana@bakery.agency",
  "role": "approver",
  "role_label": "Approver",
  "brands": [
    "Rye & Co"
  ],
  "email": "m***@client.com"
}

Email a 6-digit code to the invited address

POST/api/invites/code

No API key needed. The code goes to the address the invitation was sent to, never another: a forwarded link is useless without that mailbox. 4 codes per 15 minutes per invitation.

No API key needed.

Body (JSON)

FieldTypeDescription
t requiredstring

Responses

StatusMeans
200{ ok, challenge, sent_to }
curl
curl -X POST "https://postwire.io/api/invites/code" \
  -H "Content-Type: application/json" \
  -d '{
  "t": "inv1.eyJ3…"
}'
Node
const res = await fetch("https://postwire.io/api/invites/code", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    "t": "inv1.eyJ3…"
  }),
});
console.log(res.status, await res.json());
Python
import os, requests

r = requests.post(
    "https://postwire.io/api/invites/code",
    json={
        "t": "inv1.eyJ3…",
    },
)
print(r.status_code, r.json())

Example response

JSON
{
  "ok": true,
  "challenge": "…",
  "sent_to": "m***@client.com"
}

Join with the emailed code

POST/api/invites/verify

No API key needed. Signs the person in (a new address gets a free account: the code proved the mailbox), joins the workspace and returns a dashboard session key.

No API key needed.

Body (JSON)

FieldTypeDescription
t requiredstring
challenge requiredstring
code requiredstring

Responses

StatusMeans
200{ ok, api_key, workspace_id, role, created }
400wrong_code, expired, used
403seat_limit
409invite_used
curl
curl -X POST "https://postwire.io/api/invites/verify" \
  -H "Content-Type: application/json" \
  -d '{
  "t": "inv1.eyJ3…",
  "challenge": "…",
  "code": "482913"
}'
Node
const res = await fetch("https://postwire.io/api/invites/verify", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    "t": "inv1.eyJ3…",
    "challenge": "…",
    "code": "482913"
  }),
});
console.log(res.status, await res.json());
Python
import os, requests

r = requests.post(
    "https://postwire.io/api/invites/verify",
    json={
        "t": "inv1.eyJ3…",
        "challenge": "…",
        "code": "482913",
    },
)
print(r.status_code, r.json())

Example response

JSON
{
  "ok": true,
  "api_key": "pw_live_…",
  "workspace_id": "9d3c…",
  "role": "approver",
  "created": true
}

Join with the session you are signed in with

POST/api/invites/accept

With a dashboard session (not an API key) of the account whose address was invited. Once per link.

Needs an API key: Authorization: Bearer pw_live_….

Body (JSON)

FieldTypeDescription
t requiredstring

Responses

StatusMeans
200{ ok, workspace_id, role, already }
403invite_email_mismatch, needs_session, seat_limit
409invite_used
410invite_expired, invite_revoked
curl
curl -X POST "https://postwire.io/api/invites/accept" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "t": "inv1.eyJ3…"
}'
Node
const res = await fetch("https://postwire.io/api/invites/accept", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    "t": "inv1.eyJ3…"
  }),
});
console.log(res.status, await res.json());
Python
import os, requests

r = requests.post(
    "https://postwire.io/api/invites/accept",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
    json={
        "t": "inv1.eyJ3…",
    },
)
print(r.status_code, r.json())

Example response

JSON
{
  "ok": true,
  "workspace_id": "9d3c…",
  "role": "approver"
}