---
title: "Subscribe a contact to a list (creating or updating the contact)"
method: POST
path: "/lists/{id}/subscribe"
tags: ["Marketing (experimental)"]
---

# Subscribe a contact to a list (creating or updating the contact)

`POST /lists/{id}/subscribe`

## Request

- Base URL: `https://api.oveyon.com/v1`
- URL: `https://api.oveyon.com/v1/lists/{id}/subscribe`
- Auth: HTTP bearer

EXPERIMENTAL — enabled account by account; without it every call answers 403 module_disabled. Scopes read:contacts / write:contacts. On a list with double opt-in the subscription stays `pending`, and this call does NOT send a confirmation email (the confirmation flow is not available yet). Without double opt-in it is `subscribed` at once. Someone who unsubscribed through the link or the preference center, complained, or bounced is not subscribed again by the API (409).

## Path parameters

- `id` integer, required

## Request body

- MktSubscribeInput
  - `email` string, required: Required. Normalized before matching (case does not matter): only ASCII whitespace (space, TAB, LF, VT, FF, CR) is trimmed from the ends — a Unicode space at either end (NBSP, U+3000…) is not trimmed and the call is 400 `invalid_email`. ASCII only, the local part and the domain: an internationalized domain (IDN) enters only in its xn-- form (`ana@xn--exmple-cua.com`); `ana@exämple.com` is refused with 400 invalid_email and `reason: address_not_ascii`. The same holds when finding, tagging, unsubscribing and deleting a contact: only the ASCII form is looked up.
  - `firstName` string, nullable
  - `lastName` string, nullable
  - `attributes` object, nullable: Shallow merge into the stored properties. Names: `^[a-z][a-zA-Z0-9_]{0,63}$`, never email/firstName/lastName. A new name is registered with the type of its first value.
  - `externalId` string, nullable
  - `consent` object, nullable: Optional proof of consent of the PERSON who subscribed (your form), not of whoever calls the API.
    - `ip` string: An IPv4 or IPv6 address.
    - `source` string: Where they subscribed (e.g. the form URL).

## Response `200`

The subscription

- MktSubscription
  - `contactId` integer, required
  - `listId` integer, required
  - `status` 'pending' | 'subscribed', required: `pending` on a list with double opt-in — and NO confirmation email is sent by this call. Also `pending` when the person was already `pending` on this list (e.g. they filled your form and did not confirm yet), on any list: only their own confirmation or their preference center turns `pending` into `subscribed`, never an API call or an import.
  - `changed` boolean, required
  - `contactCreated` boolean, required

## Other responses

- `400`: invalid_email, invalid_property, invalid_property_value, bad_request (with `field`, including `consent.*`).
- `401`: Missing or invalid API key.
- `403`: module_disabled — the marketing module is not enabled on this account. Insufficient scope (`insufficient_scope`, with `required` and `have`), a scope the key holds but cannot use while its human-approval lock is on (`scope_blocked_by_approval_lock`, with `required` — approve:holds, write:send-policies, manage:webhooks, write:inbound, write:templates, write:surveys, write:domains, write:contacts), API key switched off by its owner in the panel (`key_disabled` — the key is valid; turn it back on or use another one, do not rotate), sending disabled, account suspended (`account_suspended`) or unknown (`account_unknown`), or the key used from an IP outside its allowed ranges (`ip_not_allowed`, with the `allowed` list — see /v1/credentials/ip-rules). 403 is not a limit: do not retry.
- `404`: list_not_found (identical for another account’s list).
- `409`: unsubscribed_by_contact, complained, bounced — the person decided; only they can come back. Nothing is changed by the refused call, not even the contact.
- `413`: payload_too_large — the request body exceeds this route's limit (`limit`, in bytes: 25 MB unless the operation says otherwise). The key is validated before the body is read: without a valid key the answer is 401/403 and the body is not read. Do not retry the same body.
- `415`: unsupported_media_type — send the body as `application/json` (form and plain-text bodies are not accepted).
- `422`: too_many_properties, attributes_too_large (with `limit`).
- `429`: Per-key limit (`rate_limited`, 600 requests/min by default per API key — or per MCP OAuth connection —, from any IP), per-IP limit for requests WITHOUT a valid credential (`rate_limited`, 600/min by default), too many unknown or malformed credentials from your address (`too_many_auth_failures`, 20/min by default, counted per IPv4 address or per IPv6 /48; a missing `Authorization`, an expired OAuth token or a disabled key do not count, and anything without the shape of one of our keys counts without being checked; from an address over that limit a valid key still gets through within 120 credential checks/min, and above them every credential not validated in the last 5 minutes gets this 429 until the minute turns) — all checked before the route, on every operation —, account quota (`quota_exceeded` with `window`, `used`, `limit`), warmup cap (`warmup_cap_reached`) or saturation (`queue_full`). Honor `Retry-After` with exponential backoff and jitter.
- `500`: internal_error — an unexpected failure on our side. The detail stays in our logs, never in the body. Retry reads; retry a write only when it is idempotent or carries an idempotency key.
- `503`: contact_busy — another request of this account kept writing to the same contact (or the same new tag) for a few seconds, and this one gave up instead of waiting longer. Nothing is wrong with the request: retry after `Retry-After` (5 s, also in `retryAfter`); retrying is safe. Or ip_rules_unavailable — the IP ranges bound to your key could not be checked right now (caution, not a denial; `Retry-After` 30 s).

---

[API](https://skmtc.dev/oveyon/apis/oveyon-api.md) · [All operations](https://skmtc.dev/oveyon/apis/oveyon-api/llms.txt) · [OpenAPI document](https://skmtc.dev/oveyon/apis/oveyon-api/revisions/abf64c09d297?raw)
