Invite a colleague
By the end of this tutorial you will have a second person with access to one of
your projects, added by email address, with the role you chose — and you will
have watched them go from pending to active the first time they sign in.
Members are free and unlimited on every plan, including Free. naturali is priced on runs, not seats.
Four steps:
- See who is in the project.
- Add your colleague.
- Watch them arrive.
- Change their role, or let them go.
Every step is one API call, shown for all three clients. The ids in the responses are examples — copy the ones your own calls return.
Prerequisites
-
A credential, and an account-wide one. A session JWT from Auth, or a
nat_sk_…API key that is not scoped to a single project, exported asNATURALI_TOKEN:export NATURALI_TOKEN=nat_sk_...export NATURALI_API=https://api.naturali.ai/v1 # curl examples onlyA project-scoped key cannot add members — step 2 answers
403 access_deniedwith one: the membership would outlive the key that wrote it, so a confined credential would be granting access past its own revocation. -
A project you are the
owneror anadminof, exported asPROJECT. Your first agent generation builds one, or use any project you already have:export PROJECT=proj_V1StGXR8Z5jdHi6B -
A colleague's email address. It does not need a naturali account — step 2 explains what happens either way. Use an address you control if you want to follow step 3 yourself.
1. See who is in the project
Start from what is there. A project you created has exactly one member: you, as
its owner.
- CLI
- SDK
- curl
naturali list-project-members --project-id "$PROJECT"
const { data: members } = await naturali.projects.listProjectMembers({
path: { project_id: process.env.PROJECT! },
});
curl "$NATURALI_API/projects/$PROJECT/members" \
-H "Authorization: Bearer $NATURALI_TOKEN"
{
"data": [
{
"id": "pmem_V1StGXR8Z5jdHi6B",
"project_id": "proj_V1StGXR8Z5jdHi6B",
"user_id": "user_V1StGXR8Z5jdHi6B",
"email": "you@acme.com",
"name": "You",
"status": "active",
"role": "owner",
"invited_by_user_id": null,
"created_at": "2026-07-17T00:00:00.000Z"
}
]
}
invited_by_user_id is null on that row: the founding owner was added by the act
of creating the project.
2. Add your colleague
Pick the role first.
member reads and writes everything in the project —
creating agents, minting project keys, connecting channels. admin also says
who else is here and how the project is configured. Neither can delete the
project, which stays with the owner.
- CLI
- SDK
- curl
naturali add-project-member \
--project-id "$PROJECT" \
--email ana@acme.com \
--role member
const { data: added } = await naturali.projects.addProjectMember({
path: { project_id: process.env.PROJECT! },
body: { email: 'ana@acme.com', role: 'member' },
});
curl -X POST "$NATURALI_API/projects/$PROJECT/members" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"email":"ana@acme.com","role":"member"}'
{
"id": "pmem_3ZjkQ2mN8pRtYv7X",
"project_id": "proj_V1StGXR8Z5jdHi6B",
"user_id": "user_3ZjkQ2mN8pRtYv7X",
"email": "ana@acme.com",
"name": null,
"status": "pending",
"role": "member",
"invited_by_user_id": "user_V1StGXR8Z5jdHi6B",
"created_at": "2026-07-17T00:05:00.000Z"
}
Keep the membership id — step 4 needs it:
export MEMBER=pmem_3ZjkQ2mN8pRtYv7X
status is pending because
Ana has never signed in.
If the address already
had an account it would read active and she would have access immediately;
either way the membership is recorded now, and naturali emails her a note saying
who added her and to which project.
member is the role to give a colleague who is here to build. Keep admin for
the people who should also decide who else is let in — an admin can do
everything you can except delete the project, and cannot grant admin to
anybody else, which is what keeps the two roles distinct.
The address is the credential, so a typo grants access to whoever holds that
address the first time they sign in. That is true of every emailed invitation.
status: "pending" is what makes the mistake visible before anyone acts on it —
check the list, and remove the member if the address is wrong (step 4).
3. Watch them arrive
There is nothing to accept. Ana signs in the ordinary way — she requests an emailed code for her address, at the link in the invitation — and receiving that code is what proves the address is hers. No invitation token, and nothing to expire.
If you used an address you control, sign in with it now, then read the list again as yourself:
- CLI
- SDK
- curl
naturali list-project-members --project-id "$PROJECT"
const { data: members } = await naturali.projects.listProjectMembers({
path: { project_id: process.env.PROJECT! },
});
const ana = members.data.find((m) => m.email === 'ana@acme.com');
console.log(ana?.status); // "active"
curl "$NATURALI_API/projects/$PROJECT/members" \
-H "Authorization: Bearer $NATURALI_TOKEN"
{
"data": [
{ "email": "you@acme.com", "status": "active", "role": "owner" },
{ "email": "ana@acme.com", "status": "active", "role": "member" }
]
}
status flipped on its own. It is derived from whether the account has ever
signed in, so nothing had to be accepted, swept or reconciled — and Ana was a
member the whole time she was pending.
4. Change their role, or let them go
Roles are not permanent. Promoting Ana to admin is one call, and the project
owner is the only role that may make it:
- CLI
- SDK
- curl
naturali update-project-member \
--project-id "$PROJECT" \
--member-id "$MEMBER" \
--role admin
const { data: promoted } = await naturali.projects.updateProjectMember({
path: { project_id: process.env.PROJECT!, member_id: process.env.MEMBER! },
body: { role: 'admin' },
});
curl -X PATCH "$NATURALI_API/projects/$PROJECT/members/$MEMBER" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"role":"admin"}'
{
"id": "pmem_3ZjkQ2mN8pRtYv7X",
"email": "ana@acme.com",
"status": "active",
"role": "admin",
"invited_by_user_id": "user_V1StGXR8Z5jdHi6B",
"created_at": "2026-07-17T00:05:00.000Z"
}
Removing a member is the same shape, and it answers 204:
- CLI
- SDK
- curl
naturali remove-project-member \
--project-id "$PROJECT" \
--member-id "$MEMBER"
await naturali.projects.removeProjectMember({
path: { project_id: process.env.PROJECT!, member_id: process.env.MEMBER! },
});
curl -X DELETE "$NATURALI_API/projects/$PROJECT/members/$MEMBER" \
-H "Authorization: Bearer $NATURALI_TOKEN"
An admin may remove a member but not another admin, and
anyone may remove themselves
whatever their role — nobody has to ask to be let out.
The owner's own row is the one thing no call here touches: it names who pays,
and a project with no owner would be unreachable.
What's next
- Projects — the full role matrix and how membership relates to the billing owner.
- API keys — a new colleague will want a project-scoped key of their own rather than sharing yours.
- Approvals — now that somebody else can act in the project, a human in the loop can be a different human from the one paying.