Pharos
Getting started.md

Quickstart

Ten minutes from nothing to a delivered email. Every step is an HTTP call, so this works the same from a terminal, a server, or an agent.

1. Get an API key

Keys come in two kinds: per project (phk_…) and per organization (pha_…). An organization key can address any project it owns and is the only one that can create projects, so it is the right credential to bootstrap with.

Create one in the dashboard under Settings → API keys. Pick the scopes it needs; the agent preset selects everything except the two send scopes, which is the right default for anything a model will hold. Set an expiry if the key is for a fixed piece of work — anything from a day to ten years.

Issuing a key is an owner/admin action. A key with send scopes can mail your entire list, so it carries the same authority as the sending credential itself.

The raw key is shown once, at creation. Pharos stores only its SHA-256 hash and there is no way to recover it — put it straight into your secret store as PHAROS_API_KEY.

Self-hosted deployments can also mint keys from the CLI:

pnpm org-key:create <org-slug> "Acme integration" --agent
pnpm api-key:create <project-slug> "Acme production" contacts:write,events:write,events:send

2. Create a project

Skip this if your project already exists.

curl -X POST https://pharosbase.com/api/v1/projects \
  -H "Authorization: Bearer $PHAROS_ORG_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Acme App", "sesFromEmail": "hi@acme.dev" }'

The slug is derived from the name and is globally unique — a collision returns 409. Project creation requires an organization key with projects:write; a project key is pinned to a project that already exists, so it has nothing to create.

3. Sync a contact

POST /api/v1/contacts/sync upserts contacts and their subscription state on one list. It is idempotent: sending the same desired state twice changes nothing.

curl -X POST https://pharosbase.com/api/v1/contacts/sync \
  -H "Authorization: Bearer $PHAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "acme-app",
    "listSlug": "newsletter",
    "source": "website_signup",
    "contacts": [{ "email": "reader@example.com", "subscribed": true }]
  }'

Requires contacts:write. Up to 5,000 contacts per call. Hard bounces and complaints are protected — a sync cannot resubscribe them, and the response reports how many it refused as protectedSuppressions.

Full contract: Contact sync.

4. Preview a transactional email

POST /api/v1/events composes an email from your payload. Preview does not deliver anything — it returns the composed subject, HTML and text so you (or a reviewer) can look at it first.

curl -X POST https://pharosbase.com/api/v1/events \
  -H "Authorization: Bearer $PHAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "acme-app",
    "eventType": "welcome.completed",
    "idempotencyKey": "welcome:user-42",
    "notificationCategory": "product",
    "deliveryMode": "preview",
    "recipient": { "email": "reader@example.com", "firstName": "Sam" },
    "subject": "Welcome to Acme",
    "html": "<p>Glad you are here, Sam.</p>"
  }'

Requires events:write — nothing more. This is the call an agent can make.

5. Send it

Same request, deliveryMode: "send".

  -d '{ …, "deliveryMode": "send" }'

This one requires events:send, a scope agent keys do not carry. The event is persisted under its (project, idempotencyKey) pair and queued for delivery, so repeating the request does not send twice.

Every delivery is recorded in the project's send log with its delivery and open state — visible at /<team>/<project>/sends in the dashboard or through GET /api/sends.

Full contract: Transactional events.

6. Point an agent at it

claude mcp add pharos -s user -t http https://pharosbase.com/api/mcp \
  -H "Authorization: Bearer $PHAROS_AGENT_KEY"

Nothing to install. The agent gets the same API you just used, bounded by the same key — including the part where it cannot send. See MCP server.

Next