Skip to main content
The one direction where you are the server. Ask for it with "dlr": "yes" and a dlr_url on the send.

json — the default

A POST with Content-Type: application/json:

Fields

Null fields are omitted rather than sent as nulls, so a receipt for a message that never reached a vendor does not arrive full of empty keys. Every call carries User-Agent: FireFlo/<version>.
err is new. Receipts sent before it existed carried no such key, so it is additive: nothing else in this body moved, and a receiver that ignores keys it does not recognise needs no change.

What err means, and what it does not

err is the same value the SMPP err: field carries for the same failure, so a customer taking receipts over a webhook and one taking them over SMPP are told the same thing. An omitted err means nothing reported a code, not that the code was zero. A delivered message usually carries none, and neither does a receipt FireFlo raised itself about a message no carrier ever saw. The value depends on what your provider has configured, and there are three possibilities:
  • A code FireFlo recognises, from its own documented set. This is the useful case: the same number means the same real cause whichever carrier the message went through.
  • 018, unknown. The default for a code your provider has not mapped. It is not a real reason, and it is the reason this field is worth asking them about.
  • A carrier’s own code, passed through unchanged — only where your provider has enabled that per carrier. Carriers number their failures privately, so 630 from one is unrelated to 630 from another, but it is far more useful than 018 because it can be looked up.
So do not assume every err you receive is in the documented set. Treat an unrecognised value as opaque, log it, and quote it when you raise a query — your provider can tell you what that carrier means by it, and map it so you get a stable code next time.
vendor is absent unless your provider enables it. It names which supplier carries your traffic, which is withheld from customers by construction elsewhere. err is not withheld with it — which supplier carried the message is commercially sensitive, but why your message failed is yours.

kannel — placeholder substitution

A bare GET against the registered URL with two placeholders replaced. Nothing is appended.
A URL without placeholders receives a request carrying nothing at all — no body, no query, no identifier. The endpoint is hit on schedule, returns 200, and the integration looks like it is “working but empty”.Nothing in the system flags this, because an empty callback is indistinguishable from a correctly configured one that happens to carry no data.
The format is chosen by your provider with smsg.dlr.forward.format, not by you. dlr-method is accepted on a send but does not select it.

Delivery and retries

A 3xx stopped counting as success in 0.11. A redirect to a login page used to be read as delivered and the receipt was dropped; it is now retried like any other failure, and reported failed if it never succeeds. If you relied on answering 301 or 302, answer 200 instead.A 2xx from a proxy that never reached your application still counts as success — nothing can see past it. If receipts “arrive” but your handler never runs, look at what is in front of it.
Delivery is at least once — make your handler idempotent on id plus level, not on id alone, since a message can legitimately produce a level 1 and then a level 2.

Which receipts you get

dlr_level on the send: 1 progress only, 2 the outcome (default), 3 both. Over SMPP the same choice is the registered_delivery octet — 0x11 is the SMPP spelling of dlr_level: 3.

Every receipt is recorded

Whether or not it is forwarded. Suppressing a receipt is a decision about what to tell you, never about what to record — so “I never got a receipt” is always answerable by your provider.

Delivery receipts

Building against this, with the level trap explained.

Callbacks not arriving

Diagnosing the silence.