Sign in Create account
Audience

Contact properties API

Custom fields for contacts, with a type and a default value.

Properties are the custom fields of your contacts, such as plan, city or birth date. Define the key and the type once; then use the key in properties when you create or update contacts, in imports and in the fields of forms. Each team can have up to 100 properties. In endpoints with {id}, you can use either the ID or the key.

Properties are also used in the contact batch and in the filters of dynamic segments (each type has its own operators). If you delete a property that a form uses, the form keeps receiving submissions, but the answer to that field is no longer saved on the contact.

#Types and conversion

TypeAccepted valuesStored as
stringText, up to 1,000 characters.Text, without leading or trailing spaces.
number42, "42", "1234,56" or "1234.56".Integer or decimal number.
booleantrue/false, "sim"/"não", "yes"/"no", "1"/"0", "on"/"off".true or false.
date"2024-03-18", "18/03/2024" (day first) or another recognizable date.YYYY-MM-DD.

An empty value (null or "") clears the property on the contact. Values that cannot be converted return 422.

#List properties

GET/contact-properties

Full-access key

Query parameters

  • limitinteger
    From 1 to 100. Default: 20.
  • afterstring
    Cursor for the next page.
  • beforestring
    Cursor for the previous page.
GET /contact-properties

cURL

curl https://api.postal.so/contact-properties \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/contact-properties', {
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
  },
});

const data = await response.json();

Python

import os
import requests

response = requests.get(
    "https://api.postal.so/contact-properties",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->get('contact-properties', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Response
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "contact_property",
      "id": "019a1d80-5d6e-7f80-8a9b-0c1d2e3f4a5b",
      "key": "plan",
      "type": "string",
      "fallback_value": "free",
      "created_at": "2026-10-20T09:00:00.000000Z",
      "updated_at": "2026-10-20T09:00:00.000000Z"
    }
  ]
}

#Create a property

POST/contact-properties

Full-access key

The key and the type cannot change later, because contacts already store values of that type.

Request body

  • keystringrequired
    Lowercase letters, numbers and _, starting with a letter, up to 64 characters. It cannot be id, email, external_id, user_id, first_name, last_name, unsubscribed, created_at, updated_at, properties, segments, topics, tags or source.
  • typestringrequired
    string, number, boolean or date.
  • fallback_valuestring | number | boolean | null
    Value used when the contact does not have the property (for example, when personalizing emails).

Errors

  • 422validation_error
    Invalid, reserved or already used key, invalid type, default value that does not match the type, or the team already has 100 properties.
POST /contact-properties

cURL

curl -X POST https://api.postal.so/contact-properties \
  -H "Authorization: Bearer $POSTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "plan",
    "type": "string",
    "fallback_value": "free"
  }'

Node.js

const response = await fetch('https://api.postal.so/contact-properties', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    key: 'plan',
    type: 'string',
    fallback_value: 'free',
  }),
});

const data = await response.json();

Python

import os
import requests

response = requests.post(
    "https://api.postal.so/contact-properties",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    json={
        "key": "plan",
        "type": "string",
        "fallback_value": "free",
    },
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->post('contact-properties', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'json' => [
        'key' => 'plan',
        'type' => 'string',
        'fallback_value' => 'free',
    ],
]);

$data = json_decode($response->getBody(), true);
201Response
{
  "object": "contact_property",
  "id": "019a1d80-5d6e-7f80-8a9b-0c1d2e3f4a5b"
}

#Get a property

GET/contact-properties/{id}

Full-access key

Path parameters

  • idstringrequired
    ID or key.

Errors

  • 404not_found
    The resource does not exist or belongs to another team.
GET /contact-properties/{id}

cURL

curl https://api.postal.so/contact-properties/plan \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/contact-properties/plan', {
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
  },
});

const data = await response.json();

Python

import os
import requests

response = requests.get(
    "https://api.postal.so/contact-properties/plan",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->get('contact-properties/plan', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Response
{
  "object": "contact_property",
  "id": "019a1d80-5d6e-7f80-8a9b-0c1d2e3f4a5b",
  "key": "plan",
  "type": "string",
  "fallback_value": "free",
  "created_at": "2026-10-20T09:00:00.000000Z",
  "updated_at": "2026-10-20T09:00:00.000000Z"
}

#Update a property

PATCH/contact-properties/{id}

Full-access key

Only the default value can change. Sending key or type returns 422.

Path parameters

  • idstringrequired
    ID or key.

Request body

  • fallback_valuestring | number | boolean | null
    New default value; null removes it.

Errors

  • 422validation_error
    A field is invalid. The message says which one.
  • 404not_found
    The resource does not exist or belongs to another team.
PATCH /contact-properties/{id}

cURL

curl -X PATCH https://api.postal.so/contact-properties/plan \
  -H "Authorization: Bearer $POSTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "fallback_value": "pro"
  }'

Node.js

const response = await fetch('https://api.postal.so/contact-properties/plan', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    fallback_value: 'pro',
  }),
});

const data = await response.json();

Python

import os
import requests

response = requests.patch(
    "https://api.postal.so/contact-properties/plan",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    json={
        "fallback_value": "pro",
    },
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->patch('contact-properties/plan', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'json' => [
        'fallback_value' => 'pro',
    ],
]);

$data = json_decode($response->getBody(), true);
200Response
{
  "object": "contact_property",
  "id": "019a1d80-5d6e-7f80-8a9b-0c1d2e3f4a5b"
}

#Delete a property

DELETE/contact-properties/{id}

Full-access key

Removes the definition immediately; the values the contacts had are deleted in the background.

Path parameters

  • idstringrequired
    ID or key.

Errors

  • 404not_found
    The resource does not exist or belongs to another team.
DELETE /contact-properties/{id}

cURL

curl -X DELETE https://api.postal.so/contact-properties/plan \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/contact-properties/plan', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
  },
});

const data = await response.json();

Python

import os
import requests

response = requests.delete(
    "https://api.postal.so/contact-properties/plan",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->delete('contact-properties/plan', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Response
{
  "object": "contact_property",
  "id": "019a1d80-5d6e-7f80-8a9b-0c1d2e3f4a5b",
  "deleted": true
}