**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.