Create a contact

POST/api/v1/contacts
Requires anorganization API keywith the scopecontacts:write

Request

POST/api/v1/contacts
curl -X POST 'https://app.mailyte.com/api/v1/contacts' \
  -H 'Authorization: Bearer mk_live_YOUR_API_KEY'
The key names its own organization, so no X-Organization-ID header is needed.

Response

Success.

  • dataobject
    • objectstringcontact
    • idstring

      Unique identifier for the contact.

    • emailstring

      Lower-cased and trimmed on write, so the address you read back may not be byte-identical to the one you sent.

    • namestring

      The contact's display name. `null` means NONE — an address is all a contact needs, the column is nullable and neither create nor import requires one. It is not "we did not look": the name is on the row we already read. `{{ name }}` renders as the empty string for these contacts unless the template supplies its own inline fallback — a contact-field default cannot fill it, because `name` is a reserved key applied after the defaults are merged.

    • statestringsubscribed | unsubscribed | bounced | complained | suppressed

      The answer to "will this contact be mailed". `subscribed` is exactly "we will attempt delivery"; every other value is the reason we will not. DERIVED, and read-only — it reconciles the writable `status` field with the organization-wide suppression list, which are two independent gates that can and do disagree: `status` is read when a campaign's recipient set is built, the suppression list is read at send time per message, and nothing keeps them in step. A contact whose `status` says `subscribed` but whose address — or whose whole domain — is suppressed reads `suppressed` here, and no message will leave. The contact's own recorded decision outranks the list, so somebody who unsubscribed reads `unsubscribed` even when both are true of them. A stored status we do not recognise reads `unsubscribed`, never `subscribed`: guessing "mailable" from a value we cannot read is how people get mailed who asked not to be. **`status` is the separate, writable field**, and it accepts only `subscribed`, `unsubscribed`, `bounced` and `complained` — `suppressed` is not something you can set on a contact, it is something the suppression list does to them, which is why the two have different names. Note that `GET /contacts?status=` filters the stored column, not this field.

    • fieldsobject

      Custom field values, keyed by the field `key` that `GET /contact-fields` publishes and that `{{ signup_date }}` resolves — NEVER by a `contact_field` id, which identifies the definition and appears only as that resource's `id`. **Always an object**: `{}` when the contact has no values, never a list. Free-form — a key written through the API or an import with no `contact_field` definition behind it is still a real value on the contact and is included here; `GET /contacts/attribute-keys` is where you learn which keys are declared. Values are strings as stored: a field's `type` is an editing hint, not a storage type, and nothing is coerced. Field `default_value`s are NOT merged in — they are applied at send time, and merging them here would turn a default into a real value on your next write. The matching INPUT key is still spelled `attributes`.

    • listsarray<object>

      The contact lists this contact belongs to, NAMED, so you do not have to fetch each one to display it — every path that returns a contact already loads them. They carry no `contact_count`: that is a property of the LIST, not of this membership, and `contact_list.contact_count` is where it is published. `null` means UNKNOWN — the membership pivot was not loaded for this response — and never `[]`, which would claim the contact belongs to no list. **The matching INPUT key is still spelled `list_ids`** and still takes bare ids, on create and on update.

      • idstring
      • namestring
    • tagsarray<object>

      Your own labels on this contact, NAMED. A tag is something you put on a contact and it stays there; a SEGMENT is a rule re-evaluated every time it is read, so its membership changes without anybody editing a contact. They are separate resources here for that reason. No `contact_count`: that belongs to the tag rather than to this contact having it, and `GET /contact-tags` publishes it. `null` means UNKNOWN — the relation was not loaded — never that this contact is untagged.

      • idstring
      • namestring
    • interestsarray<object>

      What this contact CHOSE — the answers they ticked on a form, as opposed to `tags`, which are labels you put on them and they never see. Each carries the question it answers, because an answer name is meaningless without it and two groups may share one. `null` MEANS NOT INCLUDED — the relation was not loaded on this response. An empty array means the contact chose nothing, which is a different fact.

      • idstring

        Match on this in a segment rule, never on the name.

      • namestring

        The answer, as the contact read it.

      • category_idstring

        The question this answers.

      • category_titlestring

        The question in words. `null` MEANS NOT INCLUDED — the category was not loaded, never that the question has no title.

    • consentobject

      Evidence of how this contact opted in. Every field is NULL MEANS UNKNOWN — we do not hold the record — and never "no consent was given", which is the opposite claim and not one we can make on your behalf. Set it when you import from another provider so your lawful basis moves with your contacts.

      • atstring

        When they opted in, as recorded by whoever collected it. NULL MEANS UNKNOWN.

      • ipstring

        The address the opt-in came from. NULL MEANS UNKNOWN.

      • sourcestring

        Where: a form name, a URL, "imported from Mailchimp". Free text rather than an enum, because the shapes this takes across providers are not enumerable and an enum would force the evidence to be discarded to fit. NULL MEANS UNKNOWN.

    • sourcestring

      How this contact arrived. Free-form, not an enum: any string up to 50 characters is accepted on create. The values we set ourselves are `manual`, `import_api` and `import_csv`. `null` means NONE — nothing was recorded — not "we did not look" and not "we do not know where they came from": a contact created through this API is stamped `manual` when you send no `source`, and an import stamps its own, so a null here is a row that predates the column or one a caller explicitly cleared.

    • created_atstring
    • updated_atstring

Returned inside the standard envelope.

Errors

StatusWhen
401The API key is missing, unknown, revoked or expired. All four answer identically, on purpose: distinguishing them would confirm which keys exist.
403The key is valid but may not do this: it lacks the required scope, its IP allowlist does not include you, or this endpoint does not accept API keys.
404No such resource in this organization.
422The request was understood but the values were not acceptable.
429Too many requests, or the organization has spent its sending allowance. `Retry-After` says how long to wait.

Every status, with what causes it and what to do, is on the error reference.