SendSetsDocs

Authentication

Learn how to authenticate with the SendSets API using API keys.

The SendSets API uses API keys for authentication. Each API key has specific permissions that control what operations it can perform.

API key format

API keys follow this format:

ssk_<43-character-random-string>

The ssk_ prefix identifies the key as a SendSets key. The remaining 43 characters are 32 random bytes encoded as base64url (no padding): 256 bits of entropy, so brute-forcing a valid key is computationally infeasible.

Keys are stored as a SHA-256 hash; the plaintext is shown exactly once on creation. To help you spot a key in the dashboard without exposing the secret, we store the first 8 characters (key_prefix, e.g. ssk_ab) and the last 4 characters (key_suffix, e.g. wxyz). Render them as ssk_ab…wxyz.

Three ways to get a key

  1. The dashboard. Settings > API keys, pick the scopes, copy the secret. This is the right path for a key a server will use.
  2. The CLI. sendsets login opens a browser approval and mints a key named for the machine that asked, then stores it at 0600. This is the right path for a key you will use yourself, and it is the only one that does not involve pasting a secret into a shell.
  3. The API. POST /v1/api-keys with a key that carries API_KEYS. The secret is in that response and nowhere else.

All three produce the same thing: a ssk_ key with a scope bitmask, listed under Settings > API keys, revocable there.

The CLI device flow

sendsets login uses a device-code handshake, so the terminal never handles your password and the browser never handles the key:

  1. The CLI calls POST /v1/auth/cli/code with the scopes it wants and the machine's hostname. It gets back a device_code it keeps, a user_code it prints, and a verification_uri_complete it opens.
  2. You approve at app.sendsets.com/cli, choosing which workspace the key belongs to. The approval is what mints the key, so it requires the MANAGE_API_KEYS organization permission.
  3. The CLI polls POST /v1/auth/cli/poll and receives the key exactly once, on the first poll after approval.

Codes expire after ten minutes, both halves are per-IP rate limited, and the device_code is stored hashed. Anything else can drive the same flow: it is two public endpoints and a browser.

Ending a key

DELETE /v1/api-keys/:id revokes any key in the workspace and needs the API_KEYS scope. DELETE /v1/api-keys/self revokes the key the call was made with and needs no scope at all, so a narrowly scoped credential can always end itself. This is what sendsets auth logout uses.

Revoking leaves the key listed, with the time and reason it ended, and its request history intact. DELETE /v1/api-keys/:id/permanent removes the row for good, along with its usage logs, and is how the Delete key button under a revoked key in Settings > API keys works. It refuses a key that could still authenticate with a 409: revoke it first, so what ended the credential is on the record. A key past its expires_at can be deleted directly, since it already authenticates nothing.

Using your API key

Include your API key in the Authorization header of every request:

curl -X GET "https://api.sendsets.com/v1/api-keys" \
  -H "Authorization: Bearer ssk_abc123..." \
  -H "Content-Type: application/json"

For mutation retries, include an Idempotency-Key header with a unique value per logical operation. SendSets stores completed mutation responses for 24 hours per organization and key, then replays matching retries instead of performing the operation again.

Verifying a credential

To check that a credential is valid and see who it belongs to, call GET /v1/me. It works with an API key, an OAuth access token, or a dashboard session, requires no specific permission, and returns the caller's identity:

curl -X GET "https://api.sendsets.com/v1/me" \
  -H "Authorization: Bearer ssk_abc123..."
{
  "user_id": "0b1f...",
  "email": "jane@acme.com",
  "name": "Jane Doe",
  "organization_id": "9a2c...",
  "organization_name": "Acme Inc",
  "auth_type": "api_key",
  "scopes": ["read_contacts", "write_contacts"]
}

auth_type is api_key, oauth, or jwt, and scopes lists the granted API scopes for key and OAuth callers (empty for dashboard sessions, which use organization roles instead). This is the right endpoint for an integration to validate a connection and render a label. The separate GET /v1/auth/me is session-only and is not reachable with an API key or OAuth token.

OAuth access tokens

API keys authenticate your own scripts. If you are building an app that other people connect their SendSets workspace to, use OAuth instead: the user grants your app scoped access and you receive a bearer access token (prefix ssat_). It goes in the same Authorization: Bearer header and is checked against the same permissions, so every endpoint below behaves identically whether you present an API key or an OAuth token. The difference is only how the credential is obtained. See OAuth for the full flow.

Key security best practices

Keep Your Keys Secret

Never expose API keys in client-side code, public repositories, or logs. Treat them like passwords.

Do

  • Store API keys in environment variables or secure secret managers
  • Use different keys for development and production
  • Restrict keys to only the permissions they need
  • Set expiration dates for keys when possible
  • Use IP allowlists to restrict key usage

Don't

  • Commit API keys to version control
  • Share API keys via email or chat
  • Use production keys in development
  • Give keys more permissions than necessary

Permissions

Each API key has a permissions bitmask that controls its capabilities. See the Permissions Reference for a complete list.

Permission categories

CategoryDescription
ReadView resources (emails, campaigns, contacts, etc.)
WriteCreate and modify resources
BulkPerform bulk operations
SpecialAdvanced features (realtime, webhooks, API key management)

Example: read-only key

A read-only API key might have these permissions:

{
  "permissions": 31
}

This combines:

  • READ_EMAILS (1)
  • READ_CAMPAIGNS (2)
  • READ_CONTACTS (4)
  • READ_UNIBOX (8)
  • READ_ANALYTICS (16)

Total: 1 + 2 + 4 + 8 + 16 = 31

IP restrictions

You can restrict API keys to specific IPs or CIDR ranges. Entries can be bare IPs (v4 or v6) or CIDR blocks; an empty list means "any IP".

{
  "name": "Production Server",
  "permissions": 688159,
  "allowed_ips": [
    "203.0.113.10",
    "203.0.113.11",
    "10.0.0.0/8",
    "2001:db8::/32"
  ]
}

Requests from outside every listed range are rejected with 403 Forbidden. There's a soft cap of 64 entries per key.

Email account restrictions

Limit API keys to specific email accounts:

{
  "name": "Marketing Team",
  "permissions": 127,
  "allowed_email_accounts": [
    "550e8400-e29b-41d4-a716-446655440000",
    "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
  ]
}

Key expiration

Set an expiration date for temporary access:

{
  "name": "Contractor Access",
  "permissions": 31,
  "expires_at": "2027-12-31T23:59:59Z"
}

After expiration, the key returns 401 Unauthorized.

Using a credential with MCP

An MCP client (Claude Code, Claude Desktop, Cursor) authenticates to SendSets with either an OAuth sign-in (the one-command path, no key to paste) or a static API key. For a key, send it as a bearer token to https://api.sendsets.com/v1/mcp; the client then sees exactly the tools the credential's scopes allow. See the MCP server page for both paths.

Error responses

401 Unauthorized

Returned when:

  • API key is missing
  • API key is invalid
  • API key has expired
  • API key has been revoked
{
  "error": "Unauthorized",
  "message": "Token not found."
}

403 Forbidden

Returned when:

  • API key lacks required permissions
  • Request IP is not in allowlist
  • Email account is not in allowlist
{
  "error": "Forbidden",
  "message": "You don't have access to this feature."
}

Next steps

On this page