Skip to content

Create a contact

Add a person to your audience.

http
POST /v1/contacts

Requires a Full access API key — contacts are people's addresses, not sending, so a sending key embedded in an app cannot read or write them.

Body parameters

ParameterTypeRequiredDescription
emailstringYesThe contact's email address. Max 254 characters. Stored lower-cased, and unique within your team.
first_namestring | nullNoMax 120 characters.
last_namestring | nullNoMax 120 characters.
unsubscribedbooleanNoDefaults to false. true marks the contact globally unsubscribed from all broadcasts and stamps unsubscribed_at.
propertiesobjectNoA flat map of contact property key → value. Keys that aren't properties on your team are ignored.
segmentsarrayNoSegments to attach the contact to, each an object { "id": "<segment id>" }. Every id must be a segment on your team.
topicsarrayNoTopic subscriptions to set, each { "id": "<topic id>", "subscription": "opt_in" | "opt_out" }. opt_in subscribes, opt_out unsubscribes.

The created contact's source is api.

Example

bash
curl -X POST 'https://api.sendgrail.com/v1/contacts' \
  -H "Authorization: Bearer $SENDGRAIL_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "email": "ada@example.com",
    "first_name": "Ada",
    "last_name": "Lovelace",
    "properties": {
      "company_name": "Analytical Engines Ltd",
      "seats": 12
    }
  }'
js
const res = await fetch('https://api.sendgrail.com/v1/contacts', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SENDGRAIL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    email: 'ada@example.com',
    first_name: 'Ada',
    last_name: 'Lovelace',
    properties: {
      company_name: 'Analytical Engines Ltd',
      seats: 12,
    },
  }),
});

const contact = await res.json();

Response

201 Created. The body carries just the new contact's id — fetch the full contact object with retrieve if you need it:

json
{
  "object": "contact",
  "id": "3f1c9a0e-6c5e-4a2b-9f2d-2b1a7c8e4d31"
}

A contact.created webhook event fires on success.

Attaching segments and topics

Pass segments and topics to place the contact into segments and set topic subscriptions in the same call, with no follow-up requests. segments is an array of { "id" }; topics is an array of { "id", "subscription" }, where subscription is opt_in or opt_out.

bash
curl -X POST 'https://api.sendgrail.com/v1/contacts' \
  -H "Authorization: Bearer $SENDGRAIL_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "email": "grace@example.com",
    "first_name": "Grace",
    "unsubscribed": false,
    "segments": [
      { "id": "7a2b6f10-5c94-4f7d-9a3e-8c1b0d6e2f45" }
    ],
    "topics": [
      { "id": "b1e7c3d9-42a6-4c0f-8b5e-9d3f1a7c6e20", "subscription": "opt_in" },
      { "id": "e5c8f2a1-70b4-4d19-9c3a-6f2e8b0d1a74", "subscription": "opt_out" }
    ]
  }'
js
const res = await fetch('https://api.sendgrail.com/v1/contacts', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SENDGRAIL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    email: 'grace@example.com',
    first_name: 'Grace',
    unsubscribed: false,
    segments: [{ id: '7a2b6f10-5c94-4f7d-9a3e-8c1b0d6e2f45' }],
    topics: [
      { id: 'b1e7c3d9-42a6-4c0f-8b5e-9d3f1a7c6e20', subscription: 'opt_in' },
      { id: 'e5c8f2a1-70b4-4d19-9c3a-6f2e8b0d1a74', subscription: 'opt_out' },
    ],
  }),
});

const contact = await res.json();

A segment or topic id that isn't one of your team's is a 404. See Contact segments and Contact topics for managing these after creation.

The email address is the identity

One address, one contact, per team. Creating a contact whose address already exists is a 422, not a silent merge:

json
{
  "message": "The email has already been taken.",
  "code": "validation_error",
  "errors": {
    "email": ["The email has already been taken."]
  },
  "request_id": "8f2c1e64-1b17-4f8c-9f0e-3b6c1e0f5c31"
}

Addresses are normalised to lower case on the way in, so Ada@Example.com and ada@example.com are the same contact — which is also why you can address a contact by its address in the path (see Retrieve a contact). If you want create-or-update behaviour, update the contact by its email address and fall back to creating it on a 404.

Properties are validated loosely on purpose

A value for a number-typed property must be numeric, or the request is a 422:

json
{
  "message": "The seats property must be a number.",
  "code": "validation_error",
  "errors": {
    "properties.seats": ["The seats property must be a number."]
  },
  "request_id": "8f2c1e64-1b17-4f8c-9f0e-3b6c1e0f5c31"
}

A key that isn't a property on your team is ignored, not rejected. A property someone deletes in the dashboard between two of your calls would otherwise turn a working integration into a stream of 422s — and there is nothing your code could usefully do about it.

Send null or "" for a key to clear the contact's value for it, which makes the property fall back to its fallback again.

See Errors for the error envelope.

Transactional email on your own domain.