Emergency server help: get in touch

chan_sip to PJSIP Migration: sip_to_pjsip.py, Option Mapping, Tests

Migrate from chan_sip to PJSIP before Asterisk 21+: run sip_to_pjsip.py, fix what it misses, map common options, use fwconsole on FreePBX, and test.

Published 8 min read

Short answer: chan_sip was removed in Asterisk 21, so any sip.conf setup must move to pjsip.conf before you upgrade. Run the official converter (contrib/scripts/sip_to_pjsip/sip_to_pjsip.py sip.conf pjsip.conf) to get a first draft, then fix what it misses: qualify, inbound auth on trunks and anything in its “Non mapped elements” block. On FreePBX use fwconsole convert2pjsip and fwconsole trunks --convert2pjsip instead.

Applies to Asterisk 20 to 22 (tested on 22.11); FreePBX 16 and 17 (tested on 17.0.33, Debian 12)

We ran these commands on our lab server (Debian 12, FreePBX 17.0.33, Asterisk 22.11) on 6 October 2026: the upstream converter on a sample sip.conf, and the fwconsole help for FreePBX’s converters. The conversion output below is from that run; we did not convert a production system.

Why you have to migrate

chan_sip was deprecated for years and the Asterisk project removed it in Asterisk 21. The current LTS releases (Asterisk 22, and Asterisk 24 when it ships) have only chan_pjsip. Our lab’s Asterisk 22.11 lists chan_pjsip.so but no chan_sip.so at all:

asterisk -rx "module show like chan_"

FreePBX 17 ships with Asterisk 22 by default, so a FreePBX 16 system with chan_sip extensions or trunks hits the same wall when you move. Asterisk 20 LTS still has chan_sip, but it enters security-fix-only status on 19 October 2026 and reaches end of life on 19 October 2027.

PJSIP is not a renamed chan_sip. One [peer] becomes several objects:

PJSIP objectHoldschan_sip equivalent
type=endpointCall settings: context, codecs, DTMF, NAT, mediaMost peer options
type=aorWhere to reach it: max contacts, static contact, qualifyhost=, qualify=
type=authUsername and passwordsecret=, username=
type=identifyMatch inbound traffic by source IPhost= on IP-authenticated trunks
type=registrationOutbound registration to a providerregister => line
type=transportBind address, protocol, NAT addresses[general] bind and NAT settings

Run sip_to_pjsip.py

The script lives in the Asterisk source tree at contrib/scripts/sip_to_pjsip/ and needs its two helper files (astconfigparser.py, astdicts.py) beside it. The Asterisk 22 version runs on Python 3. Fetch all three into a work folder, away from /etc/asterisk:

mkdir -p /root/pjsip-migration && cd /root/pjsip-migration
for f in sip_to_pjsip.py astconfigparser.py astdicts.py; do
  curl -fsSO https://raw.githubusercontent.com/asterisk/asterisk/22/contrib/scripts/sip_to_pjsip/$f
done
cp -p /etc/asterisk/sip*.conf .
python3 sip_to_pjsip.py --help

Help output on lab3:

Usage: sip_to_pjsip.py [options] [input-file [output-file]]

Converts the chan_sip configuration input-file to the chan_pjsip output-file.
The input-file defaults to 'sip.conf'.
The output-file defaults to 'pjsip.conf'.

Options:
  -h, --help            show this help message and exit
  -p PREFIX, --prefix=PREFIX
                        output prefix for include files
  -q, --quiet           don't print messages to stdout

Run it from the folder that holds sip.conf and any files it #includes, and write to a new file so nothing live is touched:

python3 sip_to_pjsip.py sip.conf pjsip-converted.conf

FreePBX 17 also carries an older copy of this script under /var/www/html/admin/modules/core/sip_to_pjsip/. On our lab it is Python 2 code and fails under Debian 12’s Python 3.11 with SyntaxError: Missing parentheses in call to 'print'. Use the upstream copy above, or FreePBX’s own fwconsole converters.

What the converter produced on our lab

Our test sip.conf had a NAT setting in [general], a registering trunk and one phone (all example values):

[general]
udpbindaddr=0.0.0.0:5060
externip=203.0.113.10
localnet=192.168.1.0/255.255.255.0
register => bob-trunk:ExamplePass1@sip.example.com

[bob-trunk]
type=peer
host=sip.example.com
username=bob-trunk
secret=ExamplePass1
fromuser=bob-trunk
insecure=port,invite
context=from-trunk
disallow=all
allow=ulaw,alaw
dtmfmode=rfc2833
qualify=yes

