I wanted to share a working example of integrating Asterisk with Retell AI using AGI + PJSIP + register-phone-call for inbound PSTN calls.
This took some trial and error, so hopefully this saves someone else time.
Final Working Call Flow
Carrier DID (BulkVS / Telnyx / etc)
→ Asterisk inbound trunk
→ Dialplan matches AI DID
→ AGI script calls Retell register-phone-call API
→ Retell returns call_id
→ Asterisk dials dynamic SIP URI
→ Retell agent answers
Important Discovery
For PJSIP dynamic SIP URI dialing, the correct syntax is:
PJSIP/<endpoint>/sip:user@domain
This matches official Asterisk community guidance. (Asterisk Community)
So in this use case:
Dial(PJSIP/retell-direct/sip:${RETELL_CALL_ID}@sip.retellai.com,60)
That was the breakthrough.
Example pjsip.conf
[transport-tcp]
type=transport
protocol=tcp
bind=0.0.0.0:5060
[retell-direct]
type=endpoint
transport=transport-tcp
disallow=all
allow=ulaw
aors=retell-direct
direct_media=no
rewrite_contact=yes
rtp_symmetric=yes
force_rport=yes
from_domain=sip.retellai.com
outbound_proxy=sip:sip.retellai.com\;lr
[retell-direct]
type=aor
max_contacts=1
contact=sip:sip.retellai.com:5060
qualify_frequency=60
Example extensions.conf
[from-carrier]
exten => +1XXXXXXXXXX,1,NoOp(Inbound call to Retell AI DID)
same => n,Set(CALLER_E164=${IF($["${CALLERID(num):0:1}" = "+"]?${CALLERID(num)}:+1${CALLERID(num)})})
same => n,Set(DID_E164=+1XXXXXXXXXX)
same => n,AGI(retell_register.py,${CALLER_E164},${DID_E164})
same => n,NoOp(RETELL_CALL_ID=${RETELL_CALL_ID})
same => n,NoOp(RETELL_ERROR=${RETELL_ERROR})
same => n,GotoIf($["${RETELL_CALL_ID}"=""]?fail)
same => n,Dial(PJSIP/retell-direct/sip:${RETELL_CALL_ID}@sip.retellai.com,60)
same => n,Hangup()
same => n(fail),NoOp(Retell registration failed: ${RETELL_ERROR})
same => n,Playback(sorry-cant-let-you-do-that)
same => n,Hangup()
Example AGI Script (retell_register.py)
#!/usr/bin/env python3
import sys
import json
import urllib.request
import urllib.error
RETELL_API_KEY = "rtk_xxxxxxxxxxxxxxxxx"
RETELL_AGENT_ID = "agent_xxxxxxxxxxxxx"
def read_agi():
while True:
line = sys.stdin.readline().strip()
if line == "":
break
def agi_cmd(cmd):
sys.stdout.write(cmd + "\n")
sys.stdout.flush()
return sys.stdin.readline()
def setvar(k,v):
safe = str(v).replace('"','\\"')
agi_cmd(f'SET VARIABLE {k} "{safe}"')
def main():
read_agi()
from_number = sys.argv[1]
to_number = sys.argv[2]
payload = {
"agent_id": RETELL_AGENT_ID,
"from_number": from_number,
"to_number": to_number,
"direction": "inbound"
}
req = urllib.request.Request(
"https://api.retellai.com/v2/register-phone-call",
data=json.dumps(payload).encode(),
headers={
"Authorization": f"Bearer {RETELL_API_KEY}",
"Content-Type": "application/json"
},
method="POST"
)
try:
with urllib.request.urlopen(req, timeout=10) as r:
data = json.loads(r.read().decode())
setvar("RETELL_CALL_ID", data["call_id"])
setvar("RETELL_ERROR", "")
except Exception as e:
setvar("RETELL_CALL_ID", "")
setvar("RETELL_ERROR", str(e))
if __name__ == "__main__":
main()
Make executable:
chmod +x /var/lib/asterisk/agi-bin/retell_register.py
Notes
1. Use REST API Key
Use the Retell REST API key, not webhook key.
2. Agent ID != Phone Number
Use:
agent_xxxxxxxxx
not the imported DID number.
3. DNS Check
Make sure your server resolves:
getent hosts sip.retellai.com
4. Firewall
Allow outbound SIP/RTP traffic.
5. Codec
I used:
allow=ulaw
Final Result
Inbound PSTN calls now route:
Carrier DID → Asterisk → Retell AI agent
with full control of routing in Asterisk.
This lets you do:
-
route only specific DIDs to AI
-
time-of-day routing
-
failover to queues/extensions
-
blend human + AI call flows
-
use your own carriers
Hope this helps someone.
Thanks to the Asterisk community thread that clarified the correct PJSIP URI format. (Asterisk Community)