AltScore
Channel Connectors

Delegated OTP delivery

Some customers need the code to leave through their own channel rather than ours: their recipients only accept mail from their domains, or the notice has to come from the system that already talks to those people. The case that motivated this channel is a promissory note, where the code that signs the document has to reach the end customer through the customer's own channel.

The delegated channel covers exactly that, and nothing else. The only thing delegated is the transport.

What stays with us

Read this table first, because it is the difference between a delivery channel and a verification channel.

Part of the OTPWho does it
Generating the codeAltScore
Expiration and maximum attemptsAltScore
Delivering the code to the personThe customer, at their endpoint
Asking for the code and verifying itAltScore, on the same screen as always
Signing the authorization and its evidenceAltScore

The person types the code into our screen, exactly as they would with WhatsApp, SMS, or email. That is why the authorization ends up with hasSignatures: true and its full signature evidence — including the otpVerifiedFrom context described in OTP Verification — and that is what makes it usable for a promissory note. If the customer both delivered the code and verified it on their side, there would be no signature of ours to show.

How to set it up

Three steps, in this order. Each one depends on the previous.

1. Create the connector

In Connectors, create a new connector with the Delegated OTP delivery channel. It asks for three things:

FieldWhat it is
NameHow you recognize it in the connectors grid
Delivery URLThe customer endpoint that receives the POST. Required, and https: anything else is rejected on save and on send
SecretThe credential agreed with the customer, under one of the two keys below

The secret is stored under one of two keys, and this channel reads only those two:

Secret keyHeader it travels in
otp_delivery_tokenAuthorization: Bearer <secret>
otp_delivery_api_keyX-Api-Key: <secret>

The form presents them as a choice — Bearer token or API key — and switching from one to the other discards the one left behind, so a connector ends up with a single secret. The restriction that matters is the list itself: no other connector secret is ever sent to this endpoint. The WhatsApp access_token lives in the same secrets dictionary and never leaves through here.

A delegated delivery connector does not handle conversations: it has no default responder, no support hours, and no keywords. It is an address with a credential.

2. Create the OTP template

In Settings → OTP Templates, create a template with the Delivered by the customer channel and pick the connector from the previous step. Without a connector the template cannot send anything, and the send fails with OTP_TEMPLATE_WITHOUT_CONNECTOR.

The template also sets the language and the slug that travel in the POST, and the code length.

3. Point the node at that template

In the flow, the OTP Verification node picks the Delivered by the customer channel and then the template. Nothing else changes: contact source, attempts, expiration, and backup methods all behave as they do on any other channel.

Unlike every other channel, this one accepts both phone and email as the contact source. The customer decides how to reach the person, so the destination can be either.

The contract with the customer endpoint

This is what the customer has to implement. The whole block can be copied and handed to their team.

A POST to the delivery URL, with Content-Type: application/json:

{
  "otpId": "2eb53384-a866-42ef-9722-d64d566ae441",
  "code": "482913",
  "target": "person@example.com",
  "contactMethod": "email",
  "templateSlug": "tenant:templateId",
  "language": "es",
  "message": "text already composed from the template's copy",
  "idempotencyKey": "2eb53384-a866-42ef-9722-d64d566ae441:0"
}

Headers:

HeaderValue
Content-Typeapplication/json
Idempotency-KeyThe same value as the idempotencyKey field
Authorization or X-Api-KeyThe configured secret, depending on which key was used

What each field means

FieldWhat it is
otpIdThe identifier of the one-time code. It is the same on the primary send and on every backup
codeThe bare code, with no text around it
targetWho to deliver it to: a phone number or an email address
contactMethodemail when target contains an at sign, phone otherwise. Derived from the value, not from how the node was configured
templateSlugThe OTP template's slug. It defaults to tenant:templateId, but can be set to whatever the customer prefers for routing on their side
languageThe template's language
messageThe message already composed from the template's copy, in case they want to use it as is
idempotencyKeyThe otpId and the send number, colon-separated: 0 is the primary send, 1 onward are the backups

message can be ignored entirely. A customer who wants their own wording composes it from code and never looks at the rest.

The response may carry a JSON identifier of its own (messageId, message_id, id, or reference); it is read, but this channel registers no delivery tracking, so it does not lead anywhere yet.

The rules, and why they exist

Only a 2xx counts as accepted. A 301 does not. If the customer moves the path and leaves a redirect behind, without this rule we would count every code as sent while nothing left their side — and with the send mode set to cascade, that phantom success would suppress the backup channel that would have worked.

The Idempotency-Key must be honored. It exists so that a retry of ours does not reach the same person twice. Two POSTs carrying the same key are the same send.

There is a 12-second wall-clock ceiling to respond. Someone is waiting in front of a screen on the other side; each network operation also has its own 8-second timeout. Past the ceiling we cut the call and the send counts as failed, which — with backups configured — beats waiting longer.

The URL must be https and cannot point at a private network or loopback. The connector is tenant configuration and the service POSTs to it from its own egress, so a URL pointing inward would turn the connector into a way to reach whatever the service can reach. localhost, metadata endpoints, and private, loopback, link-local, and reserved addresses are all refused.

When it fails

This channel's errors carry an errorSubCode saying what to check. None of them include the customer endpoint's response body: that body can quote the code we just sent, and these messages end up in a conversation notice and in unauthenticated responses.

errorSubCodeWhat happened
OTP_TEMPLATE_WITHOUT_CONNECTORThe template uses this channel but has no connector configured
OTP_CONNECTOR_NOT_FOUNDThe connector the template points at no longer exists
OTP_CONNECTOR_WRONG_CHANNELThe template points at a connector of another channel
OTP_DELEGATED_WITHOUT_URLThe connector has no delivery URL
OTP_DELEGATED_ENDPOINT_NOT_ALLOWEDThe URL is not https, or it points at a private network or loopback
OTP_DELEGATED_UNREACHABLEThe endpoint could not be reached, or the time ceiling ran out
OTP_DELEGATED_REJECTEDThe endpoint answered something other than 2xx. The status code comes back in providerStatus

In the flow, a failed send leaves through the OTP node's error handle, the same as on any other channel.

Limitations

There is no delivery confirmation. A 2xx means "the customer received it", never "it reached the person". No provider reports back to us afterwards, which is why this channel registers no delivery legs: there would be nothing to resolve later, and a leg sitting at "accepted" forever would be a "we do not know" pretending to be tracking. When knowing matters, configure a backup method on another channel and the code goes out through both.

The message body copy. The message field is built by joining three template texts: preOtpBlurb (the code runs straight onto the end of it), posOtpBlurb, and didNotAskMessage, each as its own paragraph, with the empty ones skipped. A template with none of the three sends the bare code, which is a perfectly usable message for a customer who composes their own text. Editing this copy from the application is in progress; until it lands, a freshly created template on this channel delivers the code alone.

The seller portal does not offer this channel. The seller portal's OTP login accepts WhatsApp, SMS, and email; delegated delivery is, for now, used only from conversational flows.

Things to Keep in Mind

  • What is delegated is the transport, not the verification. If the customer also wants to verify the code on their side, this channel is not what they are after: the authorization's signature comes from the person typing the code into our screen.
  • Only two secret keys are sent. otp_delivery_token and otp_delivery_api_key. Any other connector credential stays where it is.
  • A 2xx is not a delivery. With a backup method on another channel and the send mode set to cascade, the backup is only attempted if the endpoint rejected the code synchronously.
  • The endpoint has to be fast. Twelve seconds is the ceiling, and a person is waiting for the code on the other side.
  • The URL is https or it does not save. And it cannot point inside a private network.

On this page