A local FastAPI mock of the Granola API for testing ingestion pipelines without a real Granola workspace.
Project description
mock-granola-api
This is a public-source project — see CONTRIBUTING.md for the PR policy.
A local FastAPI mock of the Granola API (meeting notes, transcripts, folders, webhooks) for testing ingestion pipelines without a real Granola workspace. It mirrors the real API's schemas, ID formats, error shapes, pagination, rate limits, and signed webhook delivery closely enough that client code written against it runs unmodified against the real API — only a base URL and an API key change.
On top of the real API surface, an /admin/* control-plane (mock-only, no
auth) lets you script synthetic data — seed random notes, create specific
notes with exact content, edit them, and trigger the same events
(note.generated, note.edited, note.regenerated, note.access_granted)
a real Granola workspace would fire.
Quickstart
make run # starts the mock at http://127.0.0.1:8000
make test # runs the test suite
API surface
Public API (/v1/*, requires Authorization: Bearer grn_...) —
identical to the real Granola API:
| Method | Path | Description |
|---|---|---|
| GET | /v1/notes |
list, paginated, filterable by date/folder |
| GET | /v1/notes/{id} |
one note; ?include=transcript to include it |
| GET | /v1/folders |
list, paginated |
| POST | /v1/webhook-endpoints |
register a URL for note events |
| GET | /v1/webhook-endpoints |
list registered endpoints |
| DELETE | /v1/webhook-endpoints/{id} |
remove one |
Admin control-plane (/admin/*, mock-only, no auth) — for generating
synthetic data, not something a real ingestion client would ever call:
| Method | Path | Description |
|---|---|---|
| POST | /admin/seed |
bulk-generate count random Faker notes/folders |
| POST | /admin/reset |
wipe all state (optionally reseed) |
| POST | /admin/folders |
create one folder |
| POST | /admin/notes |
create one note with exact content (see below) |
| PATCH | /admin/notes/{id} |
edit a note, fires note.edited |
| POST | /admin/notes/{id}/regenerate |
fires note.regenerated |
| POST | /admin/notes/{id}/grant-access |
fires note.access_granted |
| GET | /admin/webhook-deliveries |
inspect delivery attempts/retries |
POST /admin/notes accepts a transcript field — a list of
{text, speaker_name?, source?, diarization_label?} turns — for exact
transcript content instead of random Faker filler. See
app/models.py::AdminTranscriptTurnInput.
Calling this API from a Python script
examples/granola_client.py is a small, dependency-light client
(httpx only) that talks to /v1/* — the same surface the real Granola API
exposes. It never touches /admin/*, so it works identically against the
mock and the real thing:
from granola_client import GranolaClient
with GranolaClient.from_env() as client:
for summary in client.iter_all_notes(page_size=10):
note = client.get_note(summary["id"], include_transcript=True)
print(note["id"], note["title"], note["owner"]["name"])
examples/ingest_notes.py is that snippet as a runnable script. Try it
against the mock:
make run # terminal 1
make acme-demo # terminal 2: seeds some notes
GRANOLA_ENV=dev GRANOLA_API_KEY=grn_dev_dummy_key \
uv run --frozen python examples/ingest_notes.py
(make ingest-demo does the same with GRANOLA_ENV/GRANOLA_API_KEY
pre-set.)
The dev/prod switch
All environment-specific behavior lives in one place:
GranolaClient.from_env(). Calling code never branches on environment — it
just constructs the client and calls the same methods either way:
| Env var | dev | prod |
|---|---|---|
GRANOLA_ENV |
dev (default) |
prod |
GRANOLA_API_BASE_URL |
optional override | optional override |
GRANOLA_API_KEY |
any grn_... string |
a real workspace key |
Left unset, GRANOLA_API_BASE_URL defaults to http://localhost:8000 in
dev and https://public-api.granola.ai in prod — set it explicitly only
when the mock lives somewhere else (e.g. a docker-compose service name, see
below). GRANOLA_API_KEY only needs to look like a real key in dev — the
mock checks the grn_... format only, never a real registry; prod needs an
actual workspace key issued by Granola.
This is the same pattern a real Google Drive connector for a different data source would establish: build and validate everything against synthetic data first, and only point at the real API once the connector is proven — no code changes between the two, only environment.
Packaging for Docker
Because the only difference between environments is env vars, the ingestion image is the same artifact in both places:
# docker-compose.dev.yml — mock + ingestion, for local/dev/CI
services:
granola-mock:
build: ./mock-granola-api
ports: ["8000:8000"]
ingestion:
image: ghcr.io/your-org/ingestion:latest
environment:
GRANOLA_ENV: dev
GRANOLA_API_BASE_URL: http://granola-mock:8000
GRANOLA_API_KEY: grn_dev_dummy_key
depends_on: [granola-mock]
# docker-compose.prod.yml — same ingestion image, no mock container at all
services:
ingestion:
image: ghcr.io/your-org/ingestion:latest
environment:
GRANOLA_ENV: prod
GRANOLA_API_KEY: ${GRANOLA_API_KEY} # real key, injected at deploy time
Context: why this exists
Built to test an ingestion pipeline that lists Granola as an inflow source
but has no live connector yet — a deal-note.md-style skill only parses a
manually pasted Granola export. examples/acme_demo.py demonstrates the
full loop this mock enables instead: seed VC-deal-shaped synthetic notes,
register a webhook and confirm signed delivery, fetch through the real
public endpoints, and render the result both as the plain-text export
deal-note.md already parses and as a wiki-dealflow frontmatter stub
(title/status/sector/ask/portfolio_company/updated) — proving a real
connector could be built and fully tested here, against synthetic data
only, before it ever sees a real Granola API key.
Development
make test # pytest
ruff check . # lint
ruff format . # format
pyright . # types
See app/ for the mock's implementation and tests/ for coverage of
pagination, auth, rate limiting, webhook signing/retries, and the admin API.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mock_granola_api-0.1.0.tar.gz.
File metadata
- Download URL: mock_granola_api-0.1.0.tar.gz
- Upload date:
- Size: 20.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d4e13b00a5c89c7abb41f0aecf4c75e9718535f20d02566706cb42c565dc4c54
|
|
| MD5 |
bba0aabe756a0a69b06303999e679f12
|
|
| BLAKE2b-256 |
376b92624b87934ec119d8730fef57e71dcc205f34bcd5b9b69cca7f131c51ad
|
File details
Details for the file mock_granola_api-0.1.0-py3-none-any.whl.
File metadata
- Download URL: mock_granola_api-0.1.0-py3-none-any.whl
- Upload date:
- Size: 18.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9b3bb020a6c2047e24c36784b44b7f9991c3d3cf3e59815ef3b6fd4d7ca03b89
|
|
| MD5 |
034936dc467e7108ecb25b5c331e657f
|
|
| BLAKE2b-256 |
fe0f5bb61fc7d7796cd585df5716083d6a99372c09a0cf365aac117672cbfcf6
|