Messaging (10DLC)
Register the workspace for US A2P 10DLC texting: check status, file the brand and campaign, refresh, and verify sole-proprietor PINs.
US carriers block application-to-person texts from local numbers unless the sender is registered with The Campaign Registry (TCR). The registration has two parts: a brand (who you are) and a campaign (what you text and how people opt in). This resource runs that registration for the workspace. It's the same pipeline as Settings → Texting in the app.
#The pipeline
none → profile_ready → brand_pending ──→ brand_ok → campaign_pending → campaign_active ✓ canText
│ │
├→ brand_otp_pending (sole proprietors: submit the PIN)
└→ brand_failed └→ campaign_failedCarrier review usually takes minutes to a few days. Poll GET /messaging, or call POST /messaging/refresh to re-check right away.
#The messaging status object
stepstring — Where the registration is. See the pipeline above.canTextboolean —trueonce the campaign is carrier-approved. Until then,POST /messagesreturns409.messagingProfileIdstring | null — The carrier messaging profile.brandobject | null —status,identityStatus,failureReasons. For sole proprietors, alsootpSentAtandotpMobileLast4.campaignobject | null — Campaign submission and carrier status.
curl https://api.ody.co/v1/api/messaging \
-H "Authorization: Bearer $ODY_API_KEY"const status = await ody.messaging.status();
if (status.canText) console.log("Ready to text");status = client.messaging.status(){
"messaging": {
"step": "campaign_pending",
"canText": false,
"messagingProfileId": "40019…",
"brand": { "status": "OK", "identityStatus": "VERIFIED", "failureReasons": [] },
"campaign": { "status": "PENDING" }
}
}#Submit a registration
/v1/api/messaging/registrationFiles the brand with TCR and stages the campaign. The campaign is created automatically once the brand is accepted.
A live filing ends a running free trial and charges the plan, plus the one-time $20 setup fee per local number, to the card on file immediately, before carriers accept anything. A resubmission never charges the same number again. The $4/mo messaging add-on starts when the campaign is approved.
- A trialing workspace must send
acknowledge_trial_end: true, confirming the customer was told. Otherwise you get409. - With no payment method, a canceled subscription, or (while trialing) no submitted identity check, you get
409"Add a payment method…". - If the card declines, nothing is filed.
- The owner gets an emailed receipt with a masked copy of the application, and every filing is audit-logged with actor
api.
With a test key, the filing always runs in mock mode: the full pipeline runs, but nothing is submitted to TCR, no fee is charged and the billing gate is skipped.
Scope: numbers:manage
Body
businessobject required — The legal identity. See below.campaignobject — Campaign settings. Sensible defaults are applied when you omit it.acknowledge_trial_endboolean — Required (true) on a free trial.mockboolean — Force mock mode on a live key (no fee, nothing filed).
business
entityTypestring —PRIVATE_PROFIT(default),PUBLIC_PROFIT,NON_PROFIT,GOVERNMENTorSOLE_PROPRIETOR.displayNamestring required — The brand name customers see.emailstring requiredverticalstring required — TCR vertical, for examplePROFESSIONAL,RETAIL,REAL_ESTATE.street,city,state,postalCodestring requiredcompanyName,einstring — Required unlessSOLE_PROPRIETOR. EIN is 9 digits; the dash is optional.firstName,lastName,mobilePhonestring — Required forSOLE_PROPRIETOR. TCR texts a PIN tomobilePhone.phonestring — Support phone, in E.164 or 10-digit US format.websitestringcountrystring — ISO 3166 alpha-2. DefaultUS.
campaign
useCasestring — DefaultMIXED.descriptionstring — At least 40 characters. Built from the business name and use case when omitted.samplesarray of strings — Up to 5 example messages.optInobject — Preferred.{ method: "web_form", url, location, formConfirmed: true, screenshotUrl? }. Ody writes a carrier-ready opt-in description from it. It overridesmessageFlow.messageFlowstring — 40–2048 characters describing one concrete opt-in method. Carriers reject descriptions that list several methods or point at a form that doesn't exist.optinMessage,helpMessagestringoptinKeywordsstring — DefaultSTART.embeddedLink,embeddedPhoneboolean — Whether your messages contain links or phone numbers.
Returns 201 with { messaging, mock? }. A 409 means TCR or the carrier rejected the filing (the message says why) or a billing gate blocked it.
curl https://api.ody.co/v1/api/messaging/registration \
-H "Authorization: Bearer $ODY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"business": {
"displayName": "Acme Plumbing",
"companyName": "Acme Plumbing LLC",
"ein": "12-3456789",
"email": "[email protected]",
"vertical": "PROFESSIONAL",
"street": "100 Congress Ave",
"city": "Austin",
"state": "TX",
"postalCode": "78701",
"website": "https://acmeplumbing.com"
},
"campaign": {
"useCase": "CUSTOMER_CARE",
"samples": ["Hi Ada, your plumber is on the way and will arrive by 2pm. Reply STOP to opt out."],
"optIn": {
"method": "web_form",
"url": "https://acmeplumbing.com/contact",
"location": "the quote form at the bottom of the contact page",
"formConfirmed": true
}
},
"acknowledge_trial_end": true
}'const { messaging } = await ody.messaging.register({
business: {
displayName: "Acme Plumbing",
companyName: "Acme Plumbing LLC",
ein: "12-3456789",
email: "[email protected]",
vertical: "PROFESSIONAL",
street: "100 Congress Ave",
city: "Austin",
state: "TX",
postalCode: "78701",
},
campaign: {
useCase: "CUSTOMER_CARE",
optIn: { method: "web_form", url: "https://acmeplumbing.com/contact", location: "the quote form at the bottom of the contact page", formConfirmed: true },
},
acknowledgeTrialEnd: true,
});res = client.messaging.register(
business={
"displayName": "Acme Plumbing",
"companyName": "Acme Plumbing LLC",
"ein": "12-3456789",
"email": "[email protected]",
"vertical": "PROFESSIONAL",
"street": "100 Congress Ave",
"city": "Austin",
"state": "TX",
"postalCode": "78701",
},
campaign={"useCase": "CUSTOMER_CARE", "optIn": {"method": "web_form", "url": "https://acmeplumbing.com/contact", "location": "the quote form at the bottom of the contact page", "formConfirmed": True}},
acknowledge_trial_end=True,
)#Refresh status
/v1/api/messaging/refreshAsks the carrier for the latest brand and campaign status and moves the pipeline forward (for example, it creates the campaign once the brand is accepted).
Scope: numbers:manage
Returns { messaging }.
curl -X POST https://api.ody.co/v1/api/messaging/refresh \
-H "Authorization: Bearer $ODY_API_KEY"const status = await ody.messaging.refresh();status = client.messaging.refresh()#Verify a sole-proprietor PIN
/v1/api/messaging/otp/verifyOnly used while step is brand_otp_pending. When a SOLE_PROPRIETOR brand is filed, TCR texts a PIN to business.mobilePhone. Submit it here. A correct PIN verifies the brand and creates the campaign immediately, so the response step is usually campaign_pending. PINs expire after 24 hours.
Scope: numbers:manage
Body
pinstring required
Returns { messaging }. Returns 400 for a wrong or expired PIN, or 409 if the registration isn't waiting for one.
curl https://api.ody.co/v1/api/messaging/otp/verify \
-H "Authorization: Bearer $ODY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"pin":"482913"}'await ody.messaging.verifyOtp("482913");client.messaging.verify_otp("482913")#Resend a sole-proprietor PIN
/v1/api/messaging/otp/resendTexts a fresh PIN to business.mobilePhone. You can request one per minute; asking again sooner returns 429.
Scope: numbers:manage
Returns { messaging }.
curl -X POST https://api.ody.co/v1/api/messaging/otp/resend \
-H "Authorization: Bearer $ODY_API_KEY"await ody.messaging.resendOtp();client.messaging.resend_otp()