[201]
type=friend
host=dynamic
secret=ExamplePass2
context=from-internal
nat=force_rport,comedia
qualify=yes
directmedia=no
disallow=all
allow=ulaw,g722
dtmfmode=rfc2833
callerid="Bob" <201>

The converter wrote this (exactly as produced, comment banner shortened):

;  Non mapped elements start
[general]
udpbindaddr = 0.0.0.0:5060

[bob-trunk]
username = bob-trunk

[201]
qualify = yes
;  Non mapped elements end

[transport-udp]
type = transport
protocol = udp
bind = 0.0.0.0:5060
external_media_address = 203.0.113.10
external_signaling_address = 203.0.113.10
local_net = 192.168.1.0/255.255.255.0

[reg_sip.example.com]
type = registration
retry_interval = 20
max_retries = 10
expiration = 120
transport = transport-udp
outbound_auth = auth_reg_sip.example.com
client_uri = sip:bob-trunk@sip.example.com
server_uri = sip:sip.example.com

[auth_reg_sip.example.com]
type = auth
password = ExamplePass1
username = bob-trunk

[bob-trunk]
type = aor
contact = sip:bob-trunk@sip.example.com

[bob-trunk]
type = identify
endpoint = bob-trunk
match = sip.example.com

[bob-trunk]
type = auth
username = bob-trunk
password = ExamplePass1

[bob-trunk]
type = endpoint
context = from-trunk
dtmf_mode = rfc4733
disallow = all
allow = ulaw,alaw
from_user = bob-trunk
auth = bob-trunk
outbound_auth = bob-trunk
aors = bob-trunk

[201]
type = aor
max_contacts = 1

[201]
type = auth
username = 201
password = ExamplePass2

[201]
type = endpoint
context = from-internal
dtmf_mode = rfc4733
disallow = all
allow = ulaw,g722
rtp_symmetric = yes
force_rport = yes
rewrite_contact = yes
direct_media = no
callerid = "Bob" <201>
auth = 201
outbound_auth = 201
aors = 201

What to fix by hand before using it:

  • qualify was not converted. qualify=yes landed in the “Non mapped” block for the phone and vanished for the trunk. Add qualify_frequency=60 (or your preferred interval in seconds) to each type=aor.
  • The trunk got inbound auth. auth = bob-trunk on the trunk endpoint means Asterisk will challenge calls from the provider. Providers usually do not answer challenges; with an identify match you normally keep only outbound_auth. The chan_sip insecure=port,invite line was dropped without a note.
  • The phone got outbound_auth it does not need. Harmless, but you can remove it.
  • Registration and auth names changed (reg_sip.example.com, auth_reg_sip.example.com). Update any monitoring that checks registration names.
  • Everything in the “Non mapped elements” block needs a manual decision. Delete the block once handled; it is not valid PJSIP.

Option mapping for common chan_sip settings

chan_sip (sip.conf)PJSIP (pjsip.conf)Object
secret=, username=password=, username=, auth_type=userpassauth
host=dynamicmax_contacts=1 (or more)aor
host=203.0.113.10contact=sip:203.0.113.10:5060 plus an identifyaor, identify
qualify=yesqualify_frequency=60aor
nat=force_rport,comedia (or nat=yes)rtp_symmetric=yes, force_rport=yes, rewrite_contact=yesendpoint
directmedia=nodirect_media=noendpoint
dtmfmode=rfc2833dtmf_mode=rfc4733endpoint
fromuser=, fromdomain=from_user=, from_domain=endpoint
context=, allow=, disallow=, callerid=same namesendpoint
register => user:pass@hosttype=registration + outbound_authregistration, auth
externip=, localnet=external_media_address=, external_signaling_address=, local_net=transport
udpbindaddr=bind= on a protocol=udp transporttransport
insecure=inviteNo direct option: match the trunk with identify and do not set inbound authendpoint, identify

The NAT mapping follows the Asterisk documentation: nat=yes becomes the three options above, nat=route becomes force_rport=yes and rewrite_contact=yes without symmetric RTP. Our PJSIP NAT settings generator writes the full NAT block for you.

Migrating on FreePBX

FreePBX stores extensions and trunks in its database and writes the pjsip.*.conf files itself, so do not paste converted files into /etc/asterisk. Use its tools. FreePBX 17.0.33 on our lab has:

