Send an SMS

Sends an SMS from one of your numbers.

POST/v1/messages

Body Parameters

  • tostringrequired

    Example: +15005550006.

  • fromstring

    One of your numbers, or a sender pool id (pool_...): TextKit picks a pool member, the same one for each recipient every time. Optional when the account has exactly one number for the key's mode. Example: +15005550100.

  • bodystringrequired

    At most 1600 characters.

  • customer_idstring

    Optional. Attribute to one of your customers. Defaults to the customer the from-number is assigned to; a number assigned to another customer is rejected (400).

  • scheduled_atstring

    Optional. Future ISO timestamp (or epoch ms), up to 30 days out. The message is queued and the response is a scheduled_message object instead of a message. A marketing message scheduled into the recipient's quiet hours is refused now (403 quiet_hours) or, with quiet_hours defer, moved to their next 8:00am; any other message is moved.

  • categorymarketing, transactional, otp

    Optional. It can make a message stricter (marketing), never looser: what a message is comes from the sending number's registration and its wording (which messages are marketing). A message outside its registration's use case is refused with 403 campaign_mismatch.

  • quiet_hoursblock, defer

    Optional. What to do with a message in the recipient's quiet hours: block refuses it (403 quiet_hours), defer holds it until their next 8:00am. Omitted: the account default (a single marketing send is refused; every other message is held). One-time codes are never held.

  • shorten_linksboolean

    Optional. Rewrite every http(s) link in body to a short link on your active link domain (POST /link-domains), with clicks tracked (links on the message, message.link_clicked events). The links are checked as you wrote them; segments are counted on the shortened text. Without an active link domain the request is refused with 400 link_domain_required: short links are never put on a shared domain. Works with scheduled_at.

  • tagsarray of string

    Optional. Your own labels, up to 10 (letters, digits, space _ . : -; up to 32 characters each; lowercased), for example a campaign or a flow. Stored on the message, sent on its webhook events, and reported by GET /v1/deliverability?group_by=tag. Filter with GET /v1/messages?tag=. At most 10 items.

  • consentobject

    Optional. How the recipient agreed to your messages. A recurring non-marketing message from a number linked to a registration needs an opt-in on file for that registration; when none is, this records it and the message is sent. Never counts for marketing (use POST /v1/consent with proof). Not with scheduled_at.

  • consent.sourceweb_form, keyword, paper, checkout, verbalrequired
  • consent.collected_atstringrequired

    When they agreed: ISO timestamp or epoch ms, not in the future.

  • consent.page_urlstring (uri)

    Required for web_form: the page the form was on.

  • mediaarray of string (uri)

    Reserved for MMS; currently returns 400 mms_not_enabled.

  • status_callbackstring (uri)

    Optional. Also send this message's status events (message.sent, message.delivered, message.failed, message.blocked) to this https URL, signed with your callback secret (POST /v1/webhooks/callback_secret; 400 callback_secret_required until it exists). Logged and retried like a webhook endpoint, under endpoint status_callback. Immediate sends only. Example: https://example.com/sms/status.

Headers

  • Idempotency-Keystring

    Makes retries safe for 24 hours: the same key and body replays the original successful response (with Idempotent-Replayed: true); a different body returns 409. Failed requests release the key. At most 255 characters.

Details

In test mode the message is simulated: it reaches delivered but no text is sent (simulated: true, an X-TextKit-Simulated header, and a notice when to is a real-looking number). The one exception is a send to your own verified phone, which is really delivered when a verification sender is available (simulated: false).

from may be omitted when the account has exactly one number for the key's mode (the sandbox number with a test key, the one live sending number with a live key); with none or several the request is refused and the error lists the choices under numbers. Supports the Idempotency-Key header, including with scheduled_at.

A live send from a US local number that is not yet linked to an approved registration (sender.state other than active) is delivered only to your verified numbers; to anyone else it answers 403 sender_not_registered with an X-TextKit-Sender-State header (test_only | pending | action_needed). The same holds for a toll-free number until its toll_free.status is verified.

An account without live access that owns a live toll-free number may use a live key here: until the account is live, a live send goes only from that toll-free number to your verified numbers (billed like any live send); any other live send answers 403 sender_not_registered.

