List contacts

GET/api/v1/contacts
Requires anorganization API keywith the scopecontacts:read

Your contacts, newest first.

For a sync, use since_last_changed. It answers the only question an integration keeping its own copy actually has — what moved — and turns a nightly full-list crawl into a request that usually returns nothing. The value is an ISO 8601 timestamp; remember to URL-encode it, because the + in +00:00 means a space in a query string.

A since_last_changed we cannot parse is IGNORED and you get everything, rather than nothing. The two failures are not symmetrical: a sync handed zero rows concludes it is up to date and stops, and silently misses every change from then on.

Filter on state, not status. status is what the contact row says; state is what the product will actually do, and reconciles that row against your organization's suppression list. A contact whose row says subscribed but whose address you suppressed answers state: "suppressed" and will never be mailed — ?status=subscribed used to hand those back under a filter that claims to select the ones it will send to.

Parameters

NameInTypeDescription
since_last_changedquerystringISO 8601. Returns only contacts modified strictly after this moment. URL-encode it.
statequerystringOne of `subscribed`, `unsubscribed`, `bounced`, `complained`, `suppressed`. What the product will do, suppression list included.
statusquerystringThe stored row value. Kept because it is published; prefer `state`.
searchquerystringMatches email or name.
list_idquerystringOnly contacts on this list.
tagquerystringOnly contacts carrying this tag, matched by NAME and case-insensitively — tags are written by name everywhere, never by id.
interest_idquerystringOnly contacts who chose this interest, matched by ID. An interest's name is form copy; rewording it must not change who matches.
per_pagequeryintegerPage size.

Request

GET/api/v1/contacts
curl -X GET '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
    • dataarray<object>
      • 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
    • pagination_metaany

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.