**EDIT please see edit below** Here's how I've opted to do it. Please do submit alternative answers if there's a better way. Use the [civicrm_post hook](https://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_post) like this: **Note that you need to replace `N` with your custom field's ID number** function myext_civicrm_post($op, $objectName, $objectId, &$objectRef) { if ($objectId && $op == 'create' && $objectName == 'Contribution') { // See if we have a field that belongs to the fieldset. $result = civicrm_api3('CustomValue', 'get', [ 'sequential' => 1, 'entity_id' => $objectId, 'entity_type' => "Contribution", 'return.custom_N' => 1, ]); if ($result['count'] == 0) { // Create the default entry. $result = civicrm_api3('CustomValue', 'create', [ 'sequential' => 1, 'entity_id' => $contribution_id, 'entity_type' => "Contribution", 'custom_N' => 'unknown', // custom_M ... ]); } } } Then when you create a Contribution, the defaults are properly set. ## Interesting observations My interest was in creating the default record when the contribution was created by the API. But I thought I'd look into how this works with using the UI too. If you use the web UI to create a contribution then the code above gets in before the main process creates/sets the custom values. i.e. This is the order of things: 1. User creates a contribution record. 2. Contribution record created. 3. Our hook runs, no custom value exists so it creates the default record. 4. The user-entered values (inc. the defaults that were set up in the form) overwrite the default created by our hook. The same process happens when you use the API to create a contribution but include values for the custom data also. ## EDIT The above worked fine until sometime around CiviCRM version 5.18 at which point it stopped working. It stopped because during creation of a contribution this code would run, creating a custom data record, then the calling code would also try to create the same custom data record - causing a database error. I found that I could acheive what I had been trying to do as follows: function myext_civicrm_post($op, $objectName, $objectId, &$objectRef) { if ($objectId && $op == 'create' && $objectName == 'Contribution') { CRM_Core_Transaction::addCallback( CRM_Core_Transaction::PHASE_POST_COMMIT, 'myfixercallback', [$objectId]); } Then `myfixercallback` would do the same work. **However!** it seems that CiviCRM - at least from 5.19, possibly earlier, now properly creates default custom data. So this hack is simply not needed any more.