# Create a contact > Capture a signup through the full LaunchList pipeline (deduplication, referral credit, trust scoring, the verification email when the form asks for it, webhooks and automations) in one call. Source: https://getlaunchlist.com/help/docs/api/contacts-create --- Creates a contact and runs the **full signup pipeline**: deduplication, referral credit, trust scoring, the verification email, webhooks and automations. This is the same path a Launch page submission takes, so nothing is skipped. One call returns the referral code, the live queue position and the trust verdict, so you do not need a second request to show someone their place in line. ## Body parameters | Parameter | Type | Required | Description | | ----------------- | ------ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `project` | string | Yes | The project this signup belongs to, as a `prj_` handle. A pinned key may only write to its own project. | | `email` | string | Conditional | The signup's email address. One of `email`, `phone` or `identifier` is required. See the note below for forms identified by something else. | | `phone` | string | Conditional | An E.164 phone number, for phone-only waitlists. | | `identifier` | string | Conditional | Any other identity type, paired with `identifier_type`. | | `identifier_type` | enum | With `identifier` | One of `email`, `phone`, `username`, `wallet`, `custom`. | | `form` | string | No | A `frm_` handle. The form's own field rules still apply. Defaults to the project's oldest active waitlist form. | | `attributes` | object | No | Up to 50 key-value pairs stored on the contact. Keys your form does not define are kept, not dropped. | | `referred_by` | string | No | The referral code that sent this person. The credit lands when they verify, or at once when nobody asks them to. | | `external_id` | string | No | Your own identifier for this person, unique per project. | Email addresses are canonicalized before deduplication, so common Gmail misspellings resolve to the same person rather than creating two. > [!NOTE] `identifier_type` is validated, but the form decides the stored type > The identity you send is written into the target form's own identity field, and the contact's > `identifier_type` is derived from that field. Sending `identifier_type` outside the five values > above is rejected, but sending a valid one that disagrees with the form does not override it. > If you need a phone-only contact, capture through a form whose identity field is a phone field. > [!NOTE] `email` beside `phone` or `identifier` is the address, not the identity > On a form identified by a phone number, a username or a wallet, send `email` together with > `phone` or `identifier` to record where that person is mailed. It is stored as the contact's > `email` and their identity stays what the form collects. It fills an empty address and never > replaces one already on file, so retrying the call changes nothing. To change an address, use > [`PATCH /v1/contacts/{id}`](/help/docs/api/contacts-update). On a form identified by an email, > the contact's `email` always equals its `identifier`. ## Subscription and verification A new signup is **subscribed** at once: joining is agreeing to hear from you, so `status` is `subscribed` and `consent` is `signup`. Whether they are also sent an email to **verify** their address is the form's own setting, **Ask people to verify their email**. The API takes no override: the form decides, the same as for a signup on your page. If you want a different answer, capture through a different form. The one addition is the project's **Hold referrals until the friend verifies**: when it is on, a new signup you pass a `referred_by` for is sent the link even if the form doesn't ask. `verification` in the response says where they stand on proving their address. It is separate from `status`: | `verification` | Meaning | | -------------- | -------------------------------------------------------------------------------------------------- | | `waiting` | A link was made (the form asks, or referrals are held until the friend verifies). Not clicked yet. | | `verified` | They clicked it. | | `not_asked` | They have an address, but nobody asked them to verify. | | `no_email` | There is no email address to verify. | `waiting` means a verification email is due, not that it has gone out: a guard can still hold it back, for example when the address is known to bounce. > [!NOTE] It never changes someone who is already on your list > If the address is already a contact, the call returns them as they are, whatever you pass. Their > status, consent and any unsubscribe stay untouched. To subscribe someone you imported as not subscribed, > or whom you unsubscribed, use [resubscribe](/help/docs/api/contacts-subscription). A person who > unsubscribed themselves can only come back by verifying their own email. ## What comes back The response is a full contact object plus `verification`, above. A few fields answer differently than you might assume: | Field | Values | | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `deliverability` | `null` until the verdict is revealed with a credit. Otherwise `deliverable`, `undeliverable`, `disposable`, `risky`, `role` or `unknown`. | | `trust_tier` | `verified`, `valid`, `unknown`, `risky` or `fake`. A fresh signup we have not finished checking is honestly `unknown`. | | `position` | The live queue position, or `null` when the contact is not on the board. | | `status` | `subscribed` for a new signup. `pending` (shown as Not subscribed) is only an imported contact you were not sure about. | | `consent` | How they agreed to hear from you: `confirmed`, `signup`, `imported`, `owner` or `v1`. `null` for an imported contact you were not sure about. | `referral.state` is `null` for somebody nobody referred, and `pending_verification` for a brand new signup who came through a referral link, because credit has not been given yet: it waits for them to verify when the form asks them to or the project holds referrals until the friend verifies (then they are sent the link even though the form doesn't ask), and otherwise follows moments after the call. See [referrals](/help/docs/api/contacts-referrals) for the full list of states. ## Headers | Header | Value | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Authorization` | Bearer token. Live keys begin `llk_live_`. | | `Idempotency-Key` | Any unique string, up to 255 characters. A replay within 24 hours returns the original response with `Idempotent-Replayed: true`. Reusing a key with a different body returns `409 idempotency_key_reused`. | | `Content-Type` | `application/json` | > [!NOTE] 201 means new, 200 means we already had them > Submitting an address already on the list is not an error. You get `200` with the existing > contact, so a form that double-fires never creates a duplicate and never shows your visitor a > failure. ## Errors | Status | Type | Code | Meaning | | ------ | ----------------------- | --------------------------- | ---------------------------------------------------------------------- | | `401` | `authentication_error` | `missing_api_key` | No `Authorization` header, or a token that does not start `llk_`. | | `401` | `authentication_error` | `invalid_api_key` | The key is unknown, revoked or expired. | | `403` | `permission_error` | `insufficient_scope` | The key does not carry `contacts:write`. | | `404` | `not_found_error` | `resource_missing` | No such project or form, or it belongs to another workspace. | | `409` | `idempotency_error` | `idempotency_key_reused` | The same `Idempotency-Key` was reused with a different body. | | `409` | `idempotency_error` | `idempotency_key_in_flight` | A request with this key is still being processed. Retry after it. | | `422` | `invalid_request_error` | `parameter_invalid` | Body failed validation. Read `errors` for the field. | | `422` | `invalid_request_error` | `no_capture_form` | No `form` given and the project has no active waitlist form. | | `422` | `invalid_request_error` | `form_without_identity` | The target form has no identity field, so it cannot capture anyone. | | `422` | `invalid_request_error` | `submission_rejected` | The spam guard refused this signup. | | `422` | `invalid_request_error` | `external_id_taken` | Another contact in this project already carries that `external_id`. | | `422` | `invalid_request_error` | `contact_not_created` | Accepted, but resolved to no contact. Check the form's identity field. | | `429` | `rate_limit_error` | `submission_rate_limited` | This one signup is being submitted too fast. Honor `Retry-After`. | | `429` | `rate_limit_error` | `rate_limit_exceeded` | Over your plan's request limit. Back off for `Retry-After` seconds. | Every failure uses one envelope, so a client parses a single shape for all of them. `param` and `errors` are present only when a specific field is at fault: ```json { "error": { "type": "invalid_request_error", "code": "parameter_invalid", "message": "One of `email`, `phone` or `identifier` is required.", "param": "email", "errors": { "email": ["One of `email`, `phone` or `identifier` is required."] }, "request_id": "req_01jq8zk3m7xn2vb4rd5eempt9h" } } ``` ## Worth knowing - **Capture never fails for billing reasons.** If the workspace is over its plan's contact limit, the signup is still stored. The response returns `locked: true` and withholds the identity until the plan is upgraded. Nobody's signups are ever refused or deleted over a plan cap. - **Positions come from Redis**, not a table scan, so this call stays fast at a million contacts. - **Referral credit follows verification.** Passing `referred_by` records who sent this person right away, but the referrer's count moves when the new signup verifies. On a form that does not ask people to verify there is nothing to wait for, so the credit lands at creation, unless the project holds referrals until the friend verifies: then they are sent the link and the credit waits for the click. - **A stage with the invited role mints an invitation.** That is not this endpoint, but it is worth knowing before you move somebody with [update](/help/docs/api/contacts-update). - **Every response carries a `Request-Id`.** Quote it in support and we can find the exact call. ## Common questions ### What happens if I submit an email address that is already on the waitlist? You get 200 with the existing contact rather than an error, so a form that double-fires never creates a duplicate and never shows the visitor a failure. A genuinely new signup returns 201. ### Is POST /v1/contacts safe to retry? Yes. Send an Idempotency-Key header and a replay within 24 hours returns the original response instead of creating a second contact. Only a successful response is stored, so a request that failed is still yours to retry with the same key. ### Why is deliverability null on a contact I just created? Because that contact has not been checked yet. Deliverability is filled in once the contact has been checked with a credit. Until then the field is null on every read path, including this one. ### Does a signup fail if the workspace is over its plan's contact limit? No. Capture is unconditional, so the signup is always stored. The response returns locked true and withholds the identity until the plan is upgraded.