Skip to content

Create a contact property

Create a custom field that every contact can carry.

http
POST /v1/contact-properties

Requires a Full access API key — properties shape your audience, so a sending key embedded in an app cannot create them.

Body parameters

ParameterTypeRequiredDescription
keystringYesThe property key, and the merge tag you render in a template. Max 50 characters; letters, numbers and underscores. Unique within your team.
typestringYesstring or number. Fixed at creation — see below.
fallback_valuestring | nullNoThe value to render when a contact has no value of its own. For a number property it must itself be numeric.

Example

bash
curl -X POST 'https://api.sendgrail.com/v1/contact-properties' \
  -H "Authorization: Bearer $SENDGRAIL_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "key": "company_name",
    "type": "string",
    "fallback_value": "Acme Corp"
  }'
js
const res = await fetch('https://api.sendgrail.com/v1/contact-properties', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SENDGRAIL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    key: 'company_name',
    type: 'string',
    fallback_value: 'Acme Corp',
  }),
});

const property = await res.json();

Response

json
{
  "object": "contact_property",
  "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
  "key": "company_name",
  "type": "string",
  "fallback_value": "Acme Corp",
  "created_at": "2026-07-14T19:44:32+00:00"
}

The key is a merge tag

Whatever you set as key is what a template renders:

html
Hello {{{contact.company_name}}}

That is why the format is restricted — a key with a space or a dash in it would not survive a template. It's also why key is unique per team: two properties answering to the same tag would make the rendered value a coin toss.

Renaming a key later is allowed, and it changes what your templates must render. Anything still rendering the old tag falls back to empty.

The type cannot be changed

type is fixed once the property exists. Changing string to number would silently invalidate every value already stored against it — "Acme Corp" is not a number, and there is no sensible thing to do with it.

Send type on an update and it's rejected, not ignored:

json
{
  "message": "The type cannot be changed after the property is created.",
  "code": "validation_error",
  "errors": {
    "type": ["The type cannot be changed after the property is created."]
  },
  "request_id": "8f2c1e64-1b17-4f8c-9f0e-3b6c1e0f5c31"
}

A 200 there would be worse: you'd believe the type had changed when it hadn't. To change a type, delete the property and create it again — and be aware that deleting it clears that value from every contact.

Transactional email on your own domain.