This page attempts to collect some principles used in the Tcl C API.
Always read the documentation instead of relying purely on the principles here.
Tcl's public API symbols start with Tcl_ or TCL_. We will never warn anyone about creating new symbols in that space.
If a function can fail, it either returns a Tcl result code (TCL_OK, TCL_ERROR, etc.) or a NULLable pointer. In the latter case, it always documents that this is the case. Almost all cases that produce errors will take an interpreter that will be used to hold an error message; if the interpreter is not also needed for context, it will be legal to pass a NULL in its place so that no error message is generated (though always at the cost of losing detail of what the failure was).
Basic memory allocation functions can never fail (they panic instead) unless they contain “Attempt” in their name; the attempting allocators _can_ fail, and their use is encouraged when dealing with large amounts of data (such as big image buffers).
Lookup functions often return a Tcl_Obj* instead of a Tcl result code. Their failure result is always a NULL, which can include “there is no such thing available”. A Tcl_Obj never encodes a NULL. (Logically, NULL is an absence of value, and does not belong to the space of strings; since everything is a string in Tcl, NULL is consequently not in Tcl.
Non-Tcl_Obj arguments or results are never transferred ownership unless the function is explicitly documented to do so. (Typically, ownership transferring functions are internal to Tcl's implementation.)
Tcl_Obj values are generally shared ownership. Tcl_IsShared will say more specifically if a particular object is shared. The allocation functions return unshared _zero-ref-count_ objects. Functions that modify an object require that it is unshared. It's usually obvious which functions are doing modification and which are taking ownership, but it is not always; in particular, some functions sometimes take ownership.
Let's consider a real example: Tcl_DictObjPut.
int Tcl_DictObjPut(interp, dictPtr, keyPtr, valuePtr);
This returns a Tcl result code and takes a (NULLable) interpreter; it can fail, and it can be told to fail quietly.
The dictPtr argument is the value being modified: it must be unshared (refcount 0 or 1; zero means newly-allocated, one means only one owner). Note that with a newly-allocated value, you can be sure from your own code whether things will fail or not; the value has not left the custody of your C code yet.
The valuePtr is always going to have its ownership taken by the dictionary unless there is an error. If you know that dictPtr is definitely a valid dictionary, the value is always going to be taken control of, and it is safe to pass a zero refcount Tcl_Obj here. If failure is a possibility (e.g., the dictionary has come from somewhere where anything could have treated it as a non-dictionary, and it has not yet been successfully used as a dictionary by some dict-API function since then) then you must make sure that you increment the reference count of the value before passing it in, and decrement it afterwards.
The keyPtr is more complex. It might have ownership taken or it might not; it depends on whether the key already existed in the dictionary (this is something you can often know in your own code). If you do not know for sure whether the key exists, the key must be passed in with a reference count of at least 1; it is only safe to use a zero refcount key when you know that the dictionary is a real dictionary and you know that the key is absent. This typically restricts doing this to initial building of the dictionary.