fwconsole convert2pjsip --all                 # every chan_sip extension
fwconsole convert2pjsip --range=5000-5100,6020  # selected extensions
fwconsole trunks --list                         # see trunks and their technology
fwconsole trunks --convert2pjsip=all            # or a single trunk ID

When restoring a FreePBX 16 backup onto 17, fwconsole backup has matching switches: --convertchansipexts2pjsip, --convertchansiptrunks2pjsip, or --skipchansipexts and --skipchansiptrunks to leave them out. Our FreePBX 16 to 17 upgrade guide covers that path.

Converted phones must re-register to the PJSIP port. If PJSIP used a different port than chan_sip on the old system (FreePBX 13-16 often ran PJSIP on 5060 and chan_sip on 5160, or the other way round), update phone provisioning or the Bind Port in Asterisk SIP Settings before the cut-over.

Test the migration

Load the new configuration on a test box or during a maintenance window. Transport changes need a full restart, not a reload:

asterisk -rx "core restart when convenient"
asterisk -rx "pjsip show transports"
asterisk -rx "pjsip show registrations"
asterisk -rx "pjsip show endpoints"
asterisk -rx "pjsip show contacts"

Then work through this list:

  1. Each provider registration shows Registered.
  2. Each phone appears in pjsip show contacts with status Avail and a round-trip time.
  3. Inbound call from the provider reaches the right context (watch with pjsip set logger on; a 401 or 403 back to the provider points to the inbound auth problem above).
  4. Outbound call with correct caller ID.
  5. Audio both ways on an external call and on a phone behind NAT.
  6. DTMF in voicemail or an IVR (checks dtmf_mode).
  7. Transfers, hold, call parking and BLF (hints use PJSIP/201 instead of SIP/201).

Search your custom dialplan for leftovers: grep -rn "SIP/" /etc/asterisk/extensions*.conf. Any Dial(SIP/...), SIPPEER() or SIP_HEADER() must change to PJSIP/..., PJSIP_ENDPOINT()/PJSIP_CONTACT() and PJSIP_HEADER().

Official documentation: Asterisk: Migrating from chan_sip to res_pjsip · sip_to_pjsip source (Asterisk 22) · Asterisk versions and EOL dates

Related: PJSIP NAT Settings Generator: Asterisk, FreePBX, Issabel, VitalPBX, 3CX · PJSIP Behind NAT: Asterisk and FreePBX Settings for Two-Way Audio · Install Asterisk 22 LTS on Debian 13: Step-by-Step Guide · Troubleshoot SIP Calls with sngrep and the Asterisk PJSIP Logger · SIP Response Codes: Lookup for Every SIP Error, With Causes and Fixes

See also: sip.conf to pjsip.conf Converter for Asterisk · Asterisk CLI Commands Cheat Sheet: PJSIP, Calls, Dialplan, Logs · PJSIP Endpoint Unreachable or Unavailable: Fix Qualify and NAT · FreePBX PJSIP Trunk Setup: Registration, IP Auth, Routes, NAT

Frequently asked questions

Which Asterisk version removed chan_sip?

Asterisk 21. Asterisk 20 LTS is the last LTS release with chan_sip; it goes security-fix-only on 19 October 2026 and end of life on 19 October 2027.

Does sip_to_pjsip.py convert everything?

No. The Asterisk docs say it is not meant to cover every configuration. In our test it skipped qualify, dropped insecure, and added inbound auth to a trunk, so review the output line by line.

Can I run chan_sip and PJSIP side by side during migration?

Only on Asterisk 20 or older, and they must listen on different ports. On Asterisk 21 and later only PJSIP exists.

How do I convert FreePBX chan_sip extensions?

Run fwconsole convert2pjsip –all or with –range, and fwconsole trunks –convert2pjsip=all for trunks. Then fwconsole reload and re-register the phones.

What replaces SIPPEER() and SIP_HEADER() in dialplan?

Use PJSIP_ENDPOINT() or PJSIP_CONTACT() for peer data and PJSIP_HEADER() to read or add SIP headers.

Maintenance record

This guide changes servers, data or security settings, so we re-check it against current versions on a fixed schedule. Take a backup or snapshot before you start.

Maintained by
srvScripts editorial team
Supported versions
Asterisk 20 to 22 (tested on 22.11); FreePBX 16 and 17 (tested on 17.0.33, Debian 12)
Last full review
Next review

Free website test

Is your website set up right?

Check SSL, security headers, redirects, robots.txt, sitemap, llms.txt and security.txt in one test. It takes about 30 seconds.