Responses

  • 201Message

    Message accepted

  • 202Message or ScheduledMessage

    Accepted and not yet sent. Either a Message with status queued (a temporary carrier failure is being retried automatically, next_attempt_at set), or a ScheduledMessage with a deferred object (a message held for quiet hours, set for the recipient's next 8:00am).

  • 400Error

    Invalid request, or link_domain_required (shorten_links with no active link domain)

  • 401Error

    Missing, malformed, or revoked API key

  • 402Error

    Billable live calls only: the prepaid credit balance cannot cover the call (insufficient_credits); nothing was done or charged

  • 403Error

    Refused by a send check: campaign_mismatch (the message is outside the sending number's registration: promotional wording or a marketing label on a non-marketing registration, anything but a one-time code from a two-factor registration, or a kind of message our registration check found does not match; with registration_id, use_case and mismatch, plus checked_by consistency_check and fingerprint when the check refused it), recipient_opted_out, quiet_hours (with recipient_local_time, timezone, send_after), consent_required (a marketing message, or a recurring message from a registered number, to a number with no opt-in on file: for the registration of the sending number when it has one, with registration_id and consent_scope), reconfirmation_pending / reconfirmation_expired (a marketing recipient not texted in 30 days is asked to confirm first; see /docs/compliance-checks), campaign_paused (marketing paused on the registration of the sending number, for a high opt-out rate), held_for_review (a new kind of message from an account in its first 30 days of live sending, or one the registration check could not place, held for our team to review; with held, fingerprint, registration_id and expires_at), review_rejected (a kind of message our team did not approve for the registration; with fingerprint), sender_not_registered, recipient_not_verified, spend_limit_reached, or forbidden (content, destination). The attempt is stored as a message with status blocked; its id is message_id in the error.

  • 409Error

    Idempotency conflict

  • 429Error

    Rate limit or quota exceeded

  • 502Error

    The carrier permanently rejected the message (carrier_error)

  • 503Error

    The content screen could not check the message (screen_unavailable), so it was not sent or charged; retry after Retry-After. Stored as a blocked message; its id is message_id in the error.

Response Fields (201)

  • idstring

    Example: msg_a1B2c3D4e5F6g7H8.

  • objectmessage
  • tostring

    Example: +15005550006.

  • fromstring

    Example: +15005550100.

  • bodystring
  • directionoutbound, inbound
  • statusqueued, sent, delivered, failed, blocked, received

    blocked: refused by one of our checks before any carrier saw it (opt-out, quiet hours, unregistered sender, content, allowance, spend limit, credits). Final, never charged; failure says why. failed: accepted, then not delivered.

  • testboolean
  • simulatedboolean

    True when nothing was handed to a carrier: every test-key message except one sent to your own verified phone. Always false on live messages. A simulated message still reaches delivered.

  • noticestring

    Only on the response to POST /messages. Present when a test-key send to a real-looking number was simulated: says nothing was sent and how to send for real.

  • schedule_idstring

    The recurring schedule this message is an occurrence of. Present only when set.

  • tagsarray of string

    The labels sent with the message (tags on POST /messages), lowercased. Present only when set.

  • categorymarketing, transactional, otp

    How the message was classified. Present when you sent one, or when it was classified as marketing.

  • disclosures_addedarray of brand, opt_out

    Wording we added to a message from a number linked to a registration (what we add): brand, your business name in front of a person's first message; opt_out, " Reply STOP to opt out." on marketing that had none. body is the text as sent. Present only when we added something.

  • deferredobject

    Present when the message was held for quiet hours and sent when the window opened.

  • deferred.reasonquiet_hours
  • deferred.held_atstring (date-time)
  • redacted_atstring (date-time)

    When the message was redacted (POST /messages/{id}/redact): body is empty and media is gone. Present only then.

  • linksarray of object

    Links shortened with shorten_links, in the order they appear. On the send response and GET /messages/{id} (with clicks so far); not on list pages. body is the text as sent, with the short links.

  • links[].urlstring

    The link as you wrote it.

  • links[].short_urlstring

    Example: https://links.example.com/Ab3dEf9h.

  • links[].clicksinteger

    Opens by people. Link previews (a messaging app fetching the link to draw a preview) are not counted.

  • links[].first_clicked_atstring (date-time)

    Nullable.

  • links[].last_clicked_atstring (date-time)

    Nullable.

  • created_atstring (date-time)
  • mediaarray of string (uri)

    MMS attachments (inbound today).

  • sent_bystring

    Console user or API key that composed it.

  • failure_reasonstring

    The carrier's raw text for a failed send. Kept for compatibility; see failure.

  • attemptsinteger

    Live sends: carrier submissions so far. More than 1 means it was retried.

  • next_attempt_atstring (date-time)

    Present while queued for an automatic carrier retry.

  • receipt_statusmissing

    The carrier accepted the message but sent no delivery report within 72 hours. status stays sent.

  • customer_idstring

    The customer this is attributed to. Present only when set.

  • timelinearray of object

    Every status transition, oldest first. Inbound messages have a single received.

  • timeline[].statusaccepted, validated, queued, sent_to_carrier, carrier_accepted, retry_scheduled, delivered, failed, blocked, received
  • timeline[].atstring (date-time)
  • timeline[].attemptinteger

    Carrier attempt this step belongs to (from sent_to_carrier on).

  • segmentsinteger

    Parts the body is split into (GSM-7: 160, then 153 each; UCS-2: 70, then 67). Example: 1.

  • encodinggsm7, ucs2
  • priceobject

    What this message costs, in USD: the outbound rate per segment, plus the recipient's carrier fee per segment, at cost, as its own line. Null on messages sent before prices were recorded. Nullable.

  • price.totalnumber

    Example: 0.0082.

  • price.currencyusd
  • price.breakdownarray of object
  • price.breakdown[].labelstring
  • price.breakdown[].amountnumber
  • destination_carrierstring

    The recipient's mobile carrier, when a cached lookup knows it. Nullable.

  • failureFailure

    Null unless status is failed. Nullable.

Errors

Every error has the same JSON shape, and request_id matches the X-Request-Id response header. Errors lists every code and what to do about it.

  • 400

    Invalid request, or link_domain_required (shorten_links with no active link domain)

  • 401

    Missing, malformed, or revoked API key

  • 402

    Billable live calls only: the prepaid credit balance cannot cover the call (insufficient_credits); nothing was done or charged

  • 403

    Refused by a send check: campaign_mismatch (the message is outside the sending number's registration: promotional wording or a marketing label on a non-marketing registration, anything but a one-time code from a two-factor registration, or a kind of message our registration check found does not match; with registration_id, use_case and mismatch, plus checked_by consistency_check and fingerprint when the check refused it), recipient_opted_out, quiet_hours (with recipient_local_time, timezone, send_after), consent_required (a marketing message, or a recurring message from a registered number, to a number with no opt-in on file: for the registration of the sending number when it has one, with registration_id and consent_scope), reconfirmation_pending / reconfirmation_expired (a marketing recipient not texted in 30 days is asked to confirm first; see /docs/compliance-checks), campaign_paused (marketing paused on the registration of the sending number, for a high opt-out rate), held_for_review (a new kind of message from an account in its first 30 days of live sending, or one the registration check could not place, held for our team to review; with held, fingerprint, registration_id and expires_at), review_rejected (a kind of message our team did not approve for the registration; with fingerprint), sender_not_registered, recipient_not_verified, spend_limit_reached, or forbidden (content, destination). The attempt is stored as a message with status blocked; its id is message_id in the error.

  • 409

    Idempotency conflict

  • 429

    Rate limit or quota exceeded

  • 502

    The carrier permanently rejected the message (carrier_error)

  • 503

    The content screen could not check the message (screen_unavailable), so it was not sent or charged; retry after Retry-After. Stored as a blocked message; its id is message_id in the error.

Idempotency

Send an Idempotency-Key header to make retries safe. For 24 hours the same key with the same body replays the original successful response (with Idempotent-Replayed: true); the same key with a different body returns 409. A failed request releases its key.