diff --git a/pyproject.toml b/pyproject.toml
index 408cc902e1d23acbb8a8c90949ccafe3f7e7a6bb..c3ea258abb781795e7e54741b3bd8a591b4b0e0a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -129,5 +129,7 @@ filterwarnings = [
     # deleting tempdir with weakref.finalize() fails sometimes
     'ignore::pytest.PytestUnraisableExceptionWarning',
     # plt.pause() in headless mode
-    'ignore:FigureCanvasAgg is non-interactive:UserWarning'
+    'ignore:FigureCanvasAgg is non-interactive:UserWarning',
+    # warnings from doctests for warnings don't need to be reported
+    "ignore:Keyword argument '.*' of .* is deprecated.*:DeprecationWarning",
 ]
diff --git a/qutil/misc.py b/qutil/misc.py
index 875e4e876688701843ab5ef93fbfbb07eaf98b66..0a6ac5d884c1291073c3b674f352a03e93f96547 100644
--- a/qutil/misc.py
+++ b/qutil/misc.py
@@ -9,7 +9,7 @@ from collections.abc import MutableMapping, Callable, Iterator
 from contextlib import contextmanager
 from importlib import import_module
 from types import ModuleType
-from typing import Dict, Optional, Union, Any, Hashable, ContextManager
+from typing import Dict, Optional, Union, Any, Hashable
 from unittest import mock
 
 from .functools import wraps
