Markup
Formatting is written the way Telegram writes it:
Plus two of ours:
A premium custom emoji is written as
. The emoji with that document id is drawn over the character in the brackets, and that character has to be a single emoji itself: it is what a reader whose client cannot resolve the id goes on seeing, and it is what the visible length counts.
Escaping
Only ten characters mean anything and therefore need escaping:[, where it would begin the custom-emoji form — write \! there for a literal one.
The two rules that refuse most texts
That is whydestinationUrl is required when validating: it is what defines “your own domain”, and the foreign-link rule cannot be evaluated without it. While a form field is still being typed, the URL is accepted with or without a scheme; anything that is not a URL either way is a 400.
Length
The limit is 700 visible characters — markup removed,-- already an em dash — counted in UTF-16 code units, so an emoji counts as 2. visibleLength in the validation response is exactly the number to put in an editor’s character counter.
The separate maxLength on the request field is a bound on request size, an order of magnitude above anything that could pass 700. Exceeding it is a 400, not a validation error.
Validating
offset and length so an editor can underline it.
offset and length address the marked-up string you sent, not the visible text — that is where the mistake is. The visible text does not contain the * that was never closed. Both are in UTF-16 code units.Error codes
Branch oncode, never on message — the wording is free to change.
Generating a draft
destinationUrl are carried into the generated text character for character — the generator is told they are load-bearing and must not be renamed, completed, or tidied away with the rest of the query string.
Generated copy is a draft, not a verdict. Validate it before you use it.
When a channel owner rewrites your ad
If the campaign hascreativeChangesAllowed (the default), a channel owner may propose his own wording. He types plain text into Telegram and the backend writes his formatting back into this same markup, so both texts render in one editor and the diff is between two texts of one kind:
originalAdCopy— your text, carried onto the placement so the card can show the diff on its own.proposedAdCopy— his variant, set only while the placement isAWAITING_CREATIVE_APPROVAL.approvedAdCopy— his variant once accepted; the text actually published.