diff --git a/client/rpc/pin.go b/client/rpc/pin.go
index 632d8f08da4a6d5ad2c9ac94cf01493901a2021b..a0469861c7ed19e89d2cc41956ac2302a3a7d7ce 100644
--- a/client/rpc/pin.go
+++ b/client/rpc/pin.go
@@ -26,6 +26,7 @@ type pinRefKeyList struct {
 type pin struct {
 	path path.ImmutablePath
 	typ  string
+	name string
 	err  error
 }
 
@@ -37,6 +38,10 @@ func (p pin) Path() path.ImmutablePath {
 	return p.path
 }
 
+func (p pin) Name() string {
+	return p.name
+}
+
 func (p pin) Type() string {
 	return p.typ
 }
@@ -53,6 +58,7 @@ func (api *PinAPI) Add(ctx context.Context, p path.Path, opts ...caopts.PinAddOp
 
 type pinLsObject struct {
 	Cid  string
+	Name string
 	Type string
 }
 
@@ -102,7 +108,7 @@ func (api *PinAPI) Ls(ctx context.Context, opts ...caopts.PinLsOption) (<-chan i
 			}
 
 			select {
-			case ch <- pin{typ: out.Type, path: path.FromCid(c)}:
+			case ch <- pin{typ: out.Type, name: out.Name, path: path.FromCid(c)}:
 			case <-ctx.Done():
 				return
 			}
diff --git a/cmd/ipfs/kubo/init.go b/cmd/ipfs/kubo/init.go
index 47aee7aeb0ff9708dd42800a297a7f2bad3ac52a..986fe90c8b136a5aa3c0c9810a73c24ee0287948 100644
--- a/cmd/ipfs/kubo/init.go
+++ b/cmd/ipfs/kubo/init.go
@@ -252,7 +252,7 @@ func initializeIpnsKeyspace(repoRoot string) error {
 
 	// pin recursively because this might already be pinned
 	// and doing a direct pin would throw an error in that case
-	err = nd.Pinning.Pin(ctx, emptyDir, true)
+	err = nd.Pinning.Pin(ctx, emptyDir, true, "")
 	if err != nil {
 		return err
 	}
diff --git a/core/commands/dag/import.go b/core/commands/dag/import.go
index d95ff7198a4be2f1f647bbd188e884c0cda1d46e..5e39393c1cec43464e81a38aa31c430d1e2f2689 100644
--- a/core/commands/dag/import.go
+++ b/core/commands/dag/import.go
@@ -152,7 +152,7 @@ func dagImport(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment
 				ret.PinErrorMsg = err.Error()
 			} else if nd, err := blockDecoder.DecodeNode(req.Context, block); err != nil {
 				ret.PinErrorMsg = err.Error()
-			} else if err := node.Pinning.Pin(req.Context, nd, true); err != nil {
+			} else if err := node.Pinning.Pin(req.Context, nd, true, ""); err != nil {
 				ret.PinErrorMsg = err.Error()
 			} else if err := node.Pinning.Flush(req.Context); err != nil {
 				ret.PinErrorMsg = err.Error()
diff --git a/core/commands/pin/pin.go b/core/commands/pin/pin.go
index db623a7e6c99ef05edf57c1a3f47e42d4a48d96d..d75d5d3865aef7d1597b10008a15ac4cfa29aafe 100644
--- a/core/commands/pin/pin.go
+++ b/core/commands/pin/pin.go
@@ -57,6 +57,28 @@ var addPinCmd = &cmds.Command{
 	Helptext: cmds.HelpText{
 		Tagline:          "Pin objects to local storage.",
 		ShortDescription: "Stores an IPFS object(s) from a given path locally to disk.",
+		LongDescription: `
+Create a pin for the given object, protecting resolved CID from being garbage
+collected.
+
+An optional name can be provided, and read back via 'ipfs pin ls --names'.
+
+Be mindful of defaults:
+
+Default pin type is 'recursive' (entire DAG).
+Pass -r=false to create a direct pin for a single block.
+Use 'pin ls -t recursive' to only list roots of recursively pinned DAGs
+(significantly faster when many big DAGs are pinned recursively)
+
+Default pin name is empty. Pass '--name' to 'pin add' to set one
+and use 'pin ls --names' to see it.
+Pin add is idempotent: pinning CID which is already pinned won't change
+the name, value passed with '--name' with the original pin is preserved.
+To rename pin, use 'pin rm' and 'pin add --name'.
+
+If daemon is running, any missing blocks will be retrieved from the network.
+It may take some time. Pass '--progress' to track the progress.
+`,
 	},
 
 	Arguments: []cmds.Argument{
@@ -64,6 +86,7 @@ var addPinCmd = &cmds.Command{
 	},
 	Options: []cmds.Option{
 		cmds.BoolOption(pinRecursiveOptionName, "r", "Recursively pin the object linked to by the specified object(s).").WithDefault(true),
+		cmds.StringOption(pinNameOptionName, "n", "An optional name for created pin(s)."),
 		cmds.BoolOption(pinProgressOptionName, "Show progress"),
 	},
 	Type: AddPinOutput{},
@@ -75,6 +98,7 @@ var addPinCmd = &cmds.Command{
 
 		// set recursive flag
 		recursive, _ := req.Options[pinRecursiveOptionName].(bool)
+		name, _ := req.Options[pinNameOptionName].(string)
 		showProgress, _ := req.Options[pinProgressOptionName].(bool)
 
 		if err := req.ParseBodyArgs(); err != nil {
@@ -87,7 +111,7 @@ var addPinCmd = &cmds.Command{
 		}
 
 		if !showProgress {
-			added, err := pinAddMany(req.Context, api, enc, req.Arguments, recursive)
+			added, err := pinAddMany(req.Context, api, enc, req.Arguments, recursive, name)
 			if err != nil {
 				return err
 			}
@@ -105,7 +129,7 @@ var addPinCmd = &cmds.Command{
 
 		ch := make(chan pinResult, 1)
 		go func() {
-			added, err := pinAddMany(ctx, api, enc, req.Arguments, recursive)
+			added, err := pinAddMany(ctx, api, enc, req.Arguments, recursive, name)
 			ch <- pinResult{pins: added, err: err}
 		}()
 
@@ -181,7 +205,7 @@ var addPinCmd = &cmds.Command{
 	},
 }
 
-func pinAddMany(ctx context.Context, api coreiface.CoreAPI, enc cidenc.Encoder, paths []string, recursive bool) ([]string, error) {
+func pinAddMany(ctx context.Context, api coreiface.CoreAPI, enc cidenc.Encoder, paths []string, recursive bool, name string) ([]string, error) {
 	added := make([]string, len(paths))
 	for i, b := range paths {
 		p, err := cmdutils.PathOrCidPath(b)
@@ -194,7 +218,7 @@ func pinAddMany(ctx context.Context, api coreiface.CoreAPI, enc cidenc.Encoder,
 			return nil, err
 		}
 
-		if err := api.Pin().Add(ctx, rp, options.Pin.Recursive(recursive)); err != nil {
+		if err := api.Pin().Add(ctx, rp, options.Pin.Recursive(recursive), options.Pin.Name(name)); err != nil {
 			return nil, err
 		}
 		added[i] = enc.Encode(rp.RootCid())
@@ -281,6 +305,7 @@ const (
 	pinTypeOptionName   = "type"
 	pinQuietOptionName  = "quiet"
 	pinStreamOptionName = "stream"
+	pinNamesOptionName  = "names"
 )
 
 var listPinCmd = &cmds.Command{
@@ -294,6 +319,7 @@ respectively.
 `,
 		LongDescription: `
 Returns a list of objects that are pinned locally.
+
 By default, all pinned objects are returned, but the '--type' flag or
 arguments can restrict that to a specific pin type or to some specific objects
 respectively.
@@ -302,10 +328,13 @@ Use --type=<type> to specify the type of pinned keys to list.
 Valid values are:
     * "direct": pin that specific object.
     * "recursive": pin that specific object, and indirectly pin all its
-    	descendants
+      descendants
     * "indirect": pinned indirectly by an ancestor (like a refcount)
     * "all"
 
+By default, pin names are not included (returned as empty).
+Pass '--names' flag to return pin names (set with '--name' from 'pin add').
+
 With arguments, the command fails if any of the arguments is not a pinned
 object. And if --type=<type> is additionally used, the command will also fail
 if any of the arguments is not of the specified type.
@@ -334,6 +363,7 @@ Example:
 		cmds.StringOption(pinTypeOptionName, "t", "The type of pinned keys to list. Can be \"direct\", \"indirect\", \"recursive\", or \"all\".").WithDefault("all"),
 		cmds.BoolOption(pinQuietOptionName, "q", "Write just hashes of objects."),
 		cmds.BoolOption(pinStreamOptionName, "s", "Enable streaming of pins as they are discovered."),
+		cmds.BoolOption(pinNamesOptionName, "n", "Enable displaying pin names (slower)."),
 	},
 	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
 		api, err := cmdenv.GetApi(env, req)
@@ -343,6 +373,7 @@ Example:
 
 		typeStr, _ := req.Options[pinTypeOptionName].(string)
 		stream, _ := req.Options[pinStreamOptionName].(bool)
+		displayNames, _ := req.Options[pinNamesOptionName].(bool)
 
 		switch typeStr {
 		case "all", "direct", "indirect", "recursive":
@@ -356,7 +387,7 @@ Example:
 		lgcList := map[string]PinLsType{}
 		if !stream {
 			emit = func(v PinLsOutputWrapper) error {
-				lgcList[v.PinLsObject.Cid] = PinLsType{Type: v.PinLsObject.Type}
+				lgcList[v.PinLsObject.Cid] = PinLsType{Type: v.PinLsObject.Type, Name: v.PinLsObject.Name}
 				return nil
 			}
 		} else {
@@ -368,7 +399,7 @@ Example:
 		if len(req.Arguments) > 0 {
 			err = pinLsKeys(req, typeStr, api, emit)
 		} else {
-			err = pinLsAll(req, typeStr, api, emit)
+			err = pinLsAll(req, typeStr, displayNames, api, emit)
 		}
 		if err != nil {
 			return err
@@ -402,8 +433,10 @@ Example:
 			if stream {
 				if quiet {
 					fmt.Fprintf(w, "%s\n", out.PinLsObject.Cid)
-				} else {
+				} else if out.PinLsObject.Name == "" {
 					fmt.Fprintf(w, "%s %s\n", out.PinLsObject.Cid, out.PinLsObject.Type)
+				} else {
+					fmt.Fprintf(w, "%s %s %s\n", out.PinLsObject.Cid, out.PinLsObject.Type, out.PinLsObject.Name)
 				}
 				return nil
 			}
@@ -411,8 +444,10 @@ Example:
 			for k, v := range out.PinLsList.Keys {
 				if quiet {
 					fmt.Fprintf(w, "%s\n", k)
-				} else {
+				} else if v.Name == "" {
 					fmt.Fprintf(w, "%s %s\n", k, v.Type)
+				} else {
+					fmt.Fprintf(w, "%s %s %s\n", k, v.Type, v.Name)
 				}
 			}
 
@@ -437,11 +472,13 @@ type PinLsList struct {
 // PinLsType contains the type of a pin
 type PinLsType struct {
 	Type string
+	Name string
 }
 
 // PinLsObject contains the description of a pin
 type PinLsObject struct {
 	Cid  string `json:",omitempty"`
+	Name string `json:",omitempty"`
 	Type string `json:",omitempty"`
 }
 
@@ -502,7 +539,7 @@ func pinLsKeys(req *cmds.Request, typeStr string, api coreiface.CoreAPI, emit fu
 	return nil
 }
 
-func pinLsAll(req *cmds.Request, typeStr string, api coreiface.CoreAPI, emit func(value PinLsOutputWrapper) error) error {
+func pinLsAll(req *cmds.Request, typeStr string, detailed bool, api coreiface.CoreAPI, emit func(value PinLsOutputWrapper) error) error {
 	enc, err := cmdenv.GetCidEncoder(req)
 	if err != nil {
 		return err
@@ -520,7 +557,7 @@ func pinLsAll(req *cmds.Request, typeStr string, api coreiface.CoreAPI, emit fun
 		panic("unhandled pin type")
 	}
 
-	pins, err := api.Pin().Ls(req.Context, opt)
+	pins, err := api.Pin().Ls(req.Context, opt, options.Pin.Ls.Detailed(detailed))
 	if err != nil {
 		return err
 	}
@@ -532,6 +569,7 @@ func pinLsAll(req *cmds.Request, typeStr string, api coreiface.CoreAPI, emit fun
 		err = emit(PinLsOutputWrapper{
 			PinLsObject: PinLsObject{
 				Type: p.Type(),
+				Name: p.Name(),
 				Cid:  enc.Encode(p.Path().RootCid()),
 			},
 		})
@@ -748,15 +786,15 @@ func pinVerify(ctx context.Context, n *core.IpfsNode, opts pinVerifyOpts, enc ci
 	out := make(chan any)
 	go func() {
 		defer close(out)
-		for p := range n.Pinning.RecursiveKeys(ctx) {
+		for p := range n.Pinning.RecursiveKeys(ctx, false) {
 			if p.Err != nil {
 				out <- PinVerifyRes{Err: p.Err.Error()}
 				return
 			}
-			pinStatus := checkPin(p.C)
+			pinStatus := checkPin(p.Pin.Key)
 			if !pinStatus.Ok || opts.includeOk {
 				select {
-				case out <- PinVerifyRes{Cid: enc.Encode(p.C), PinStatus: pinStatus}:
+				case out <- PinVerifyRes{Cid: enc.Encode(p.Pin.Key), PinStatus: pinStatus}:
 				case <-ctx.Done():
 					return
 				}
diff --git a/core/coreapi/block.go b/core/coreapi/block.go
index 0c5597c8dcffe5a8ca1ba58d002a83ba43fb12fa..b386ecd0a8e28dc1c1a85bcec617e2d8fbce5721 100644
--- a/core/coreapi/block.go
+++ b/core/coreapi/block.go
@@ -60,7 +60,7 @@ func (api *BlockAPI) Put(ctx context.Context, src io.Reader, opts ...caopts.Bloc
 	}
 
 	if settings.Pin {
-		if err = api.pinning.PinWithMode(ctx, b.Cid(), pin.Recursive); err != nil {
+		if err = api.pinning.PinWithMode(ctx, b.Cid(), pin.Recursive, ""); err != nil {
 			return nil, err
 		}
 		if err := api.pinning.Flush(ctx); err != nil {
diff --git a/core/coreapi/dag.go b/core/coreapi/dag.go
index c4d0dc9d20eb357fccd4d9642f3db8af4aca5d2a..70686f62e03f2473dda823101b2d8b3a6b81a958 100644
--- a/core/coreapi/dag.go
+++ b/core/coreapi/dag.go
@@ -30,7 +30,7 @@ func (adder *pinningAdder) Add(ctx context.Context, nd ipld.Node) error {
 		return err
 	}
 
-	if err := adder.pinning.PinWithMode(ctx, nd.Cid(), pin.Recursive); err != nil {
+	if err := adder.pinning.PinWithMode(ctx, nd.Cid(), pin.Recursive, ""); err != nil {
 		return err
 	}
 
@@ -51,7 +51,7 @@ func (adder *pinningAdder) AddMany(ctx context.Context, nds []ipld.Node) error {
 	for _, nd := range nds {
 		c := nd.Cid()
 		if cids.Visit(c) {
-			if err := adder.pinning.PinWithMode(ctx, c, pin.Recursive); err != nil {
+			if err := adder.pinning.PinWithMode(ctx, c, pin.Recursive, ""); err != nil {
 				return err
 			}
 		}
diff --git a/core/coreapi/object.go b/core/coreapi/object.go
index e8f94b1d5d45c67cd69d995895d7d9c70de621b4..fca98bc5fa4ab50d851d20d6bcf89dc060da0561 100644
--- a/core/coreapi/object.go
+++ b/core/coreapi/object.go
@@ -133,7 +133,7 @@ func (api *ObjectAPI) Put(ctx context.Context, src io.Reader, opts ...caopts.Obj
 	}
 
 	if options.Pin {
-		if err := api.pinning.PinWithMode(ctx, dagnode.Cid(), pin.Recursive); err != nil {
+		if err := api.pinning.PinWithMode(ctx, dagnode.Cid(), pin.Recursive, ""); err != nil {
 			return path.ImmutablePath{}, err
 		}
 
diff --git a/core/coreapi/pin.go b/core/coreapi/pin.go
index 5cb92a81924283017a54116b3e47081f42393f0a..8db582a4ffa9dd14fee79f474073533c0af1aa82 100644
--- a/core/coreapi/pin.go
+++ b/core/coreapi/pin.go
@@ -38,7 +38,7 @@ func (api *PinAPI) Add(ctx context.Context, p path.Path, opts ...caopts.PinAddOp
 
 	defer api.blockstore.PinLock(ctx).Unlock(ctx)
 
-	err = api.pinning.Pin(ctx, dagNode, settings.Recursive)
+	err = api.pinning.Pin(ctx, dagNode, settings.Recursive, settings.Name)
 	if err != nil {
 		return fmt.Errorf("pin: %s", err)
 	}
@@ -67,7 +67,7 @@ func (api *PinAPI) Ls(ctx context.Context, opts ...caopts.PinLsOption) (<-chan c
 		return nil, fmt.Errorf("invalid type '%s', must be one of {direct, indirect, recursive, all}", settings.Type)
 	}
 
-	return api.pinLsAll(ctx, settings.Type), nil
+	return api.pinLsAll(ctx, settings.Type, settings.Detailed), nil
 }
 
 func (api *PinAPI) IsPinned(ctx context.Context, p path.Path, opts ...caopts.PinIsPinnedOption) (string, bool, error) {
@@ -231,12 +231,12 @@ func (api *PinAPI) Verify(ctx context.Context) (<-chan coreiface.PinStatus, erro
 	out := make(chan coreiface.PinStatus)
 	go func() {
 		defer close(out)
-		for p := range api.pinning.RecursiveKeys(ctx) {
+		for p := range api.pinning.RecursiveKeys(ctx, false) {
 			var res *pinStatus
 			if p.Err != nil {
 				res = &pinStatus{err: p.Err}
 			} else {
-				res = checkPin(p.C)
+				res = checkPin(p.Pin.Key)
 			}
 			select {
 			case <-ctx.Done():
@@ -252,6 +252,7 @@ func (api *PinAPI) Verify(ctx context.Context) (<-chan coreiface.PinStatus, erro
 type pinInfo struct {
 	pinType string
 	path    path.ImmutablePath
+	name    string
 	err     error
 }
 
@@ -263,6 +264,10 @@ func (p *pinInfo) Type() string {
 	return p.pinType
 }
 
+func (p *pinInfo) Name() string {
+	return p.name
+}
+
 func (p *pinInfo) Err() error {
 	return p.err
 }
@@ -271,16 +276,17 @@ func (p *pinInfo) Err() error {
 //
 // The caller must keep reading results until the channel is closed to prevent
 // leaking the goroutine that is fetching pins.
-func (api *PinAPI) pinLsAll(ctx context.Context, typeStr string) <-chan coreiface.Pin {
+func (api *PinAPI) pinLsAll(ctx context.Context, typeStr string, detailed bool) <-chan coreiface.Pin {
 	out := make(chan coreiface.Pin, 1)
 
 	emittedSet := cid.NewSet()
 
-	AddToResultKeys := func(c cid.Cid, typeStr string) error {
+	AddToResultKeys := func(c cid.Cid, name, typeStr string) error {
 		if emittedSet.Visit(c) {
 			select {
 			case out <- &pinInfo{
 				pinType: typeStr,
+				name:    name,
 				path:    path.FromCid(c),
 			}:
 			case <-ctx.Done():
@@ -296,25 +302,25 @@ func (api *PinAPI) pinLsAll(ctx context.Context, typeStr string) <-chan coreifac
 		var rkeys []cid.Cid
 		var err error
 		if typeStr == "recursive" || typeStr == "all" {
-			for streamedCid := range api.pinning.RecursiveKeys(ctx) {
+			for streamedCid := range api.pinning.RecursiveKeys(ctx, detailed) {
 				if streamedCid.Err != nil {
 					out <- &pinInfo{err: streamedCid.Err}
 					return
 				}
-				if err = AddToResultKeys(streamedCid.C, "recursive"); err != nil {
+				if err = AddToResultKeys(streamedCid.Pin.Key, streamedCid.Pin.Name, "recursive"); err != nil {
 					out <- &pinInfo{err: err}
 					return
 				}
-				rkeys = append(rkeys, streamedCid.C)
+				rkeys = append(rkeys, streamedCid.Pin.Key)
 			}
 		}
 		if typeStr == "direct" || typeStr == "all" {
-			for streamedCid := range api.pinning.DirectKeys(ctx) {
+			for streamedCid := range api.pinning.DirectKeys(ctx, detailed) {
 				if streamedCid.Err != nil {
 					out <- &pinInfo{err: streamedCid.Err}
 					return
 				}
-				if err = AddToResultKeys(streamedCid.C, "direct"); err != nil {
+				if err = AddToResultKeys(streamedCid.Pin.Key, streamedCid.Pin.Name, "direct"); err != nil {
 					out <- &pinInfo{err: err}
 					return
 				}
@@ -324,21 +330,21 @@ func (api *PinAPI) pinLsAll(ctx context.Context, typeStr string) <-chan coreifac
 			// We need to first visit the direct pins that have priority
 			// without emitting them
 
-			for streamedCid := range api.pinning.DirectKeys(ctx) {
+			for streamedCid := range api.pinning.DirectKeys(ctx, detailed) {
 				if streamedCid.Err != nil {
 					out <- &pinInfo{err: streamedCid.Err}
 					return
 				}
-				emittedSet.Add(streamedCid.C)
+				emittedSet.Add(streamedCid.Pin.Key)
 			}
 
-			for streamedCid := range api.pinning.RecursiveKeys(ctx) {
+			for streamedCid := range api.pinning.RecursiveKeys(ctx, detailed) {
 				if streamedCid.Err != nil {
 					out <- &pinInfo{err: streamedCid.Err}
 					return
 				}
-				emittedSet.Add(streamedCid.C)
-				rkeys = append(rkeys, streamedCid.C)
+				emittedSet.Add(streamedCid.Pin.Key)
+				rkeys = append(rkeys, streamedCid.Pin.Key)
 			}
 		}
 		if typeStr == "indirect" || typeStr == "all" {
@@ -353,7 +359,7 @@ func (api *PinAPI) pinLsAll(ctx context.Context, typeStr string) <-chan coreifac
 						if emittedSet.Has(c) {
 							return true // skipped
 						}
-						err := AddToResultKeys(c, "indirect")
+						err := AddToResultKeys(c, "", "indirect")
 						if err != nil {
 							out <- &pinInfo{err: err}
 							return false
diff --git a/core/coreiface/options/pin.go b/core/coreiface/options/pin.go
index 75c2b8a26347608d5f386f413045b143aaaafa97..0efd853ef22529ec684b9730574726a40c47d083 100644
--- a/core/coreiface/options/pin.go
+++ b/core/coreiface/options/pin.go
@@ -5,11 +5,13 @@ import "fmt"
 // PinAddSettings represent the settings for PinAPI.Add
 type PinAddSettings struct {
 	Recursive bool
+	Name      string
 }
 
 // PinLsSettings represent the settings for PinAPI.Ls
 type PinLsSettings struct {
-	Type string
+	Type     string
+	Detailed bool
 }
 
 // PinIsPinnedSettings represent the settings for PinAPI.IsPinned
@@ -194,6 +196,15 @@ func (pinLsOpts) pinType(t string) PinLsOption {
 	}
 }
 
+// Detailed is an option for [Pin.Ls] which sets whether or not to return
+// detailed information, such as pin names and modes.
+func (pinLsOpts) Detailed(detailed bool) PinLsOption {
+	return func(settings *PinLsSettings) error {
+		settings.Detailed = detailed
+		return nil
+	}
+}
+
 type pinIsPinnedOpts struct{}
 
 // All is an option for Pin.IsPinned which will make it search in all type of pins.
@@ -263,6 +274,14 @@ func (pinOpts) Recursive(recursive bool) PinAddOption {
 	}
 }
 
+// Name is an option for Pin.Add which specifies an optional name to add to the pin.
+func (pinOpts) Name(name string) PinAddOption {
+	return func(settings *PinAddSettings) error {
+		settings.Name = name
+		return nil
+	}
+}
+
 // RmRecursive is an option for Pin.Rm which specifies whether to recursively
 // unpin the object linked to by the specified object(s). This does not remove
 // indirect pins referenced by other recursive pins.
diff --git a/core/coreiface/pin.go b/core/coreiface/pin.go
index 25a775965fb7e28158f6b65dc61c57c520c930a4..ed837fc9ce254cd891cb24e8e6422eb13f84184a 100644
--- a/core/coreiface/pin.go
+++ b/core/coreiface/pin.go
@@ -13,6 +13,9 @@ type Pin interface {
 	// Path to the pinned object
 	Path() path.ImmutablePath
 
+	// Name is the name of the pin.
+	Name() string
+
 	// Type of the pin
 	Type() string
 
diff --git a/core/coreunix/add.go b/core/coreunix/add.go
index 83bec6d03b77b62cc3dd97a893605cae6b67f1f1..a8d7e5982f0cd11a4a7568023f60584c4d429df3 100644
--- a/core/coreunix/add.go
+++ b/core/coreunix/add.go
@@ -186,7 +186,7 @@ func (adder *Adder) PinRoot(ctx context.Context, root ipld.Node) error {
 		adder.tempRoot = rnk
 	}
 
-	err = adder.pinning.PinWithMode(ctx, rnk, pin.Recursive)
+	err = adder.pinning.PinWithMode(ctx, rnk, pin.Recursive, "")
 	if err != nil {
 		return err
 	}
diff --git a/docs/changelogs/v0.26.md b/docs/changelogs/v0.26.md
index 3a9f088cac62812aad0ccde1ac05e7107899bbdb..9eddcfd8595f5c2eee91af8cb88e81f65da0c76e 100644
--- a/docs/changelogs/v0.26.md
+++ b/docs/changelogs/v0.26.md
@@ -7,6 +7,7 @@
 - [Overview](#overview)
 - [๐Ÿ”ฆ Highlights](#-highlights)
   - [Several deprecated commands have been removed](#several-deprecated-commands-have-been-removed)
+  - [Support optional pin names](#support-optional-pin-names)
 - [๐Ÿ“ Changelog](#-changelog)
 - [๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Contributors](#-contributors)
 
@@ -30,6 +31,10 @@ Several deprecated commands have been removed:
 - `ipfs dns` deprecated in [April 2022, Kubo 0.13](https://github.com/ipfs/kubo/commit/76ae33a9f3f9abd166d1f6f23d6a8a0511510e3c), use `ipfs resolve /ipns/{name}` instead.
 - `ipfs tar` deprecated [April 2022, Kubo 0.13](https://github.com/ipfs/kubo/pull/8849)
 
+#### Support optional pin names
+
+You can now add a name to a pin when pinning a CID. To do so, use `ipfs pin add --name "Some Name" bafy...`. You can list your pins, including their names, with `ipfs pin ls --names`.
+
 ### ๐Ÿ“ Changelog
 
 - Export a `kubo.Start` function so users can programmatically start Kubo from within a go program.
diff --git a/docs/examples/kubo-as-a-library/go.mod b/docs/examples/kubo-as-a-library/go.mod
index 34b101c9a0a3ac18cf7cee27917120eeae9937eb..b669171fd7d45f6c33584e6a21d255a6439483d2 100644
--- a/docs/examples/kubo-as-a-library/go.mod
+++ b/docs/examples/kubo-as-a-library/go.mod
@@ -7,7 +7,7 @@ go 1.20
 replace github.com/ipfs/kubo => ./../../..
 
 require (
-	github.com/ipfs/boxo v0.16.0
+	github.com/ipfs/boxo v0.16.1-0.20240104124825-38fb74f76b0d
 	github.com/ipfs/kubo v0.0.0-00010101000000-000000000000
 	github.com/libp2p/go-libp2p v0.32.2
 	github.com/multiformats/go-multiaddr v0.12.0
diff --git a/docs/examples/kubo-as-a-library/go.sum b/docs/examples/kubo-as-a-library/go.sum
index 9d63ebb13d0bcf7d0c8383fae0aa9051107e4c1b..c348ddd60d150ec3a2211dae5fbb58c77f867e11 100644
--- a/docs/examples/kubo-as-a-library/go.sum
+++ b/docs/examples/kubo-as-a-library/go.sum
@@ -303,8 +303,8 @@ github.com/ipfs-shipyard/nopfs/ipfs v0.13.2-0.20231027223058-cde3b5ba964c h1:7Uy
 github.com/ipfs-shipyard/nopfs/ipfs v0.13.2-0.20231027223058-cde3b5ba964c/go.mod h1:6EekK/jo+TynwSE/ZOiOJd4eEvRXoavEC3vquKtv4yI=
 github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
 github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
-github.com/ipfs/boxo v0.16.0 h1:A9dUmef5a+mEFki6kbyG7el5gl65CiUBzrDeZxzTWKY=
-github.com/ipfs/boxo v0.16.0/go.mod h1:jAgpNQn7T7BnibUeReXcKU9Ha1xmYNyOlwVEl193ow0=
+github.com/ipfs/boxo v0.16.1-0.20240104124825-38fb74f76b0d h1:7tDzalCLHYr4tzrjNrfqZvIki2MEBSrpLBdc0bssDZk=
+github.com/ipfs/boxo v0.16.1-0.20240104124825-38fb74f76b0d/go.mod h1:jAgpNQn7T7BnibUeReXcKU9Ha1xmYNyOlwVEl193ow0=
 github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA=
 github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU=
 github.com/ipfs/go-block-format v0.0.2/go.mod h1:AWR46JfpcObNfg3ok2JHDUfdiHRgWhJgCQF+KIgOPJY=
diff --git a/fuse/ipns/common.go b/fuse/ipns/common.go
index 7306196c8c625f27609b52e6e8c706a42f779467..d220867766afd2c36f5c6718ff0e5c057970eeb1 100644
--- a/fuse/ipns/common.go
+++ b/fuse/ipns/common.go
@@ -18,7 +18,7 @@ func InitializeKeyspace(n *core.IpfsNode, key ci.PrivKey) error {
 
 	emptyDir := ft.EmptyDirNode()
 
-	err := n.Pinning.Pin(ctx, emptyDir, false)
+	err := n.Pinning.Pin(ctx, emptyDir, false, "")
 	if err != nil {
 		return err
 	}
diff --git a/gc/gc.go b/gc/gc.go
index c85f9d6bf24dca0b7080459fcf8ae259db89044d..51df59e54089a0836e7f050d1bf47cbb2cf926e5 100644
--- a/gc/gc.go
+++ b/gc/gc.go
@@ -154,7 +154,7 @@ func GC(ctx context.Context, bs bstore.GCBlockstore, dstor dstore.Datastore, pn
 // Descendants recursively finds all the descendants of the given roots and
 // adds them to the given cid.Set, using the provided dag.GetLinks function
 // to walk the tree.
-func Descendants(ctx context.Context, getLinks dag.GetLinks, set *cid.Set, roots <-chan pin.StreamedCid) error {
+func Descendants(ctx context.Context, getLinks dag.GetLinks, set *cid.Set, roots <-chan pin.StreamedPin) error {
 	verifyGetLinks := func(ctx context.Context, c cid.Cid) ([]*ipld.Link, error) {
 		err := verifcid.ValidateCid(verifcid.DefaultAllowlist, c)
 		if err != nil {
@@ -188,7 +188,7 @@ func Descendants(ctx context.Context, getLinks dag.GetLinks, set *cid.Set, roots
 			}
 
 			// Walk recursively walks the dag and adds the keys to the given set
-			err := dag.Walk(ctx, verifyGetLinks, wrapper.C, func(k cid.Cid) bool {
+			err := dag.Walk(ctx, verifyGetLinks, wrapper.Pin.Key, func(k cid.Cid) bool {
 				return set.Visit(toCidV1(k))
 			}, dag.Concurrent())
 			if err != nil {
@@ -226,7 +226,7 @@ func ColoredSet(ctx context.Context, pn pin.Pinner, ng ipld.NodeGetter, bestEffo
 		}
 		return links, nil
 	}
-	rkeys := pn.RecursiveKeys(ctx)
+	rkeys := pn.RecursiveKeys(ctx, false)
 	err := Descendants(ctx, getLinks, gcs, rkeys)
 	if err != nil {
 		errors = true
@@ -249,14 +249,14 @@ func ColoredSet(ctx context.Context, pn pin.Pinner, ng ipld.NodeGetter, bestEffo
 		}
 		return links, nil
 	}
-	bestEffortRootsChan := make(chan pin.StreamedCid)
+	bestEffortRootsChan := make(chan pin.StreamedPin)
 	go func() {
 		defer close(bestEffortRootsChan)
 		for _, root := range bestEffortRoots {
 			select {
 			case <-ctx.Done():
 				return
-			case bestEffortRootsChan <- pin.StreamedCid{C: root}:
+			case bestEffortRootsChan <- pin.StreamedPin{Pin: pin.Pinned{Key: root}}:
 			}
 		}
 	}()
@@ -270,15 +270,15 @@ func ColoredSet(ctx context.Context, pn pin.Pinner, ng ipld.NodeGetter, bestEffo
 		}
 	}
 
-	dkeys := pn.DirectKeys(ctx)
+	dkeys := pn.DirectKeys(ctx, false)
 	for k := range dkeys {
 		if k.Err != nil {
 			return nil, k.Err
 		}
-		gcs.Add(toCidV1(k.C))
+		gcs.Add(toCidV1(k.Pin.Key))
 	}
 
-	ikeys := pn.InternalPins(ctx)
+	ikeys := pn.InternalPins(ctx, false)
 	err = Descendants(ctx, getLinks, gcs, ikeys)
 	if err != nil {
 		errors = true
diff --git a/gc/gc_test.go b/gc/gc_test.go
index 4fb6dbf09be65c3ee642e24be58c47e7fb9c3e69..c5d00714d588316976d4c35cfb19967b251e542d 100644
--- a/gc/gc_test.go
+++ b/gc/gc_test.go
@@ -38,14 +38,14 @@ func TestGC(t *testing.T) {
 		// direct
 		root, _, err := daggen.MakeDagNode(dserv.Add, 0, 1)
 		require.NoError(t, err)
-		err = pinner.PinWithMode(ctx, root, pin.Direct)
+		err = pinner.PinWithMode(ctx, root, pin.Direct, "")
 		require.NoError(t, err)
 		expectedKept = append(expectedKept, root.Hash())
 
 		// recursive
 		root, allCids, err := daggen.MakeDagNode(dserv.Add, 5, 2)
 		require.NoError(t, err)
-		err = pinner.PinWithMode(ctx, root, pin.Recursive)
+		err = pinner.PinWithMode(ctx, root, pin.Recursive, "")
 		require.NoError(t, err)
 		expectedKept = append(expectedKept, toMHs(allCids)...)
 	}
diff --git a/go.mod b/go.mod
index 74b4cd3479dfe9fddb38f9ccd48ef2da498b4626..b9fba0309983675582ca0ec6572d57704f1af53c 100644
--- a/go.mod
+++ b/go.mod
@@ -17,7 +17,7 @@ require (
 	github.com/hashicorp/go-multierror v1.1.1
 	github.com/ipfs-shipyard/nopfs v0.0.12-0.20231027223058-cde3b5ba964c
 	github.com/ipfs-shipyard/nopfs/ipfs v0.13.2-0.20231027223058-cde3b5ba964c
-	github.com/ipfs/boxo v0.16.0
+	github.com/ipfs/boxo v0.16.1-0.20240104124825-38fb74f76b0d
 	github.com/ipfs/go-block-format v0.2.0
 	github.com/ipfs/go-cid v0.4.1
 	github.com/ipfs/go-cidutil v0.1.0
diff --git a/go.sum b/go.sum
index f69fbedd031ac67ed30b1b793bf06d087acb8cb9..8cae85a9f836cabcc58cdf5eec463b2ca742fbd1 100644
--- a/go.sum
+++ b/go.sum
@@ -337,8 +337,8 @@ github.com/ipfs-shipyard/nopfs/ipfs v0.13.2-0.20231027223058-cde3b5ba964c h1:7Uy
 github.com/ipfs-shipyard/nopfs/ipfs v0.13.2-0.20231027223058-cde3b5ba964c/go.mod h1:6EekK/jo+TynwSE/ZOiOJd4eEvRXoavEC3vquKtv4yI=
 github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
 github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
-github.com/ipfs/boxo v0.16.0 h1:A9dUmef5a+mEFki6kbyG7el5gl65CiUBzrDeZxzTWKY=
-github.com/ipfs/boxo v0.16.0/go.mod h1:jAgpNQn7T7BnibUeReXcKU9Ha1xmYNyOlwVEl193ow0=
+github.com/ipfs/boxo v0.16.1-0.20240104124825-38fb74f76b0d h1:7tDzalCLHYr4tzrjNrfqZvIki2MEBSrpLBdc0bssDZk=
+github.com/ipfs/boxo v0.16.1-0.20240104124825-38fb74f76b0d/go.mod h1:jAgpNQn7T7BnibUeReXcKU9Ha1xmYNyOlwVEl193ow0=
 github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA=
 github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU=
 github.com/ipfs/go-bitswap v0.11.0 h1:j1WVvhDX1yhG32NTC9xfxnqycqYIlhzEzLXG/cU1HyQ=
diff --git a/test/cli/pins_test.go b/test/cli/pins_test.go
index 8a36d469523bc8d6969ccb652dc13f29784c5f64..a57f1b89b0846949505cc525c422a66ef71b862a 100644
--- a/test/cli/pins_test.go
+++ b/test/cli/pins_test.go
@@ -209,4 +209,67 @@ func TestPins(t *testing.T) {
 		testPins(t, testPinsArgs{runDaemon: true, baseArg: "--cid-base=base32"})
 		testPins(t, testPinsArgs{runDaemon: true, lsArg: "--stream", baseArg: "--cid-base=base32"})
 	})
+
+	t.Run("test pinning with names cli text output", func(t *testing.T) {
+		t.Parallel()
+
+		node := harness.NewT(t).NewNode().Init()
+		cidAStr := node.IPFSAddStr(RandomStr(1000), "--pin=false")
+		cidBStr := node.IPFSAddStr(RandomStr(1000), "--pin=false")
+
+		_ = node.IPFS("pin", "add", "--name", "testPin", cidAStr)
+
+		outARegular := cidAStr + " recursive"
+		outADetailed := outARegular + " testPin"
+		outBRegular := cidBStr + " recursive"
+		outBDetailed := outBRegular + " testPin"
+
+		pinLs := func(args ...string) []string {
+			return strings.Split(node.IPFS(StrCat("pin", "ls", args)...).Stdout.Trimmed(), "\n")
+		}
+
+		lsOut := pinLs("-t=recursive")
+		require.Contains(t, lsOut, outARegular)
+		require.NotContains(t, lsOut, outADetailed)
+
+		lsOut = pinLs("-t=recursive", "--names")
+		require.Contains(t, lsOut, outADetailed)
+		require.NotContains(t, lsOut, outARegular)
+
+		_ = node.IPFS("pin", "update", cidAStr, cidBStr)
+		lsOut = pinLs("-t=recursive", "--names")
+		require.Contains(t, lsOut, outBDetailed)
+		require.NotContains(t, lsOut, outADetailed)
+	})
+
+	// JSON that is also the wire format of /api/v0
+	t.Run("test pinning with names json output", func(t *testing.T) {
+		t.Parallel()
+
+		node := harness.NewT(t).NewNode().Init()
+		cidAStr := node.IPFSAddStr(RandomStr(1000), "--pin=false")
+		cidBStr := node.IPFSAddStr(RandomStr(1000), "--pin=false")
+
+		_ = node.IPFS("pin", "add", "--name", "testPinJson", cidAStr)
+
+		outARegular := `"` + cidAStr + `":{"Type":"recursive"`
+		outADetailed := outARegular + `,"Name":"testPinJson"`
+		outBRegular := `"` + cidBStr + `":{"Type":"recursive"`
+		outBDetailed := outBRegular + `,"Name":"testPinJson"`
+
+		pinLs := func(args ...string) string {
+			return node.IPFS(StrCat("pin", "ls", "--enc=json", args)...).Stdout.Trimmed()
+		}
+
+		lsOut := pinLs("-t=recursive")
+		require.Contains(t, lsOut, outARegular)
+		require.NotContains(t, lsOut, outADetailed)
+
+		lsOut = pinLs("-t=recursive", "--names")
+		require.Contains(t, lsOut, outADetailed)
+
+		_ = node.IPFS("pin", "update", cidAStr, cidBStr)
+		lsOut = pinLs("-t=recursive", "--names")
+		require.Contains(t, lsOut, outBDetailed)
+	})
 }
diff --git a/test/dependencies/go.mod b/test/dependencies/go.mod
index e55e9f10c4871fc9ad89e870fc3da8129e1e3c70..2cb03508ae5fdea386dfde2ac0993e2cf0937da9 100644
--- a/test/dependencies/go.mod
+++ b/test/dependencies/go.mod
@@ -103,7 +103,7 @@ require (
 	github.com/hexops/gotextdiff v1.0.3 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/ipfs/bbloom v0.0.4 // indirect
-	github.com/ipfs/boxo v0.16.0 // indirect
+	github.com/ipfs/boxo v0.16.1-0.20240104124825-38fb74f76b0d // indirect
 	github.com/ipfs/go-block-format v0.2.0 // indirect
 	github.com/ipfs/go-cid v0.4.1 // indirect
 	github.com/ipfs/go-datastore v0.6.0 // indirect
diff --git a/test/dependencies/go.sum b/test/dependencies/go.sum
index 35f92c1a80486474a37b16af36ddc9deedd2e757..8681e257c4d6eff6f2623d28a469964db99f37f0 100644
--- a/test/dependencies/go.sum
+++ b/test/dependencies/go.sum
@@ -342,8 +342,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
 github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
-github.com/ipfs/boxo v0.16.0 h1:A9dUmef5a+mEFki6kbyG7el5gl65CiUBzrDeZxzTWKY=
-github.com/ipfs/boxo v0.16.0/go.mod h1:jAgpNQn7T7BnibUeReXcKU9Ha1xmYNyOlwVEl193ow0=
+github.com/ipfs/boxo v0.16.1-0.20240104124825-38fb74f76b0d h1:7tDzalCLHYr4tzrjNrfqZvIki2MEBSrpLBdc0bssDZk=
+github.com/ipfs/boxo v0.16.1-0.20240104124825-38fb74f76b0d/go.mod h1:jAgpNQn7T7BnibUeReXcKU9Ha1xmYNyOlwVEl193ow0=
 github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs=
 github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM=
 github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=