gastownhall/beads · error

Notion API error %s (%d): %s

Error message

Notion API error %s (%d): %s

What it means

Notion returned a non-2xx status and its JSON error body carried a structured message, so the library surfaces it as 'Notion API error <code> (<status>): <message>'. This is the server rejecting the request: bad token, missing permissions, invalid payload, rate limiting, etc. apiErr.Code is Notion's machine-readable error code (e.g. unauthorized, object_not_found, rate_limited).

Source

Thrown at internal/notion/client.go:299

	if err != nil {
		return nil, fmt.Errorf("request failed: %w", err)
	}
	defer func() { _ = resp.Body.Close() }()

	body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBytes))
	if err != nil {
		return nil, fmt.Errorf("read response: %w", err)
	}
	if resp.StatusCode >= 200 && resp.StatusCode < 300 {
		return body, nil
	}

	var apiErr struct {
		Code    string `json:"code"`
		Message string `json:"message"`
	}
	if err := json.Unmarshal(body, &apiErr); err == nil && apiErr.Message != "" {
		return nil, fmt.Errorf("Notion API error %s (%d): %s", apiErr.Code, resp.StatusCode, apiErr.Message)
	}
	return nil, fmt.Errorf("Notion API error (%d): %s", resp.StatusCode, strings.TrimSpace(string(body)))
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Match on the code: 'unauthorized' → regenerate the integration token; 'object_not_found' → share the target page/database with the integration.
  2. For 'rate_limited' (429), retry with exponential backoff respecting the Retry-After header.
  3. For 'validation_error', compare your request payload against the Notion API docs for that endpoint.
  4. Confirm the database ID and that your integration has at least read access to it in Notion's UI.

Example fix

// before
resp, err := client.Query(ctx, q) // Notion API error unauthorized (401): API token is invalid.

// after
var apiErr *notion.APIError
if errors.As(err, &apiErr) && apiErr.Code == "rate_limited" {
    time.Sleep(retryAfter)
    resp, err = client.Query(ctx, q) // retry with backoff
}
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-flight check
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.notion.com/v1/users/me", nil)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Notion-Version", "2022-06-28")
resp, err := http.DefaultClient.Do(req)
if err == nil && resp.StatusCode == 401 {
    return fmt.Errorf("notion token invalid")
}

Try / catch

var apiErr *notion.APIError
if errors.As(err, &apiErr) {
    switch apiErr.Code {
    case "unauthorized":  // fix token
    case "object_not_found": // share DB with integration
    case "rate_limited":  // backoff and retry
    case "validation_error": // fix payload
    }
}

Prevention

When it happens

Trigger: 401 unauthorized (invalid/revoked integration token); 404 object_not_found (page/database ID wrong or not shared with the integration); 400 validation_error (malformed properties/filter); 429 rate_limited (too many requests); 403 restricted_resource.

Common situations: Token rotated/revoked in Notion; database created but never shared with the integration (Notion's connection menu); wrong database ID (missing/extra dashes is fine, but wrong ID 404s); burst-bulk syncs hitting rate limits.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/d2eaa24dcc60e138. Report an issue: GitHub.