The Nylas Service Account is in beta. The authentication mechanism and features may change before general availability.
Nylas Service Accounts are different from Google and Microsoft “Service Accounts”. Google and Microsoft Service Accounts are used for bulk authentication grants. Nylas Service Accounts are an organization-level authentication mechanism that uses cryptographic request signing to access admin APIs like the Manage Domains API.
A Nylas Service Account authenticates your requests to organization-level Nylas admin APIs. Unlike API keys and access tokens, Nylas Service Account authentication uses RSA cryptographic request signing. Each request includes a signature generated from your private key, which Nylas verifies server-side.
Currently, Nylas Service Account authentication is used by:
Before you begin
Section titled “Before you begin”To get a Nylas Service Account, contact Nylas Support. They will provide you with a credentials JSON file.
Credential fields
Section titled “Credential fields”Your Nylas Service Account credentials file contains the following fields:
| Field | Description |
|---|---|
name | A human-readable name for the service account. |
type | The credential type. Always "service_account". |
private_key_id | The unique identifier for your private key. Used as the X-Nylas-Kid header value. |
private_key | Your RSA private key in PEM format. Used to sign requests. |
organization_id | The ID of your Nylas organization. |
region | The Nylas data center region (us or eu). |
Store your credentials file securely. The private key grants access to organization-level admin APIs. Never commit it to version control or expose it in client-side code. Use a secrets manager or environment variables.
Required headers
Section titled “Required headers”Every request authenticated with a Nylas Service Account must include these four custom headers:
| Header | Description |
|---|---|
X-Nylas-Kid | The private_key_id from your credentials file. Identifies which key Nylas should use to verify the signature. |
X-Nylas-Timestamp | The current time in seconds as a Unix timestamp. Must be within 5 minutes of the server time. |
X-Nylas-Nonce | A unique, randomly generated string (minimum 16 characters) for each request. Nylas rejects duplicate nonces to prevent replay attacks. |
X-Nylas-Signature | A Base64-encoded RSA-SHA256 signature of the request. See Generating the signature for details. |
Generating the signature
Section titled “Generating the signature”To generate the X-Nylas-Signature header value, follow these steps:
-
Build the canonical data object containing:
path— the API endpoint path (for example,/v3/admin/domains)method— the HTTP method in lowercase (for example,get,post)timestamp— the same Unix timestamp used inX-Nylas-Timestampnonce— the same nonce used inX-Nylas-Noncepayload— (only for POST/PUT/PATCH) the canonical JSON request body as a string. Omit this field for GET and DELETE requests.
-
Serialize to canonical JSON. The keys must be sorted alphabetically with no extra whitespace.
-
Hash the canonical JSON. Compute a SHA-256 digest of the serialized string.
-
Sign the hash. Sign the SHA-256 digest using RSA PKCS#1 v1.5 with your private key (2048-bit minimum).
-
Encode the signature. Base64-encode the resulting signature bytes.
Keep your system clock synchronized. Nylas rejects requests where the X-Nylas-Timestamp header differs from the server time by more than 5 minutes. Use NTP to keep your clock accurate.
Reference implementation
Section titled “Reference implementation”The following Go program generates signed curl commands for testing the Manage Domains API. For production use, integrate the signing logic directly into your application.
package main
import ( "crypto" "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/json" "encoding/pem" "fmt" "os" "sort" "strings" "time")
const nonceLength = 20
// generateNonce creates a cryptographically// secure random string.func generateNonce() (string, error) { const chars = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" result := make([]byte, nonceLength) randomBytes := make([]byte, nonceLength) if _, err := rand.Read(randomBytes); err != nil { return "", fmt.Errorf( "failed to generate random bytes: %w", err) } for i := 0; i < nonceLength; i++ { result[i] = chars[randomBytes[i]%byte(len(chars))] } return string(result), nil}
// canonicalJSON produces a deterministic JSON// representation with sorted keys.func canonicalJSON( data map[string]interface{},) ([]byte, error) { keys := make([]string, 0, len(data)) for k := range data { keys = append(keys, k) } sort.Strings(keys)
var sb strings.Builder sb.WriteString("{") for i, k := range keys { if i > 0 { sb.WriteString(",") } keyJSON, err := json.Marshal(k) if err != nil { return nil, fmt.Errorf( "failed to marshal key %s: %w", k, err) } sb.Write(keyJSON) sb.WriteString(":") valueJSON, err := json.Marshal(data[k]) if err != nil { return nil, fmt.Errorf( "failed to marshal value for key %s: %w", k, err) } sb.Write(valueJSON) } sb.WriteString("}") return []byte(sb.String()), nil}
// loadPrivateKey decodes and parses a// base64-encoded PEM private key.func loadPrivateKey( base64Key string,) (*rsa.PrivateKey, error) { pemBytes, err := base64.StdEncoding.DecodeString( base64Key) if err != nil { return nil, fmt.Errorf( "failed to decode base64: %w", err) }
block, _ := pem.Decode(pemBytes) if block == nil { return nil, fmt.Errorf("failed to parse PEM block") }
// Try PKCS#1 format first. if key, err := x509.ParsePKCS1PrivateKey( block.Bytes); err == nil { return key, nil }
// Try PKCS#8 format. keyInterface, err := x509.ParsePKCS8PrivateKey( block.Bytes) if err != nil { return nil, fmt.Errorf( "failed to parse private key "+ "(tried PKCS#1 and PKCS#8): %w", err) } rsaKey, ok := keyInterface.(*rsa.PrivateKey) if !ok { return nil, fmt.Errorf("private key is not RSA") } return rsaKey, nil}
// signRequest creates the signature for a// Nylas API request.func signRequest( privateKey *rsa.PrivateKey, path, method string, timestamp int64, nonce string, payload []byte,) (string, error) { canonicalData := map[string]interface{}{ "method": method, "nonce": nonce, "path": path, "timestamp": timestamp, }
// Only include payload for POST/PUT/PATCH. if len(payload) > 0 && (method == "post" || method == "put" || method == "patch") { canonicalData["payload"] = string(payload) }
canonicalBytes, err := canonicalJSON(canonicalData) if err != nil { return "", fmt.Errorf( "failed to create canonical JSON: %w", err) }
hashed := sha256.Sum256(canonicalBytes) signature, err := rsa.SignPKCS1v15( rand.Reader, privateKey, crypto.SHA256, hashed[:]) if err != nil { return "", fmt.Errorf("failed to sign: %w", err) } return base64.StdEncoding.EncodeToString( signature), nil}
func main() { // Required environment variables. privateKeyBase64 := os.Getenv("PRIVATE_KEY_B64") path := os.Getenv("REQUEST_PATH") kid := os.Getenv("NYLAS_KID")
if privateKeyBase64 == "" || path == "" || kid == "" { fmt.Fprintln(os.Stderr, "Error: PRIVATE_KEY_B64, REQUEST_PATH, "+ "and NYLAS_KID are required") os.Exit(1) }
// Optional environment variables. method := strings.ToLower( os.Getenv("REQUEST_METHOD")) if method == "" { method = "get" } payloadStr := os.Getenv("REQUEST_PAYLOAD") baseURL := os.Getenv("BASE_URL") if baseURL == "" { baseURL = "https://api.us.nylas.com" }
privateKey, err := loadPrivateKey(privateKeyBase64) if err != nil { fmt.Fprintf(os.Stderr, "Error loading private key: %v\n", err) os.Exit(1) }
timestamp := time.Now().Unix() nonce, err := generateNonce() if err != nil { fmt.Fprintf(os.Stderr, "Error generating nonce: %v\n", err) os.Exit(1) }
// Canonicalize the payload if provided. var payloadBytes []byte if payloadStr != "" { var payloadMap map[string]interface{} if err := json.Unmarshal( []byte(payloadStr), &payloadMap); err != nil { fmt.Fprintf(os.Stderr, "Error parsing REQUEST_PAYLOAD: %v\n", err) os.Exit(1) } payloadBytes, err = canonicalJSON(payloadMap) if err != nil { fmt.Fprintf(os.Stderr, "Error creating canonical payload: %v\n", err) os.Exit(1) } }
signature, err := signRequest( privateKey, path, method, timestamp, nonce, payloadBytes) if err != nil { fmt.Fprintf(os.Stderr, "Error signing request: %v\n", err) os.Exit(1) }
// Print the signed curl command. fmt.Printf( "curl --location --request %s '%s%s' \\\n", strings.ToUpper(method), baseURL, path) fmt.Printf( " --header 'X-Nylas-Kid: %s' \\\n", kid) fmt.Printf( " --header 'X-Nylas-Nonce: %s' \\\n", nonce) fmt.Printf( " --header 'X-Nylas-Timestamp: %d' \\\n", timestamp) fmt.Printf( " --header 'X-Nylas-Signature: %s' \\\n", signature) fmt.Printf( " --header 'Content-Type: application/json'") if payloadStr != "" { fmt.Printf(" \\\n --data '%s'", payloadStr) } fmt.Println()}Usage: Set the required environment variables and run the program.
PRIVATE_KEY_B64="$(cat private_key_b64.txt)" \REQUEST_PATH="/v3/admin/domains" \NYLAS_KID="08d8bdae-9981-432c-ac66-3006919d908e" \go run domain.goPRIVATE_KEY_B64="$(cat private_key_b64.txt)" \REQUEST_PATH="/v3/admin/domains/<DOMAIN_ID>/info" \REQUEST_METHOD="post" \REQUEST_PAYLOAD='{"type":"ownership"}' \NYLAS_KID="08d8bdae-9981-432c-ac66-3006919d908e" \go run domain.goRelated resources
Section titled “Related resources”- Managing domains — register and verify email domains using a Nylas Service Account
- Manage Domains API reference — full API reference for domain endpoints
- Manage API Keys API reference — full API reference for API key endpoints