Create or update a contact

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

Creates the contact if this email is new, updates it if it is not — the call to reach for when you are keeping your own database in step with ours.

Without it a sync needs a GET, a branch, and then a POST or a PATCH: three round trips, and a race between the read and the write.

status_if_new is the one to understand. It applies only when the contact is created. A nightly sync that sends status: subscribed for everybody in your CRM would resurrect every unsubscribe it touched — the worst thing this API can do to you, because the person asked not to be mailed and then was. Send status_if_new to say "subscribed as far as I know" without overriding what the contact themselves decided here; send status only when you mean to change it.

Lists named in list_ids are added. The contact's other memberships are left alone.

Checks. Once Bounce protection is on for your account, a new contact, or a contact whose address changes, is checked in the background with one check: its email_status reads pending until the answer lands, then valid, risky, invalid (and the contact becomes bounced) or unknown. With Bounce protection switched off, or no checks left, the contact is still saved, as not_checked. A contact from an event (unconfirmed) is checked when it becomes subscribed. The write never fails because of a check.

Send "check": "wait" to have the address checked before we answer (up to about 8 seconds), so the email_status in the reply is already the answer. Any other value is ignored. If the checker is slow, the contact is saved and checked in the background; if no checks are left, it is saved as not_checked.

The reply carries X-Mailyte-Checks-Remaining and, when it applies, X-Mailyte-Checks-Warning: running_low | ran_out. Neither is sent until Bounce protection is on for your account.

Request body

  • emailstringrequired

    The key. Matched case-insensitively.

  • namestring

    The person's name, as it should appear in your emails ({{name}}).

  • attributesobject

    Custom fields as key–value pairs. On a contact you already have, this REPLACES all their custom fields: send every field you want to keep.

  • statusstringsubscribed | unsubscribed | bounced | complained

    Applied whether the contact is new or existing. Use it only when you mean to change somebody’s state.

  • status_if_newstringsubscribed | unsubscribed | bounced | complained

    Applied ONLY on create, and ignored for a contact that already exists. This is what makes a repeated sync safe.

  • sourcestring

    Optional. Where this contact came from, in your own words (up to 50 characters), like checkout.

  • list_idsarray<string>

    Lists to add the contact to. Their other lists are left alone. Copy IDs from Contacts → Lists, or read them from GET /contact-lists.

  • consented_atstring

    When this person agreed to be emailed, in ISO 8601. Bring it when you move from another service: it is your proof of consent.

  • consent_ipstring

    The address the agreement came from. IPv4 or IPv6.

  • consent_sourcestring

    Where it happened, in your own words — a form name, a checkout step, "imported from Mailchimp". Free text, because what counts as evidence differs by regulator and we will not pick for you.

  • checkstring

    Send "wait" to check the address before we answer (up to about 8 seconds) when this creates the contact or changes its address, while Bounce protection is on. Any other value is ignored.

Request

PUT/api/v1/contacts
curl -X PUT 'https://app.mailyte.com/api/v1/contacts' \
  -H 'Authorization: Bearer mk_live_YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "email": "ada@example.com",
    "name": "Ada Lovelace",
    "status_if_new": "subscribed",
    "attributes": {
      "plan": "pro"
    },
    "check": "wait"
  }'
The key names its own organization, so no X-Organization-ID header is needed.

Response

201 when the contact was created, 200 when it already existed.

  • 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 name. Null when none was given; {{ name }} is then empty unless your template gives a fallback.

    • statestringsubscribed | unsubscribed | bounced | complained | suppressed

      Whether this contact will be emailed. subscribed means yes; any other value is the reason not. It combines the contact's status with your suppression list, so a subscribed contact whose address is suppressed reads suppressed. Read-only: you set status (subscribed, unsubscribed, bounced or complained) instead. Note that ?status= filters on status, not on this.

    • email_statusstringpending | valid | risky | invalid | unknown | not_checked

      The result of checking whether the mailbox exists. valid: it exists. invalid: it does not. risky: it may not take mail (a catch-all domain, a role address such as info@, a throwaway address or a full inbox). unknown: the check could not tell. pending: being checked. not_checked: added after Bounce protection launched and not checked. Null: never checked, from before Bounce protection launched. Read-only; filter with ?email_status=.

    • email_full_untilstring

      Until when marketing to this contact is held because their inbox is full. When a check finds the inbox full, marketing waits 7 days; near the end it is checked again for free and the hold either ends or runs another 7 days. App email is never held. Null means no hold. Read-only.

    • fieldsobject

      Custom field values, keyed by the field's key (the name {{ signup_date }} uses). Always an object, {} when empty. Values are strings as stored; field defaults are not filled in here. When writing, send these as attributes.

    • listsarray<object>

      The lists this contact is on, with their names. When writing, send list IDs as list_ids.

      • idstring
      • namestring
    • tagsarray<object>

      Your labels on this contact, with their names.

      • idstring
      • namestring
    • interestsarray<object>

      What this contact chose on a form, each with the question it answers. [] means they chose nothing.

      • 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

      How this contact agreed to be emailed. Each field is null when we have no record. Set it when you import from another service, so your proof of consent comes with your contacts.

      • atstring

        When they agreed. Null when unknown.

      • ipstring

        The IP address they agreed from. Null when unknown.

      • sourcestring

        Where they agreed, such as a form name, a URL or "imported from Mailchimp". Null when unknown.

    • sourcestring

      How this contact was added: manual, import_api, import_csv or your own text (up to 50 characters). Null when nothing was recorded.

    • 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.