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.
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.
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.
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 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 |
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:
{
"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: trueand 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_byrecords 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.
- 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.