Team and plan
The people in the workspace, what each of them may do, and which plan the workspace is on. The same things the Team and Plan screens show, for a workspace set up or checked by machine.
| Method | Path | Permission |
|---|---|---|
GET | /v1/team/users | team:read |
POST | /v1/team/users | team:write |
PATCH | /v1/team/users/{userId} | team:write |
POST | /v1/team/users/{userId}/resend | team:write |
POST | /v1/team/users/{userId}/delete | team:write |
GET | /v1/licence | plan:read |
POST | /v1/licence/offer/{offerId}/accept | signed-in person: the owner |
POST | /v1/licence/offer/billing-details | signed-in person: the owner |
POST | /v1/team/name | signed-in person: the owner |
POST | /v1/team/leave | signed-in person: a colleague |
GET | /v1/team/markets | signed-in person |
POST | /v1/team/markets | signed-in person: the owner |
The Team permission is offered only on a plan with a team (Small business and Enterprise). Renaming the workspace, leaving it, choosing its industries and changing the plan belong to a signed-in person, and no key can reach them: see Only for a signed-in person.
A key holding team:write is a way into the workspace, not only a way to read it: it can invite a
colleague with the Administration power, who manages API keys. See
Authentication. Give it only to a key whose job is setting
workspaces up. An AI assistant's connection holds it only where the person connecting it left that switch on,
because one instruction slipped into something the assistant reads could invite somebody whose access
outlives the connection.
The people
curl http://localhost:4000/v1/team/users \
-H "Authorization: Bearer rw_your_key_here"
{
"workspace": { "name": "Northwind Markets" },
"users": [
{
"id": "01M38Z6Y78B0DG1817DA49VCMJ",
"email": "sam.carter@northwind.example",
"displayName": "Sam Carter",
"role": "OWNER",
"powers": [],
"lastLoginAt": "2026-09-24T20:26:39.741Z",
"createdAt": "2026-09-24T05:46:29.539Z"
},
{
"id": "01M390CPHEWQ9GWKYWBPTM5YTC",
"email": "priya.nair@northwind.example",
"displayName": "Priya Nair",
"role": "MEMBER",
"powers": ["operate"],
"lastLoginAt": null,
"createdAt": "2026-09-24T06:07:06.889Z"
}
],
"seats": { "used": 3, "max": 10, "included": 10, "additional": 0, "granted": 0 }
}
role is OWNER for the person the workspace belongs to, who holds every power (so their powers
list is empty), and MEMBER for everybody else. powers is what a colleague may do:
| Power | On the Team screen | What it opens |
|---|---|---|
operate | Operations | Channels, brands, content sources and the pipeline: the day to day. |
approve | Approvals | Approve and Reject on posts, and updates of posts, awaiting approval, and no editing. It is also what switches a channel's approval off, and on Enterprise it reads the compliance record and sets its approval rules. |
admin | Administration | The Team, API keys and Webhook screens: the people, the credentials that reach the outside (API keys, a channel's platform connector), showing a key or the webhook's signing secret again behind their own password, and the webhook's address, its test delivery and sending a delivery again. On Enterprise it reads the compliance record too, and adds and removes its auditors. |
No power reaches round another. An administrator makes API keys, but can give a key approvals:write
only while holding Approvals, and plan:read only as the owner, since the plan carries the prices
and the next invoice; asking otherwise is a 403 (see Authentication).
Which emails each power brings is on the Team screen's page,
Who gets which email: nothing about what the
workspace pays reaches anybody but the owner.
An auditor (Enterprise) is not a colleague: auditors are not in this list, take no seat, and are added and removed under Compliance; the routes here answer one with a 409.
seats counts colleagues, never the owner: used of max, where max is the sum of what the plan
includes (included, or a number agreed with Reelwire), seats bought as an extra (additional) and
seats Reelwire granted (granted). lastLoginAt is null for somebody who has not signed in yet.
Inviting a colleague
curl -X POST http://localhost:4000/v1/team/users \
-H "Authorization: Bearer rw_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "email": "lea.martin@northwind.example", "displayName": "Lea Martin", "powers": ["operate"] }'
| Field | For |
|---|---|
email | Their address, up to 320 characters. It signs them in, and it has to be unused anywhere on Reelwire. |
displayName | Optional, up to 120 characters. They can change it themselves on their profile. |
powers | At least one of operate, approve and admin: with none they would sign in to an empty dashboard, so that is a 400. |
It answers 201 with the new colleague (user, as the list shows them), password, a starting
password made for them, and minutes, which is null because the setup link does not expire. The
account is ready at once: an email goes to the address with a link to choose their own password,
which is how they are meant to get in. Sending a newer link stops every earlier one working. The
starting password is in this answer and nowhere else: it is kept only as a hash, so it cannot be read
again.
A 409 says why it could not be done: the plan has no team (upgrade it), every seat is taken (remove somebody, or buy seats on the Plan screen), or the address cannot be used here, which is all it says about an address that belongs to somebody else.
Changing, resending and removing
PATCH /v1/team/users/{userId} takes powers (the whole set they end with, at least one: powers are
replaced, not merged) and displayName (an empty string takes the name away), and answers with the
changed user. Nobody can change the owner's powers or their own; either is a 409.
POST /v1/team/users/{userId}/resend sends a fresh setup link to somebody who lost theirs, and answers
{ "sent": true, "email": "...", "minutes": null }. Not to the owner, who resets their own password,
and not twice within a minute to the same person: both are a 409.
POST /v1/team/users/{userId}/delete removes somebody at once and answers { "removed": true }: they are
signed out everywhere, their account is deleted, and an email tells them so. What they did stays in
the audit trail and on the posts they approved. The owner cannot be removed, and nobody can remove
themselves.
The plan
curl http://localhost:4000/v1/licence \
-H "Authorization: Bearer rw_your_key_here"
{
"licence": {
"plan": "ENTERPRISE",
"state": "ACTIVE",
"term": "MONTHLY",
"capabilities": ["customApi", "team", "approvals", "compliance", "syndicate", "priorityRendering", "staffSetup"],
"watermark": false,
"paysBy": "CARD",
"invoiceDays": 30,
"offer": null,
"colleagues": 3,
"pending": null,
"costs": {
"renewsAt": "2026-10-24T05:46:29.135Z",
"lines": [
{ "key": "plan", "label": "Reelwire Enterprise subscription (4000 videos a month, 25 seats, 300 syndication participants, 80000 MB storage)", "cents": 120000 }
],
"totalCents": 120000,
"currency": "EUR"
}
},
"plans": [
{ "id": "FREE", "name": "Free", "priceCents": 0, "renders": 15, "seats": 0, "syndicationPartners": 0, "storageMb": 50, "watermark": false, "capabilities": ["customApi"] }
]
}
Shortened here: the answer carries more of costs and all four plans. It answers the owner, or a key
holding plan:read, which only the owner can give; any other colleague is refused with a 403,
because it carries the prices and the next invoice. capabilities is what this workspace's plan
switches on, and the same list decides what the dashboard offers and what every route allows:
customApi (your own custom feeds), team, approvals (a channel can hold posts for a reviewer:
Small business and Enterprise), compliance (the compliance record,
Enterprise), syndicate, priorityRendering and staffSetup (free setup by our staff: Small business and Enterprise). holds is what was bought on top of the plan, pending a change already chosen that
starts at the next renewal, such as a move to a smaller plan. plans lists every plan with its price
and allowances, cheapest first; syndicationPartners is its syndication participants allowance and
connections how many platform accounts it may connect through the posting service.
paysBy is how the workspace pays: CARD, or INVOICE, where Stripe emails every invoice, payable
within invoiceDays days, and nothing is taken from a card. Only an Enterprise offer
sets INVOICE; invoiceDays means nothing while paysBy is CARD. On Enterprise the plan's
allowances and the prices of its extras are the ones agreed in the offer the workspace accepted, and
the Enterprise entry of plans carries its agreed price and allowances.
Enterprise is agreed with Reelwire, not chosen. For a workspace that is not on it, its entry in
plans has contact: true, and its prices (priceCents, monthlyCents, yearlyCents) and
allowances (renders, seats, syndicationPartners, storageMb, connections) are null; every other entry has
contact: false. Asking for Enterprise when changing the plan, or at the checkout, is a 409 that
says to use Contact us on the Plan screen. offer is the offer Reelwire made for this workspace and
it has not accepted yet, or null.
How much of this month's allowance is left, and of seats, syndication participants and storage, is
GET /v1/renders/quota (allowance:read); what the extras cost is in that answer only for the owner
or a plan:read key. See Renders.
Changing the plan, buying extras and the invoices are the owner's, on the Plan, Allowance and Billing screens: they change what the workspace pays, and no key can reach them.
An Enterprise offer
What Reelwire agrees with a customer for Enterprise is written as an offer for its workspace, which
the owner accepts on the Plan screen. While one stands,
GET /v1/licence carries it as offer:
{
"licence": {
"plan": "FREE",
"paysBy": "CARD",
"invoiceDays": 30,
"offer": {
"id": "eo_01M3C8Q0Y7ZK4D2N6R9T1V3W5X",
"term": "YEARLY",
"monthlyCents": 69000,
"priceCents": 690000,
"yearlyCents": 690000,
"currency": "EUR",
"renders": 3000,
"seats": 25,
"partners": 250,
"storageMb": 5000,
"connections": 9,
"extras": [
{ "stepId": "renders-250", "dimension": "renders", "amount": 250, "listCents": 12900, "cents": 9900 },
{ "stepId": "seats-1", "dimension": "seats", "amount": 1, "listCents": 3900, "cents": 3900 },
{ "stepId": "storage-100", "dimension": "storage", "amount": 100, "listCents": 199, "cents": 199 }
],
"paysBy": "INVOICE",
"invoiceDays": 45,
"expiresAt": "2026-11-15T09:12:44.000Z",
"road": "new-invoice",
"billing": null,
"needsBilling": true
}
}
}
Shortened: the rest of licence and plans as above.
| Field | What it is |
|---|---|
id | The offer, for accepting it. |
term | MONTHLY or YEARLY: the billing term the plan starts on. |
monthlyCents | The agreed price for a month, before tax. |
priceCents | What one period of term costs, before tax. |
yearlyCents | What a year costs, before tax: ten months, as on every plan. |
renders, seats, partners, storageMb, connections | What the plan includes for this workspace: videos a month, seats besides the owner, syndication participants, storage in MB, and platform accounts connected through the posting service. |
extras | Every extra the workspace may add itself, one step each, with its list price (listCents) and the price agreed (cents), both monthly. Syndication participants are never among them: more of them are a new offer. |
paysBy, invoiceDays | CARD, or INVOICE, payable within invoiceDays days. |
expiresAt | When the offer stops standing. A new offer replaces the one before. |
road | What accepting does here: checkout (Stripe's checkout, by card), amend-card (the paid plan moves up, charged to the card on file), new-invoice (a subscription invoiced from the start) or amend-invoice (the paid plan moves up and onto invoices). |
billing | For an offer paid by invoice, the billing details Stripe has: company, forename, surname, street, postalCode, city, province, country and vatNumber. null where none are on file. |
needsBilling | true while an offer paid by invoice cannot be accepted, because the billing details its invoices are made out to are missing. |
POST /v1/licence/offer/{offerId}/accept, with no body, accepts it, by its road, and answers one of:
{ "licence": { ... } }, the plan as it now is, where the move was made at once: by card on a paid plan, or by invoice. The first invoice of a new invoiced subscription arrives by email within the hour.{ "checkout": { "url": "...", "sessionId": "..." } }, Stripe's checkout to open, where the first period is paid by card. Nothing changes until the payment goes through.202with{ "confirmation": { "url": "...", "label": "...", "expiresAt": "..." } }, where the bank asks the cardholder to confirm the payment, as for any move up: nothing changes until it is confirmed on Stripe's page aturl.
It is a 404 for an offer this workspace was not made, and a 409 that says why for one that no
longer stands (accepted, withdrawn, or past expiresAt), for an offer paid by invoice with no billing
details yet, and for a yearly offer while something else is waiting for the charge date. It is a 403
while the owner's email address is not confirmed: nothing is paid from an address nobody has confirmed,
here or at the checkout, so follow the link in the confirmation email first.
POST /v1/licence/offer/billing-details answers { "url": "..." }: Stripe's billing page, where the
owner enters the details an offer paid by invoice is made out to. Nothing is charged or subscribed
there. It is a 409 while no offer stands.
Both answer the owner, signed in, and nobody else: a key is refused whatever its permissions.
Only for a signed-in person
These answer a dashboard session and never a key, because a key holds no role and is nobody's membership:
POST /v1/team/namewith{ "name": "Northwind Markets" }(up to 120 characters) renames the workspace and answers{ "workspace": { "name": "..." } }. The owner's alone, since every colleague sees the name; anybody else is refused with a 403.POST /v1/team/leavetakes a colleague out of the workspace and into a new one of their own on Free, which they own, and answers it as{ "workspace": { "id": "...", "name": "...", "plan": "FREE" } }; nothing of the old workspace goes with them. The owner cannot leave (a 409).GET /v1/team/marketsanswers every industry (markets[]:id,name,description,chosen), when the workspace chose (chosenAt) and whether this person may change it (canChange).POST /v1/team/marketswith{ "markets": ["brokers", "tech"] }(ids from that list:brokersis Finance) replaces the choice and answers the ids it kept andchosenAt. It is the owner's alone (anybody else gets a 403), an id it does not know is dropped, and at least one has to remain: none is a 400. The industries decide which feeds and templates the workspace is offered.