# Compliance & registration

Source: https://jointextkit.com/docs/compliance
Index: https://jointextkit.com/llms.txt

Carriers only deliver business texting at volume from a registered **brand**
(your business) and **campaign** (what you send, and how people opted in).
Most registrations are rejected for the same few reasons: a privacy policy
without the SMS clause, an opt-in form missing "Msg & data rates may apply",
a pre-checked consent box. TextKit checks for all of them before anything
is filed, writes the copy you are missing, and files the registration for you.

Filing is part of the service. The registration fees themselves (brand,
campaign and vetting fees) are billed separately from the per-segment rate; see [Pricing](/docs/pricing). Preparing a registration,
running the checks and everything in the sandbox costs nothing, and it works
with test keys exactly as it does with live keys.

## How it works

1. **You create a registration**: business name, website, the page where
   people opt in, what you will send (the use case), and 2 to 5 real
   examples of those texts. In the console you only type your website: we
   [fill in the rest](#fill-in-from-your-website) for you to review.
2. **We check your website and your examples** right away (details below).
   Every finding comes with the evidence (the URL and the text we found, or
   what we looked for) and, if it fails, the exact text to paste.
3. **You fix and recheck** until every check passes and the examples are
   yours. The registration is then `ready`.
4. **You submit.** The first time for a business, someone authorized to act
   for it signs a short [attestation](#business-verification), and we verify
   the business before anything is filed, usually within 1 business day.
   Then TextKit's compliance team files the brand and campaign with the
   carriers (`submitted`, then `in_review`).
5. **Carriers decide**, usually in 3 to 7 business days: `approved`, or
   `rejected` with a plain-language reason and the exact fix. Fix it, recheck,
   and submit again.

Every registration answers five questions at all times, in its `guidance`
field and on the [console Registration page](/console/registration):
what happened, why, who acts next (`you`, `delivered`, `carriers`, or
`nobody`), exactly what to do, and what happens after.

## Fill in from your website

Give us the website and we suggest the rest of the registration: business
name, a legal name if the site states one, the use case, a description, the
opt-in, privacy and terms pages, support email, contact phone, address and
industry. The console form does this when you click **Fill in from my
website**; over the API it is one call:

```bash
curl -X POST https://api.jointextkit.com/v1/registrations/prefill \
  -H "Authorization: Bearer $TEXTKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"website": "https://acmeplumbing.com"}'
```

```json
{
  "object": "registration_prefill",
  "reachable": true,
  "error": null,
  "fields": {
    "website": "https://acmeplumbing.com/",
    "business_name": "Acme Plumbing",
    "use_case": "account_notification",
    "description": "Licensed plumbers serving Springfield since 1998. Acme Plumbing sends account alerts and notifications to customers who have opted in. ...",
    "opt_in_url": "https://acmeplumbing.com/text-updates",
    "privacy_url": "https://acmeplumbing.com/privacy",
    "terms_url": "https://acmeplumbing.com/terms",
    "support_email": "help@acmeplumbing.com",
    "brand": { "legal_name": "Acme Plumbing LLC", "vertical": "CONSTRUCTION", "contact_email": "help@acmeplumbing.com", "contact_phone": "+14155550132" }
  },
  "suggested": ["business_name", "use_case", "description", "opt_in_url", "privacy_url", "terms_url", "support_email", "brand.legal_name", "brand.vertical", "brand.contact_email", "brand.contact_phone"],
  "sources": { "business_name": "page", "opt_in_url": "page" },
  "candidates": {
    "opt_in": [{ "url": "https://acmeplumbing.com/text-updates", "confidence": 1, "reason": "Has a phone number field and SMS consent wording" }],
    "privacy": [{ "url": "https://acmeplumbing.com/privacy", "confidence": 1, "reason": "Link labelled \"Privacy Policy\"" }],
    "terms": [{ "url": "https://acmeplumbing.com/terms", "confidence": 1, "reason": "Link labelled \"Terms of Service\"" }]
  },
  "about": "Licensed plumbers serving Springfield since 1998.",
  "ai": false,
  "policy_checks": { "privacy": true, "terms": true }
}
```

- `fields` is shaped like the body of `POST /v1/registrations`. Review it,
  add what only you know, and send it on. **We never suggest your EIN or
  entity type.**
- `suggested` names every field we filled in, and `sources` says where each
  came from: `page` (read from your pages by fixed rules) or `ai`.
- `candidates` lists the pages that could be your opt-in, privacy policy and
  terms, best first, each with the reason. `fields` holds the best one when
  we are confident enough; pick another from the list if we chose wrong.
- `policy_checks` says whether the suggested privacy policy and terms pass
  the [website check](#what-the-website-check-looks-for)'s rules for them (the no-sharing clause;
  SMS terms with STOP and HELP), by the same fixed rules.
- A site we cannot read returns `reachable: false` with the reason in
  `error`, and no suggestions.

**What we read.** We fetch your homepage and the few pages it links
to that look like your sign-up, contact, privacy policy and terms pages:
public pages only, under the same rules as the website check below. Names,
contact details, address and page links are read with fixed rules. Where
drafting is available (`ai: true`), we also use the
text of those public pages to draft the description, pick the use case and
industry, and write three example messages (`fields.sample_messages`).
Drafting uses nothing from your account
([Privacy §4](/privacy)), and can only choose a business name the page
states and an opt-in page from `candidates`. If its examples would not pass
the [example check](#your-example-messages) they are left out.

The console asks for facts only: its "Add a number" form never suggests a use
case or example messages, and nothing is drafted there. The
toll-free verification form reads your site with the fixed rules only, with no
drafting, and takes only your legal name, brand, address and policy pages.

**Suggestions are drafts.** Nothing is saved or filed by this call. Example
messages we draft are starter drafts: send them as `starter_messages` when
you create the registration and it keeps `samples_source: "starter"` until
you edit them (send `sample_messages`) or confirm them
(`samples_confirmed: true`). Suggestions can be wrong; you are responsible
for what you submit.

One read per website per account every 10 minutes: asking again sooner
returns the same suggestions. Limit: 10 websites a minute, separate from the
website check's limit.

## Use cases

| `use_case` | Registry use case | For |
| --- | --- | --- |
| `2fa` | 2FA | One-time passcodes |
| `account_notification` | ACCOUNT_NOTIFICATION | Account alerts and notices |
| `customer_care` | CUSTOMER_CARE | Support conversations |
| `delivery_notification` | DELIVERY_NOTIFICATION | Order and delivery status |
| `fraud_alert` | FRAUD_ALERT | Suspicious-activity alerts |
| `security_alert` | SECURITY_ALERT | Account security notices |
| `higher_education` | HIGHER_EDUCATION | Campus and enrollment updates |
| `marketing` | MARKETING | Offers and promotions |
| `mixed` | MIXED | Service messages and marketing |
| `polling_voting` | POLLING_VOTING | Surveys (not political) |
| `public_service_announcement` | PUBLIC_SERVICE_ANNOUNCEMENT | Community alerts |
| `low_volume` | LOW_VOLUME | Small mixed programs |

Pick the one that matches what you actually send. Sending a different kind of
message than you registered is a policy violation
([Messaging Policy §6](/messaging-policy)); register another campaign instead.

## What the website check looks for

| Finding | Passes when |
| --- | --- |
| `site_reachable` | Your website loads publicly over http(s). |
| `business_identity` | The business name appears in the page text (legal suffixes like LLC are ignored). |
| `privacy_policy_exists` | A privacy policy loads (we follow the "Privacy" link if you do not give a URL). |
| `privacy_no_sharing` | It says mobile / SMS opt-in data is not shared with third parties. |
| `terms_exist` | Terms of service load (we follow the "Terms" link). |
| `terms_sms_program` | The terms describe the SMS program, with STOP and HELP. |
| `optin_reachable` | The opt-in page loads. |
| `optin_phone_field` | It has a phone number field, or a "Text KEYWORD to NUMBER" instruction. |
| `optin_brand` | The disclosure names your brand. |
| `optin_message_types` | It says what messages to expect. |
| `optin_frequency` | "Message frequency varies", or a cadence like "4 msgs per week". |
| `optin_rates` | "Message and data rates may apply" ("Msg & data" also passes). |
| `optin_stop` | "Reply STOP to cancel". |
| `optin_help` | "Reply HELP for help". |
| `optin_links` | Links to the privacy policy and terms. |
| `optin_not_prechecked` | The consent checkbox starts unchecked. |

The check is fixed pattern matching. The generated
copy is written to pass it: paste the fix, recheck, and that finding passes.
Without an opt-in URL we check your homepage, and a missing phone field there
is a warning rather than a failure.

We fetch only the public pages you name (and the policy links on your
homepage), from our servers, with a short timeout and a size cap. Addresses
that are not on the public internet are never fetched. We keep the findings
and their short evidence snippets, not the pages.

## Your example messages

Carriers compare the examples on your registration with the texts you
actually send. Examples that do not match your traffic are one of the most
common reasons a registration is rejected, and a sender suspended later. So
the examples we file are **yours**, not ours.

Send them as `sample_messages`: 2 to 5 strings, 20 to 320 characters each,
written as a recipient would get them (a real-looking name, order number or
code, not a template variable). `description` (optional, 40 to 500
characters) says what you send and to whom; leave it out and we file a
generated one.

If you create a registration without `sample_messages`, it comes back with
**starter drafts** for your use case and `samples_source: "starter"`: our
templates, or the drafts written for your business if you sent
`starter_messages` from [Fill in from your website](#fill-in-from-your-website)
(drafts that would not pass the checks below are ignored). Starter
drafts are never filed: edit them, or send `samples_confirmed: true` to
`/recheck` if they already read like your texts. Either makes
`samples_source` `"customer"`.

`sample_check` holds the findings, in the same shape as the website check:

| Finding | Passes when |
| --- | --- |
| `samples_confirmed` | The examples are yours (edited or confirmed), not starter drafts. |
| `samples_count` | There are 2 to 5. |
| `samples_length` | Each is 20 to 320 characters. |
| `samples_brand` | Every example names your business (the name without LLC / Inc, its leading word, or its initials). |
| `samples_opt_out` | At least one says how to stop ("Reply STOP to opt out"). Required for `marketing` and `mixed`; a warning for other use cases; not asked of `2fa`. |
| `samples_links` | No public link shorteners. |
| `samples_placeholders` | No unfilled variables like `{{name}}` or `[Brand]`. |
| `samples_distinct` | No two examples are the same. |
| `samples_use_case` | `2fa` only: an example shows a code. |
| `samples_description` | Your own description, if you wrote one, is 40 to 500 characters. |
| `samples_consent` | Nothing says the program texts people who never signed up (listings, scraped or bought numbers). |
| `samples_content` | No example has content carriers refuse even with consent (high-risk lending, cannabis, gambling, phishing and the rest of the [Messaging Policy](/messaging-policy) prohibited list), the same rules every live send is screened with. |

These are fixed rules. They cannot tell whether an
example matches what you will send: that part is on you, and it is what
carriers judge. A registration is `ready` only when the website check
passes, the example check passes, and `samples_source` is `"customer"`. A
recheck that changes only the examples does not fetch your website again.

## Business verification

Carriers only accept registrations from real, verified businesses, so we
verify each business once, before its first registration is filed. Three
things make that possible:

- **An authorized representative.** The contact on the registration
  (`brand.contact_name`, `contact_title`, `contact_email`,
  `contact_phone`) is a person who works for the business and can act for
  it. Their email must be at the business's own domain: a free mailbox
  (gmail.com, outlook.com and the like) or a shared inbox (info@, sales@,
  support@) is refused when you save the details, with a message naming
  it. Carriers verify the business by writing to that person.
- **A street address.** The registered address is a street address in a
  US state or territory, with a ZIP code. A P.O. box is refused.
- **A signed attestation.** The first submit for a business carries a
  statement signed by the representative, or by someone else who can act
  for the business: that they are authorized, that the details match the
  business's IRS records, and that the business will text only people who
  agreed to it and will follow the Messaging Policy. In the console you
  type your name and title and tick the box; through the API you send it
  with the submit:

```bash
curl -X POST https://api.jointextkit.com/v1/registrations/reg_.../submit \
  -H "Authorization: Bearer $TEXTKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"attestation": {"signer_name": "Dana Smith", "signer_title": "Owner", "accepted": true}}'
```

Without a signature on file the submit answers `400 attestation_required`
(`param: "attestation"`). We keep each signature as signed: the exact
wording, the name and title, who sent it (the console user or the API key),
the IP address, the browser, and the date of our legal documents at the
time. It is never edited; a later signature is a new record.

After the signed submit the registration is `submitted` and our team checks
the business and the representative, usually within 1 business day. Part of
that check compares the business's names and the representative's name with
the US Treasury's sanctions list (OFAC): carriers do not register businesses
tied to sanctioned people or countries. A name that happens to match is
checked by a person, not refused automatically.
`guidance` says so (`next_actor: "delivered"`). Nothing is filed with
the carriers until the business is verified:

- **Verified**: the registration is filed from there, exactly as you
  submitted it (another `registration.updated` event with
  `change: "business verified; filed"`). Later registrations for the same
  business file straight away with no new signature.
- **Not verified**: the registration comes back `rejected` with
  `rejection.code: "business_unverified"`, the fix, and any note from our
  team. Correct the details, recheck, and submit again; that submit is
  signed again.

A signature covers the business's legal name, entity type, EIN, address and
representative. Changing any of those is a new business to us: the next
submit needs a new signature and a new review. Changing the website,
industry, use case or messages does not.

Live access for the account needs a verified business, so the first
verification is also what takes a sandbox account live when our team
approves it (see [Going live](#going-live)).

**Toll-free numbers.** A [toll-free verification](/docs/numbers#toll-free-numbers)
form is signed the same way when you submit it, but it does not wait for our
review: our team checks the form and sends it to the carriers, and when the
carriers verify it, the business on the form is verified with us too and a
sandbox account goes live. The exceptions: a business we have already
declined, or one whose sanctions-list check needs a person to look, stays
with our team.

## What we file, and the copy we keep

When you submit, we store an exact copy of what was filed: your business
details, example messages, description, the opt-in story, and how the checks
stood. A resubmission adds a new copy; a copy is never changed. Read them at
`GET /v1/registrations/{id}/submissions` or under **Filed versions** on the
[console Registration page](/console/registration), so you can always show
what the carriers reviewed. The copy is written when you submit, so while a
registration waits on [business verification](#business-verification) its
`filed_by` names our compliance team, who hold it until the business is
verified and then file it unchanged.

## Opt-in, opt-out and help wording

Three optional fields put your own words in what is filed:

| Field | What it is | Rules |
| --- | --- | --- |
| `opt_in_flow` | How people opt in, in your words. | 40 to 500 characters. The opt-in page, the disclosure and the START keyword are added after it. |
| `stop_reply` | The confirmation sent when someone opts out. | Names your business, says they are unsubscribed, no link. |
| `help_reply` | The reply to HELP. | Names your business, gives a way to reach you, keeps the STOP instruction. |

Leave them out and we file standard wording in your business name. A reply
that drops a required part is a 400 that names it. Numbers on the
registration send exactly the replies it filed.

`includes_links`, `includes_phone_numbers`, `age_gated` and
`direct_lending` are plain yes/no answers about your messages. A link or
phone number in an example counts as yes whatever you send.

## Generated copy

Every registration includes `generated`, built from your business name and
use case:

- `opt_in_cta`: the disclosure to place next to the phone field
- `privacy_clause` and `sms_terms`: paste into your privacy policy and terms
- `sample_messages`: the starter drafts (what is filed is the top-level
  `sample_messages`, repeated in `campaign.sample_messages`)
- `help_reply`, `stop_reply`, `opt_in_confirmation`
- `brand` and `campaign`: the full registration draft, including
  `message_flow`, keywords, and the embedded-link, embedded-phone,
  age-gated and direct-lending flags. `brand.missing` lists what the registry
  still needs from you (legal name, EIN, address, ...); send it in `brand`.

## Hosted opt-in page

No sign-up page of your own, or one that keeps failing the check? Turn on a
**hosted opt-in page** for the registration (console, the registration page,
"Hosted opt-in page"). We host a **sign-up page** for your business at
`jointextkit.com/optin/<your-page>`: a phone field and an unchecked
consent box, with every disclosure carriers look for (your business name,
what you send and how often, "Msg & data rates may apply", STOP and HELP)
and links to **your own** terms and privacy policy. We don't write those
for you: your privacy policy needs the line that mobile numbers and consent
are never shared for marketing. "Use as my opt-in page" files the sign-up
page as the registration's opt-in page. The page is not indexed by search
engines.

Sign-ups are **double opt-in**. A sign-up is kept as a request
(`opt_in_request` in the number's [consent history](/docs/opt-out)) with
its evidence: the exact wording the person agreed to, the page, their IP
address and browser, and the time. Once a number on the registration is
approved and your account is live, that number texts them to reply YES. Their
YES (or START) within 7 days makes it an
[opt-in on file](/docs/compliance-checks#opt-in-on-file), with that evidence
and the reply's message id, fires `message.opted_in` with
`method: "hosted_page"`, and sends your confirmation text
(`generated.opt_in_confirmation`). Before then, nothing counts: a sign-up
while your account cannot text yet stays a request.

- Typing someone else's number signs nobody up, and the page answers every
  sign-up the same way, so it never shows whether a number was texted or
  opted out.
- A number that opted out is **not** signed back up and gets no text: the
  request is kept as evidence, and only a START from that phone lifts it.
- US and Canadian numbers only; at most 10 sign-ups an hour from one network
  and 3 a day per number; at most 50 "reply YES" texts per page an hour (a
  sign-up past that is kept as a request, untexted).
- Turning the page off stops sign-ups; its address and every record stay.

**Toll-free numbers.** Verifying a toll-free number does not need a
registration. When you have no hosted page and no opt-in page of your own,
the toll-free verification form comes with one: the same three pages,
written from the form (the name on your texts, what you send, your website
and contact) and kept in step with it until you submit. The form's opt-in
page, terms and privacy addresses and its description of how people opt in
are filled in from it. A sign-up there is an opt-in on file for the account
(a toll-free number has no registration to scope it to), and once one of
your toll-free numbers is verified it also gets a confirmation text from
that number.

It is a convenience, not legal advice: you remain responsible for using the
consent the way you described it.

## API

```bash
# Create (runs the website check immediately)
curl -X POST https://api.jointextkit.com/v1/registrations \
  -H "Authorization: Bearer $TEXTKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "business_name": "Acme Plumbing LLC",
    "website": "https://acmeplumbing.com",
    "opt_in_url": "https://acmeplumbing.com/text-updates",
    "use_case": "customer_care",
    "support_email": "help@acmeplumbing.com",
    "sample_messages": [
      "Acme Plumbing: Hi Sam, your plumber Dana arrives tomorrow between 9 and 11 AM. Reply STOP to opt out.",
      "Acme Plumbing: Your job #4821 is done. Reply here with any questions about the repair."
    ],
    "brand": { "legal_name": "Acme Plumbing LLC", "ein": "12-3456789" }
  }'

# Fix the site, then recheck (any field can be corrected in the same call)
curl -X POST https://api.jointextkit.com/v1/registrations/reg_.../recheck \
  -H "Authorization: Bearer $TEXTKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"opt_in_url": "https://acmeplumbing.com/signup"}'

# Submit once status is "ready". Every submit acknowledges the fee terms (see
# Registration fees); the first submit for a business also carries the signed
# attestation (see Business verification), later ones need none.
curl -X POST https://api.jointextkit.com/v1/registrations/reg_.../submit \
  -H "Authorization: Bearer $TEXTKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"fees_acknowledged": true, "attestation": {"signer_name": "Dana Smith", "signer_title": "Owner", "accepted": true}}'

# Get one / its filed copies / list all
curl https://api.jointextkit.com/v1/registrations/reg_... \
  -H "Authorization: Bearer $TEXTKIT_API_KEY"
curl https://api.jointextkit.com/v1/registrations/reg_.../submissions \
  -H "Authorization: Bearer $TEXTKIT_API_KEY"
curl https://api.jointextkit.com/v1/registrations \
  -H "Authorization: Bearer $TEXTKIT_API_KEY"
```

A response, trimmed:

```json
{
  "id": "reg_a1B2c3D4e5F6",
  "object": "registration",
  "status": "checks_failed",
  "guidance": {
    "what_happened": "The website check found 1 problem: consent box is not pre-checked.",
    "why": "Carrier reviewers reject registrations whose website or opt-in is missing these disclosures. Fixing them now avoids a rejection later.",
    "next_actor": "you",
    "next_step": "Fix each failed item using its copy-paste text, publish the change on your website, then click Recheck website. If a URL was wrong, correct it when you recheck.",
    "after": "When every check passes the registration becomes Ready to submit."
  },
  "check": {
    "passed": 15, "failed": 1,
    "findings": [{
      "id": "optin_not_prechecked",
      "status": "fail",
      "evidence": { "url": "https://acmeplumbing.com/text-updates", "snippet": "<input type=\"checkbox\" name=\"sms_consent\" checked>" },
      "why": "Consent must be an action the person takes...",
      "fix": { "summary": "Remove the `checked` attribute from the consent checkbox...", "copy": null }
    }]
  },
  "rejection": null,
  "generated": { "opt_in_cta": "By checking this box, I agree to receive ..." }
}
```

States: `draft` → `checks_failed` / `ready` → `submitted` → `in_review` →
`approved` / `rejected`. Recheck works in `draft`, `checks_failed`, `ready`
and `rejected`; submit only in `ready`. Anything else returns **409**
`invalid_state` with a message saying what to do instead. Creating and
rechecking share a budget of 20 website checks per minute per account.

## Rejections

A rejection carries `rejection.code`, a plain `explanation`, the exact `fix`,
any `note` from our compliance team, and `related_findings`; those findings are
marked `flagged_by_rejection` on the next recheck. Codes:
`website_unreachable`, `brand_mismatch`, `privacy_sharing_clause`,
`terms_missing_sms`, `optin_disclosure_incomplete`, `optin_not_verifiable`,
`prechecked_consent`, `samples_missing_brand`, `samples_use_case_mismatch`,
`message_flow_unclear`, `url_shortener_or_link_mismatch`,
`restricted_content`, `business_unverified` (our team could not verify the
business before filing; see [Business verification](#business-verification)),
`other`.

## Events

`registration.updated` fires on every status change (pollable at
`/v1/events`, pushed to webhooks). `data` has `id`, `status`,
`previous_status`, `change` (e.g. `resubmitted`), and `reason_code` on
rejections.

## Sending before approval

A live number is **test only until its registration is approved**: it can
text your verified numbers (your own phone, and any number you verify under
Billing → Verified numbers), so you can build and test with real texts while
the carriers review. It cannot text anyone else yet. Carriers block traffic
from unregistered numbers, so we stop it before it leaves rather than let it
fail downstream.

A send to an unverified recipient from a number that is not yet active
answers:

```
HTTP/1.1 403
X-TextKit-Sender-State: test_only | pending | action_needed

{ "error": { "code": "sender_not_registered", "param": "from", "message": "+14155550132 is still being registered with the carriers, so it cannot text this recipient yet. ..." } }
```

Nothing is sent and nothing is charged. Once the registration is approved,
every live number on the account is linked to it automatically and becomes
`active`; watch `sender.state` on [the number](/docs/numbers#sender-status)
or subscribe to `number.sender_updated`. Test keys and sandbox numbers are
never affected.

A toll-free number follows the same rule until the carriers verify it. An
account that bought one before it had live access can send live with a live
key, but until the account is live, only from that toll-free number and only
to your verified numbers; a send from any other number answers the same
`403 sender_not_registered`, saying your account is not live yet (see
[Toll-free numbers](/docs/numbers#toll-free-numbers)).

## Registration fees

Carrier registration fees are **separate from the per-segment rate**, billed
at twice what the registry or carrier charges us ([Terms §4.3](/terms)). There are three: a one-time **brand** fee
when your business is first registered, a **vetting** fee each time a
campaign is filed (a resubmission is filed again), and a **monthly campaign
fee** for as long as the campaign is registered, billed for a **minimum of
3 months** from approval. They are **non-refundable, including when a
registration is rejected**: the registry charges us whether or not it
approves. No page prints an amount, because the amount is whatever the
carrier bills, and it can change.

Submitting asks you to acknowledge exactly that. In the console it is a box
on the review step; over the API the submit body must carry
`fees_acknowledged: true` (otherwise **400** `invalid_request`, param
`fees_acknowledged`, and nothing is filed). The acknowledgment is kept on
the filed copy (`fee_acknowledgment`: the wording, the time, your IP).

Each fee is recorded on the registration as the registry charges us,
starting as an estimate that our team confirms against the carrier's bill,
and is listed under **Registration fees** on the
[Registration page](/console/registration) and on
[Billing](/console/billing). Fees are billed on **their own invoice**, never
from your prepaid credits: with a saved card on the account the invoice is
charged to it; without one it is emailed, with **14 days** to pay. The
account's owner and admins get an email either way, and a receipt when it is
paid. A fee invoice that cannot be charged never pauses sending; you get the
link to pay it, and our team is told.

If our team records an approval by mistake (nothing was filed with the
carriers, so they charged nothing), we undo it and waive the fees that
approval started. You are never billed a campaign fee for an approval the
carriers did not make; an approval that later happens for real starts them
again.

## Going live

Submitting a registration from a sandbox account is also your request for
live access. There is no second form: we read the business, website, use
case and opt-in from the registration. You can keep building in the sandbox
while both are reviewed.

A registration must be complete before it can be submitted. If business
details are missing (legal name, tax ID, address, industry, the
representative's name, title, work email and phone), submit answers **400**
naming them, and the registration stays `ready`. The first submit for a
business also needs the signed [attestation](#business-verification), and
our team verifies the business before filing anything; live access is
approved only for an account with a verified business.

Carrier review usually takes 3 to 7 business days. If it runs past 10 days we
flag the registration (`review_stalled: true`), our team follows up with the
carriers, and your numbers show `action_needed` with an explanation, so a
review never sits silently.

## Opt-in proof

[Messaging Policy §1.4](/messaging-policy) asks you to keep evidence of every
consent. Record it with the opt-in and it lives in your consent ledger, next to
the opt-in it proves:

```bash
curl -X POST https://api.jointextkit.com/v1/consent/+14155550132 \
  -H "Authorization: Bearer $TEXTKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "opted_in",
    "proof": {
      "source": "web_form",
      "collected_at": "2026-09-30T18:04:05Z",
      "page_url": "https://acmeplumbing.com/text-updates",
      "disclosure": "By checking this box, I agree to receive ...",
      "ip": "203.0.113.7",
      "user_agent": "Mozilla/5.0 ...",
      "registration_id": "reg_a1B2c3D4e5F6"
    }
  }'
```

| Field | |
| --- | --- |
| `source` | Required. `web_form`, `keyword`, `paper`, `checkout`, or `verbal`. |
| `collected_at` | When they consented (ISO or epoch ms). Defaults to now. |
| `page_url` | Required for `web_form`. |
| `disclosure` | The exact wording they saw (up to 2,000 characters). |
| `ip`, `user_agent` | For online forms. |
| `registration_id`, `campaign_id` | The registration or carrier campaign it was collected under. |

`GET /v1/consent/{phone}` returns `proof` on the history entry, and
`GET /v1/consent/export?type=ledger` exports every consent event with
`proof_*` columns. Keep proof for at least four years after your last message
to the number.

This page is educational, not legal advice.
