README.md

# Granola

Elixir client for the [Granola API](https://docs.granola.ai/introduction).

## Installation

<!-- x-release-please-start-version -->
```elixir
def deps do
  [
    {:granola, "~> 1.0.0"}
  ]
end
```
<!-- x-release-please-end -->

## Usage

### Create a client

```elixir
client = Granola.new(api_key: "grn_YOUR_API_KEY")
```

API keys can be created in Granola under **Settings → API** (Business/Enterprise
plans).

### List notes

```elixir
{:ok, result} = Granola.Notes.list(client)

result.notes    # list of note summaries
result.hasMore  # true if there are more pages
result.cursor   # pass as :cursor to fetch the next page
```

Filter and paginate:

```elixir
{:ok, result} = Granola.Notes.list(client,
  created_after: ~D[2026-01-01],
  page_size: 30
)

# Next page
{:ok, next} = Granola.Notes.list(client, cursor: result.cursor)
```

Available filters: `:created_before`, `:created_after`, `:updated_after`,
`:cursor`, `:page_size` (1–30, default 10).

### Get a note

```elixir
{:ok, note} = Granola.Notes.get(client, "not_1d3tmYTlCICgjy")

note.id               # "not_1d3tmYTlCICgjy"
note.title            # "Quarterly yoghurt budget review"
note.summary_text     # plain text summary
note.summary_markdown # markdown summary
note.owner            # %{name: "...", email: "..."}
note.attendees        # list of %{name, email}
note.calendar_event   # associated calendar event or nil
note.web_url          # link to note in Granola web app
```

Request the full transcript:

```elixir
{:ok, note} = Granola.Notes.get(client, "not_1d3tmYTlCICgjy", include: :transcript)

for segment <- note.transcript do
  IO.puts("#{segment.speaker.source}: #{segment.text}")
end
```

Each transcript segment has `:speaker` (with `:source` of `"microphone"` or
`"speaker"`), `:text`, `:start_time`, and `:end_time`.

> Notes without a generated AI summary return a 404 error.

### Stream all notes

`Granola.Notes.stream/2` lazily paginates through all notes, fetching the next
page only when needed:

```elixir
Granola.Notes.stream(client, created_after: ~D[2026-01-01])
|> Stream.each(fn note -> IO.puts(note.title) end)
|> Stream.run()
```

Accepts the same filter options as `list/2`, except `:cursor` and `:page_size`.

### Error handling

All functions return `{:ok, result}` on success or `{:error, reason}` on failure:

```elixir
case Granola.Notes.get(client, id) do
  {:ok, note} -> note
  {:error, {404, _body}} -> :not_found
  {:error, {401, _body}} -> :unauthorized
  {:error, %Req.TransportError{} = err} -> {:network_error, err}
end
```

## Testing

Use `Req.Test` to stub HTTP calls without making real requests:

```elixir
client = Granola.new(api_key: "grn_test", plug: {Req.Test, __MODULE__})

Req.Test.stub(__MODULE__, fn conn ->
  Req.Test.json(conn, %{
    "notes" => [],
    "hasMore" => false,
    "cursor" => nil
  })
end)

assert {:ok, result} = Granola.Notes.list(client)
```

## Rate limits

The Granola API allows 25 requests per 5 seconds (burst) or 5 requests/second
sustained. Retries are disabled by default in the client; implement your own
retry/backoff if needed (or pass `retry: :safe_transient` to `Granola.new/1`).