Contacts
A contact is a person in one organization's audience: an email address, optional name, arbitrary custom properties, and tags.
Creating one
curl -X POST http://api.localhost/v1/contacts \
-H "X-API-Key: $NOTIFYZR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "someone@example.com",
"name": "Ada Lovelace",
"properties": { "plan": "pro", "signed_up_at": "2026-01-14" }
}'Email addresses are normalised to lower case, and are unique per organization.
Custom properties
Properties are free-form JSON, but the keys you intend to filter on should be declared through /v1/contact-properties. A declared property has a type, which is what lets a segment offer the right operators for it.
Numbers and dates compare as their own types in a segment filter. If a property's stored type stops matching its declaration — because you changed the declaration after writing data — rows with the old type simply drop out of the filter rather than erroring the whole segment.
Subscription status
unsubscribed and unsubscribed_at are derived, read-only fields. They are not columns on the contact; they come from the suppression list, joined by email address. That matters because:
- Deleting a contact does not resubscribe the address.
- An address can be suppressed without ever having been a contact.
Change the state with its own endpoints, not by updating the contact:
curl -X POST http://api.localhost/v1/contacts/$ID/unsubscribe -H "X-API-Key: $KEY"
curl -X DELETE http://api.localhost/v1/contacts/$ID/unsubscribe -H "X-API-Key: $KEY"See Unsubscribes for the list itself.
Bulk import
POST /v1/contact-imports takes a CSV, either as multipart/form-data or as JSON with a csv string.
The file is parsed and validated synchronously, so a malformed row comes back in the response rather than being discovered later:
{
"id": "…",
"total_rows": 10000,
"failed_rows": 3,
"errors": [{ "row": 42, "message": "Invalid email address" }]
}Only the write is queued. A broken row is dropped and counted; a broken file (unknown column, no email mapping) rejects the whole upload. Limits are 10 MB and 50,000 rows.
If the contact quota runs out partway through, the contacts that fit are real, the rest are counted as skipped, and the import completes rather than failing.