Team roles and audit log
Invite teammates with a role (owner, admin, sender, approver, viewer), work inside a team workspace with X-Workspace, per-member API keys, and the audit log of who did what.
Every account has one workspace. The owner (or an admin) invites people by email with a role. Members work inside the owner's account: its balance, sender IDs, contacts, templates and campaigns. Every action is recorded in the audit log.
Roles
| Role | Can |
|---|---|
owner | Everything, including billing and the team. The account holder (not stored as a member). |
admin | Everything except ownership: team, workspace settings, webhooks, sender ID requests, sending, approving. |
sender | Send messages, run campaigns, manage contacts and templates, create their own API keys. Not: team, workspace settings, webhook settings, alerts, sender ID requests, top-ups, approving campaigns. |
approver | Read-only, plus approve or reject campaigns that need sign-off. |
viewer | Read-only: reports, logs, contacts, campaigns. |
Read-only roles can still use calculation endpoints that change nothing
(rate check, number validation, previews, template render). A blocked action
returns 403 with code role_forbidden and the reason.
Approvals are maker-checker: the person who submitted a campaign can't approve
it (403 self_approval), even if they are an approver or admin. See
Campaigns.
Team endpoints
These are dashboard (signed-in) endpoints.
| Method | Path | Description |
|---|---|---|
GET | /v1/team | Members (owner first) with role, status (active / pending), can_manage, my_role, roles. |
GET | /v1/team/roles | Role list with descriptions. |
POST | /v1/team/invite | {email, role} - owner or admin. role is admin, sender, approver or viewer. An email invitation is sent. |
PATCH | /v1/team/{member_id} | {role} - owner or admin. |
DELETE | /v1/team/{member_id} | Remove a member; their API keys stop working. |
GET | /v1/workspaces | The workspaces you can work in (own + teams you joined) and pending invites. |
POST | /v1/workspaces/{id}/accept | Accept an invitation (sign in with the invited email). |
POST | /v1/workspaces/{id}/leave | Leave a team, or decline an invitation. |
Working in a team workspace
Send the workspace id in a header on every request:
X-Workspace: 603The API then acts on that workspace's account with your role. A workspace you
don't belong to returns 403 not_a_member. API keys ignore the header: a key
always acts in the workspace it was created in. The dashboard sets this header for
you when you pick a workspace in the switcher. Platform admin rights never
apply inside a customer workspace.
API keys created by members
A key created by a team member belongs to the workspace (it bills the
workspace balance) and carries the creator's role: a sender's key can send but
can't manage the team or webhooks, and a viewer or approver can only create
query keys. Members below admin can revoke only the keys they created. Keys
list created_by and created_role. Removing a member disables their keys.
Audit log
Owner and admins only.
GET https://sms.esmsafrica.io/v1/audit-logs?action=campaign.&actor=&q=&date_from=&date_to=&page=0&limit=50
GET https://sms.esmsafrica.io/v1/audit-logs/exportEach entry: at, actor (email, or null for actions the platform took such
as a campaign finishing), role, via (web, api or system), action,
target_type, target_id, summary, meta, ip. action filters by
prefix. The export is CSV.
Recorded actions include: message.sent and message.scheduled (dashboard
sends; API sends are in the request log), bulk.sent, campaign.created,
campaign.launched, campaign.submitted, campaign.approved,
campaign.rejected, campaign.pause, campaign.resume, campaign.cancel,
campaign.retry_failed, campaign.duplicated, campaign.deleted,
campaign.test_sent, campaign.paused and campaign.completed (system),
import.uploaded, import.pasted, import.gsheet, contacts.imported,
contact.saved, contact.updated, contact.deleted, group.*,
template.*, api_key.created, api_key.revoked, team.invited,
team.role_changed, team.removed, team.joined, team.left,
settings.updated.
Example
# A member's signed-in session token (API keys ignore X-Workspace):
curl https://sms.esmsafrica.io/v1/campaigns \
-H "Authorization: Bearer $ESMS_SESSION_TOKEN" -H "X-Workspace: 603"// Keys created by a member already act in the member's workspace with their role.
const esms = new Esms(process.env.ESMS_MEMBER_KEY);
const list = await esms.campaigns.list({ status: "pending_approval" });esms = Esms(api_key=os.environ["ESMS_MEMBER_KEY"])
pending = esms.campaigns.list(status="pending_approval")$esms = new \Esms\Client(getenv('ESMS_MEMBER_KEY'));
$pending = $esms->campaigns->list(['status' => 'pending_approval']);client := esms.New(os.Getenv("ESMS_MEMBER_KEY"))
pending, _ := client.Campaigns.List(ctx, esms.ListQuery{"status": "pending_approval"})