How to Create or Update SendGrid Contact Info with Custom Fields

SendGrid has some nice features, though the documentation can, in some cases, be woefully out of date or lacking decent examples. One of the platform’s weaknesses is custom fields for contact info. So, how do you properly create or update SendGrid contact info that has custom fields?

The API docs for contacts does tell us the right structure: an object with an array of contact objects, each of these having the reserved fields and a custom fields object. Unfortunately, it gives no indication of what that should look like.

Logically, one would assume that whatever we name our custom fields, then, like the reserved fields, we would just provide that name with the value. This is how it should work, and, in fact, that’s how you can query for a contact using a custom field. But for some reason, SendGrid decided that it would be better for the user to provide the ID of the custom field when creating/updating a contact. It would be great if the web portal showed you those IDs, but nope. The only way to get those is to query for them.

Requesting Custom Fields

The following example is for maintaining each contact’s donations and if they are subscribed to a newsletter. This would be useful for tailoring emails to specific users.

There are two ways you can do this. Either you never expect your custom fields to change, in which case you can query them once and store that in code. Or, you’ll need to request them every so often and store them in a table or by some other means. I’m personally going with the former, as it’s simpler, but the process is similar.

Below is the curl command and what the resulting response data looks like.

curl --request GET \
  --url \
  --header 'authorization: Bearer <<YOUR_SENDGRID_API_KEY_HERE>>' \
  --data '{}'

    "custom_fields": [
        {"id": "e1_T", "name": "user_id", "field_type": "Text", "_metadata": {"self": ""}},
        {"id": "e2_T", "name": "username", "field_type": "Text", "_metadata": {"self": ""}},
        {"id": "e3_T", "name": "type", "field_type": "Text", "_metadata": {"self": ""}},
        {"id": "e4_N", "name": "donations_amount", "field_type": "Number", "_metadata": {"self": ""}},
        {"id": "e5_N", "name": "donations_count", "field_type": "Number", "_metadata": {"self": ""}},
        {"id": "e6_T", "name": "newsletter_subscribed", "field_type": "Text", "_metadata": {"self": ""}},
    "reserved_fields": [
        {"id": "_rf0_T", "name": "first_name", "field_type": "Text"},
        {"id": "_rf1_T", "name": "last_name", "field_type": "Text"},
        {"id": "_rf2_T", "name": "email", "field_type": "Text"},
        {"id": "_rf3_T", "name": "alternate_emails", "field_type": "Text"},
        {"id": "_rf4_T", "name": "address_line_1", "field_type": "Text"},
        {"id": "_rf5_T", "name": "address_line_2", "field_type": "Text"},
        {"id": "_rf6_T", "name": "city", "field_type": "Text"},
        {"id": "_rf7_T", "name": "state_province_region", "field_type": "Text"},
        {"id": "_rf8_T", "name": "postal_code", "field_type": "Text"},
        {"id": "_rf9_T", "name": "country", "field_type": "Text"},
        {"id": "_rf10_T", "name": "phone_number", "field_type": "Text"},
        {"id": "_rf11_T", "name": "whatsapp", "field_type": "Text"},
        {"id": "_rf12_T", "name": "line", "field_type": "Text"},
        {"id": "_rf13_T", "name": "facebook", "field_type": "Text"},
        {"id": "_rf14_T", "name": "unique_name", "field_type": "Text"},
        {"id": "_rf15_T", "name": "email_domains", "field_type": "Text", "read_only": true},
        {"id": "_rf16_D", "name": "last_clicked", "field_type": "Date", "read_only": true},
        {"id": "_rf17_D", "name": "last_opened", "field_type": "Date", "read_only": true},
        {"id": "_rf18_D", "name": "last_emailed", "field_type": "Date", "read_only": true},
        {"id": "_rf19_T", "name": "singlesend_id", "field_type": "Text", "read_only": true},
        {"id": "_rf20_T", "name": "automation_id", "field_type": "Text", "read_only": true},
        {"id": "_rf21_D", "name": "created_at", "field_type": "Date", "read_only": true},
        {"id": "_rf22_D", "name": "updated_at", "field_type": "Date", "read_only": true},
        {"id": "_rf23_T", "name": "contact_id", "field_type": "Text", "read_only": true}
    "_metadata": {"self": ""}

Though the data for the custom fields is almost the same as for reserved fields, we are unable to use the names and instead need the IDs for custom fields.

Building the Request

To make life easier, instead of hard coding the fields in when building the contact info object, I’ve set up an enum. The reserved fields are using the names and the custom fields use the IDs.

class SendGridFields(str, Enum):
    email = 'email',
    first_name = 'first_name',
    last_name = 'last_name',
    phone_number = 'phone_number',
    address_line_1 = 'address_line_1',
    address_line_2 = 'address_line_2',
    city = 'city',
    state_province_region = 'state_province_region',
    postal_code = 'postal_code',
    user_id = 'e1_T',
    username = 'e2_T',
    type = 'e3_T',
    donations_amount = 'e4_N',
    donations_count = 'e5 _N',
    newsletter_subscribed = 'e6_T'

Next, create the contact info object:

contact_info = {,
    SendGridFields.first_name: self.firstname if self.firstname else '',
    SendGridFields.last_name: self.lastname if self.lastname else '',
    SendGridFields.phone_number: if else '',
    SendGridFields.address_line_1: self.address1 if self.address1 else '',
    SendGridFields.address_line_2: self.address2 if self.address2 else '', if else '',
    SendGridFields.state_province_region: self.state if self.state else '',
    SendGridFields.postal_code: self.zipcode if self.zipcode else '',
    'custom_fields': {
        SendGridFields.username: self.username,
        SendGridFields.type: self.get_type(),
        SendGridFields.donations_amount: float(self.get_donations_amount() or 0),
        SendGridFields.donations_count: float(self.get_donations_count() or 0),
        SendGridFields.newsletter_subscribed: str(bool(newsletter_subscribed))

Then we can just create the client and make the request. Note that you have to build up the request if you’re using the client. Currently, only mail is built out as a class. All other API needs to be specified in a similar format. For example, sendgrid.client followed by the endpoint marketing.contacts and then the request type with the body.

sendGrid = SendGridAPIClient(api_key=config.sendgrid_apikey)
response ={'contacts': [contact_info]})

Future hopes for SendGrid Contact Info

That’s it. Hopefully, SendGrid improves the documentation and maybe switches the custom fields to using the names instead of IDs. I would also like to see the dependency built out more, i.e. have a Contact class that has a send function similar to how mail works.