Skip to main content

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.

MethodPathPermission
GET/v1/team/usersteam:read
POST/v1/team/usersteam:write
PATCH/v1/team/users/{userId}team:write
POST/v1/team/users/{userId}/resendteam:write
POST/v1/team/users/{userId}/deleteteam:write
GET/v1/licenceplan:read
POST/v1/licence/offer/{offerId}/acceptsigned-in person: the owner
POST/v1/licence/offer/billing-detailssigned-in person: the owner
POST/v1/team/namesigned-in person: the owner
POST/v1/team/leavesigned-in person: a colleague
GET/v1/team/marketssigned-in person
POST/v1/team/marketssigned-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:

PowerOn the Team screenWhat it opens
operateOperationsChannels, brands, content sources and the pipeline: the day to day.
approveApprovalsApprove 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.
adminAdministrationThe 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"] }'
FieldFor
emailTheir address, up to 320 characters. It signs them in, and it has to be unused anywhere on Reelwire.
displayNameOptional, up to 120 characters. They can change it themselves on their profile.
powersAt 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.

FieldWhat it is
idThe offer, for accepting it.
termMONTHLY or YEARLY: the billing term the plan starts on.
monthlyCentsThe agreed price for a month, before tax.
priceCentsWhat one period of term costs, before tax.
yearlyCentsWhat a year costs, before tax: ten months, as on every plan.
renders, seats, partners, storageMb, connectionsWhat 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.
extrasEvery 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, invoiceDaysCARD, or INVOICE, payable within invoiceDays days.
expiresAtWhen the offer stops standing. A new offer replaces the one before.
roadWhat 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).
billingFor 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.
needsBillingtrue 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.
  • 202 with { "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 at url.

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/name with { "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/leave takes 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/markets answers every industry (markets[]: id, name, description, chosen), when the workspace chose (chosenAt) and whether this person may change it (canChange). POST /v1/team/markets with { "markets": ["brokers", "tech"] } (ids from that list: brokers is Finance) replaces the choice and answers the ids it kept and chosenAt. 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.