diff --git a/api/api.go b/api/api.go
index 6dcacc5f6..33fcba409 100644
--- a/api/api.go
+++ b/api/api.go
@@ -71,6 +71,9 @@ const (
 	ParamWithResults     = "withResults"
 	ParamFinalResults    = "finalResults"
 	ParamManuallyEnded   = "manuallyEnded"
+	ParamChainId         = "chainId"
+	ParamHash            = "hash"
+	ParamProposerAddress = "proposerAddress"
 	ParamHeight          = "height"
 	ParamReference       = "reference"
 	ParamType            = "type"
diff --git a/api/api_types.go b/api/api_types.go
index d948b7207..efb390503 100644
--- a/api/api_types.go
+++ b/api/api_types.go
@@ -54,6 +54,14 @@ type TransactionParams struct {
 	Type   string `json:"type,omitempty"`
 }
 
+// BlockParams allows the client to filter blocks
+type BlockParams struct {
+	PaginationParams
+	ChainID         string `json:"chainId,omitempty"`
+	Hash            string `json:"hash,omitempty"`
+	ProposerAddress string `json:"proposerAddress,omitempty"`
+}
+
 // FeesParams allows the client to filter fees
 type FeesParams struct {
 	PaginationParams
@@ -439,3 +447,9 @@ type Block struct {
 	comettypes.Block `json:",inline"`
 	Hash             types.HexBytes `json:"hash" `
 }
+
+// BlockList is used to return a paginated list to the client
+type BlockList struct {
+	Blocks     []*indexertypes.Block `json:"blocks"`
+	Pagination *Pagination           `json:"pagination"`
+}
diff --git a/api/chain.go b/api/chain.go
index 13fd80127..7907d5d99 100644
--- a/api/chain.go
+++ b/api/chain.go
@@ -18,6 +18,7 @@ import (
 	"go.vocdoni.io/dvote/vochain"
 	"go.vocdoni.io/dvote/vochain/genesis"
 	"go.vocdoni.io/dvote/vochain/indexer"
+	"go.vocdoni.io/dvote/vochain/indexer/indexertypes"
 	"go.vocdoni.io/dvote/vochain/state"
 )
 
@@ -178,6 +179,14 @@ func (a *API) enableChainHandlers() error {
 	); err != nil {
 		return err
 	}
+	if err := a.Endpoint.RegisterMethod(
+		"/chain/blocks",
+		"GET",
+		apirest.MethodAccessTypePublic,
+		a.chainBlockListHandler,
+	); err != nil {
+		return err
+	}
 	if err := a.Endpoint.RegisterMethod(
 		"/chain/organizations/filter/page/{page}",
 		"POST",
@@ -972,6 +981,95 @@ func (a *API) chainBlockByHashHandler(_ *apirest.APIdata, ctx *httprouter.HTTPCo
 	return ctx.Send(convertKeysToCamel(data), apirest.HTTPstatusOK)
 }
 
+// chainBlockListHandler
+//
+//	@Summary		List all blocks
+//	@Description	Returns the list of blocks, ordered by descending height.
+//	@Tags			Chain
+//	@Accept			json
+//	@Produce		json
+//	@Param			page			query		number	false	"Page"
+//	@Param			limit			query		number	false	"Items per page"
+//	@Param			chainId			query		string	false	"Filter by exact chainId"
+//	@Param			hash			query		string	false	"Filter by partial hash"
+//	@Param			proposerAddress	query		string	false	"Filter by exact proposerAddress"
+//	@Success		200				{object}	BlockList
+//	@Router			/chain/blocks [get]
+func (a *API) chainBlockListHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error {
+	params, err := parseBlockParams(
+		ctx.QueryParam(ParamPage),
+		ctx.QueryParam(ParamLimit),
+		ctx.QueryParam(ParamChainId),
+		ctx.QueryParam(ParamHash),
+		ctx.QueryParam(ParamProposerAddress),
+	)
+	if err != nil {
+		return err
+	}
+
+	return a.sendBlockList(ctx, params)
+}
+
+// sendBlockList produces a filtered, paginated BlockList,
+// and sends it marshalled over ctx.Send
+//
+// Errors returned are always of type APIerror.
+func (a *API) sendBlockList(ctx *httprouter.HTTPContext, params *BlockParams) error {
+	// TODO: replace this by a.indexer.BlockList when it's available
+	blockList := func(limit, offset int, _, _, _ string) ([]*indexertypes.Block, uint64, error) {
+		if offset < 0 {
+			return nil, 0, fmt.Errorf("invalid value: offset cannot be %d", offset)
+		}
+		if limit <= 0 {
+			return nil, 0, fmt.Errorf("invalid value: limit cannot be %d", limit)
+		}
+		height := a.vocapp.Height()
+		total := uint64(height) - uint64(a.vocapp.Node.BlockStore().Base())
+		start := height - uint32(params.Page*params.Limit)
+		end := start - uint32(params.Limit)
+		list := []*indexertypes.Block{}
+		for h := start; h > end; h-- {
+			tmblock := a.vocapp.GetBlockByHeight(int64(h))
+			if tmblock == nil {
+				break
+			}
+			list = append(list, &indexertypes.Block{
+				ChainID:         tmblock.ChainID,
+				Height:          tmblock.Height,
+				Time:            tmblock.Time,
+				Hash:            types.HexBytes(tmblock.Hash()),
+				ProposerAddress: tmblock.ProposerAddress.Bytes(),
+				LastBlockHash:   tmblock.LastBlockID.Hash.Bytes(),
+				TxCount:         int64(len(tmblock.Txs)),
+			})
+		}
+
+		return list, uint64(total), nil
+	}
+
+	blocks, total, err := blockList(
+		params.Limit,
+		params.Page*params.Limit,
+		params.ChainID,
+		params.Hash,
+		params.ProposerAddress,
+	)
+	if err != nil {
+		return ErrIndexerQueryFailed.WithErr(err)
+	}
+
+	pagination, err := calculatePagination(params.Page, params.Limit, total)
+	if err != nil {
+		return err
+	}
+
+	list := &BlockList{
+		Blocks:     blocks,
+		Pagination: pagination,
+	}
+	return marshalAndSend(ctx, list)
+}
+
 // chainTransactionCountHandler
 //
 //	@Summary		Transactions count
@@ -1320,3 +1418,18 @@ func parseTransactionParams(paramPage, paramLimit, paramHeight, paramType string
 		Type:             paramType,
 	}, nil
 }
+
+// parseBlockParams returns an BlockParams filled with the passed params
+func parseBlockParams(paramPage, paramLimit, paramChainId, paramHash, paramProposerAddress string) (*BlockParams, error) {
+	pagination, err := parsePaginationParams(paramPage, paramLimit)
+	if err != nil {
+		return nil, err
+	}
+
+	return &BlockParams{
+		PaginationParams: pagination,
+		ChainID:          paramChainId,
+		Hash:             util.TrimHex(paramHash),
+		ProposerAddress:  util.TrimHex(paramProposerAddress),
+	}, nil
+}
diff --git a/vochain/indexer/indexertypes/block.go b/vochain/indexer/indexertypes/block.go
new file mode 100644
index 000000000..4954dbc22
--- /dev/null
+++ b/vochain/indexer/indexertypes/block.go
@@ -0,0 +1,20 @@
+package indexertypes
+
+import (
+	"time"
+
+	"go.vocdoni.io/dvote/types"
+)
+
+// Block represents a block handled by the Vochain.
+// The indexer Block data type is different from the vochain state data type
+// since it is optimized for querying purposes and not for keeping a shared consensus state.
+type Block struct {
+	ChainID         string         `json:"chainId"`
+	Height          int64          `json:"height"`
+	Time            time.Time      `json:"time"`
+	Hash            types.HexBytes `json:"hash"`
+	ProposerAddress types.HexBytes `json:"proposer"`
+	LastBlockHash   types.HexBytes `json:"lastBlockHash"`
+	TxCount         int64          `json:"txCount"`
+}