@@ -299,3 +299,74 @@ def timeout(value: float, on_exceeded: Callable[[float], None] | None = None,
         if raise_exc:
             raise TimeoutError(msg.format(exceeded.elapsed))
 
+
+def deprecate_kwarg(old: str, new: str | None = None):
+    """Decorator factory to deprecate the keyword argument *old*.
+
+    Optionally in favor of *new*.
+
+    Examples
+    --------
+    Deprecate a kwarg in favor of a new name:
+
+    >>> @deprecate_kwarg('old', 'new')
+    ... def foo(new=3):
+    ...     print(new)
+
+    Promote warnings to errors for doctest:
+
+    >>> with filter_warnings(action='error', category=DeprecationWarning):
+    ...     foo(old=4)
+    Traceback (most recent call last):
+    ...
+    DeprecationWarning: Keyword argument 'old' of foo is deprecated. Use 'new' instead.
+
+    Using the new name works as expected:
+
+    >>> foo(new=5)
+    5
+
+    Only deprecate a kwarg without giving a replacement:
+
+    >>> @deprecate_kwarg('old')
+    ... def foo(new=3):
+    ...     print(new)
+
+    Again promote the warning to an error:
+
+    >>> with filter_warnings(action='error', category=DeprecationWarning):
+    ...     foo(old=4)  # doctest: +ELLIPSIS
+    Traceback (most recent call last):
+    ...
+    DeprecationWarning: Keyword argument 'old' of foo is deprecated.
+
+    Without promoting the warning, a regular :class:`TypeError` is raised:
+
+    >>> foo(old=4)  # doctest: +ELLIPSIS
+    Traceback (most recent call last):
+    ...
+    TypeError: foo() got an unexpected keyword argument 'old'
+
+    """
+
+    def decorator(func):
+
+        @wraps(func)
+        def wrapped(*args, **kwargs):
+            if old not in kwargs:
+                return func(*args, **kwargs)
+
+            msg = f"Keyword argument '{old}' of {func.__qualname__} is deprecated."
+            if new is not None:
+                if new in kwargs:
+                    raise TypeError(msg + f" Cannot also specify '{new}'.")
+                else:
+                    msg = msg + f" Use '{new}' instead."
+                    kwargs[new] = kwargs.pop(old)
+
+            warnings.warn(msg, DeprecationWarning, stacklevel=2)
+            return func(*args, **kwargs)
+
+        return wrapped
+
+    return decorator
diff --git a/qutil/signal_processing/fourier_space.py b/qutil/signal_processing/fourier_space.py
index 73677a8cad23027b0b480c343691630142e82773..ad512796e1aa66cef7f810d12ff785974876faf8 100644
--- a/qutil/signal_processing/fourier_space.py
+++ b/qutil/signal_processing/fourier_space.py
@@ -46,6 +46,7 @@ import numpy as np
 from qutil import math
 from qutil.caching import cache
 from qutil.functools import wraps
+from qutil.misc import deprecate_kwarg
 from qutil.signal_processing._common import _parse_filter_edges
 from qutil.typecheck import check_literals
 from scipy import integrate
@@ -113,31 +114,77 @@ def Id(x: _S, f: _T, *_, **__) -> Tuple[_S, _T]:
     return x, f
 
 
-def derivative(x, f, deriv_order: int = 0, overwrite_x: bool = False,
+@deprecate_kwarg('deriv_order', 'order')
+def derivative(x, f, order: int = 0, overwrite_x: bool = False,
                **_) -> Tuple[np.ndarray, np.ndarray]:
-    """Perform (anti-)derivatives.
+    r"""Compute the (anti-)derivative.
+
+    The sign convention is according to the following defintion of the
+    Fourier transform:
+
+    .. math::
+        \hat{f}(\omega) &= \int_{-\infty}^\infty\mathrm{d}t
+            e^{-i\omega t} f(t) \\
+        f(t) &= \int_{-\infty}^\infty\frac{\mathrm{d}\omega}{2\pi}
+            e^{i\omega t} \hat{f}(\omega)
 
     .. note::
-        For negative antiderivatives, the zero-frequency component is
-        set to zero (due to zero-division).
+        For negative ``order`` (antiderivatives), the zero-frequency
+        component is set to zero (due to zero-division).
 
     Parameters
     ----------
     x : array_like
-        The data to be filtered.
+        Target data.
     f : array_like
         Frequencies corresponding to the last axis of `x`.
-    deriv_order : int
+    order : int
         The order of the derivative. If negative, the antiderivative is
         computed (indefinite integral). Default: 0.
     overwrite_x : bool, default False
         Overwrite the input data array.
+
+    Examples
+    --------
+    Compare finite-differences to the Fourier space derivative:
+
+    >>> dt = .1; n = 51
+    >>> t = np.arange(n * dt, step=dt)
+    >>> xt = np.sin(5 * t)
+    >>> f = np.fft.rfftfreq(n, dt)
+    >>> xf = np.fft.rfft(xt)
+
+    Compute the derivatives:
+
+    >>> import matplotlib.pyplot as plt
+    >>> xfd, f = derivative(xf, f, order=1)
+    >>> xtd = np.gradient(xt, t)
+    >>> plt.plot(t, 5 * np.cos(5 * t), label='Analytical')  # doctest: +SKIP
+    >>> plt.plot(t, xtd, label='Finite differences')  # doctest: +SKIP
+    >>> plt.plot(t, np.fft.irfft(xfd, n=n), label='FFT')  # doctest: +SKIP
+    >>> plt.legend(); plt.xlabel('$t$'); plt.ylabel('$dx/dt$')  # doctest: +SKIP
+
+    Integrals:
+
+    >>> from scipy import integrate
+    >>> xfii, f = derivative(xf, f, order=-2)
+    >>> xti = integrate.cumulative_simpson(xt, x=t, initial=0)
+    >>> xtii = integrate.cumulative_simpson(xti, x=t, initial=0)
+    >>> plt.plot(t, -xt / 25, label='Analytical')  # doctest: +SKIP
+    >>> # cheating a bit here because indefinite integration is tricky
+    >>> plt.plot(t, xtii - np.linspace(0, 1, n), label='Finite differences')  # doctest: +SKIP
+    >>> plt.plot(t, np.fft.irfft(xfii, n=n), label='FFT')  # doctest: +SKIP
+    >>> plt.legend(); plt.xlabel('$t$'); plt.ylabel(r'$\iint x dt$')  # doctest: +SKIP
+
+    Note that the backtransform method naturally only works well for
+    periodic data.
+
     """
     x = np.array(x, copy=not overwrite_x)
     f = np.asanyarray(f)
     with np.errstate(invalid='ignore', divide='ignore'):
-        x *= (2*np.pi*f)**deriv_order
-    if deriv_order < 0:
+        x *= (2j*np.pi*f)**order
+    if order < 0:
         x[..., (f == 0).nonzero()] = 0
     return x, f