Working with PJSIP Headers in Asterisk Dialplans

Getting Started -- Last reviewed 2026-07-31 pjsip sip headers dialplan troubleshooting p-asserted-identity diversion Found this useful? Upvote it. ×

Working with PJSIP Headers in Asterisk Dialplans

SIP headers carry routing, identity, privacy, diversion, billing, and vendor-specific information between SIP devices. In Asterisk with chan_pjsip, the main dialplan tool for request headers is PJSIP_HEADER(). It can read headers from an inbound PJSIP channel and add, update, or remove headers on an outbound PJSIP channel.

The part that trips people up is direction. A normal dialplan context runs on the caller channel. If you add a header there with PJSIP_HEADER(add,...), you are not usually modifying the outbound INVITE to the callee. To change headers on the outbound channel, run PJSIP_HEADER() inside a Dial pre-dial handler using the lowercase b() option.

Scope matters

PJSIP_HEADER() works against the PJSIP channel where it is executed. If your dialplan is currently running on a Local channel, a DAHDI channel, or the caller side of a call when you meant the callee side, the function will not affect the SIP message you are trying to change.

Quick Rules

Task Use Where it must run
Read a header from the inbound INVITE PJSIP_HEADER(read,Header-Name) Normal dialplan on the inbound PJSIP channel
List inbound header names PJSIP_HEADERS() Normal dialplan on the inbound PJSIP channel
Add a request header to the outbound INVITE PJSIP_HEADER(add,Header-Name) Callee pre-dial handler with Dial(...,b(...))
Update or remove a header you previously added PJSIP_HEADER(update,...) or PJSIP_HEADER(remove,...) Same outbound channel scope where the header exists
Set headers before the outbound PJSIP channel exists PJSIP_INHERITABLE_HEADER() Caller channel on supported Asterisk releases
Read a 200 OK response header PJSIP_RESPONSE_HEADER() Pre-connect handler with Dial(...,U(...))
Read or set a parameter in the From header PJSIP_HEADER_PARAM() Inbound channel for reads, callee pre-dial handler for outbound writes

Use PJSIP_HEADER() for SIP request headers. It does not read arbitrary SIP responses. For response headers from the successful outbound INVITE transaction, use PJSIP_RESPONSE_HEADER().

Version Notes

Function Since Practical caveat
PJSIP_HEADER() 12.0.0 Core request-header function for PJSIP channels
PJSIP_HEADERS() 16.20.0, 18.6.0 Lists inbound INVITE header names, not values
PJSIP_RESPONSE_HEADER() 16.28.0, 18.14.0, 19.6.0 Reads 200 OK response headers from the outbound channel in a pre-connect handler
PJSIP_HEADER_PARAM() 18.16.0, 20.1.0 Currently supports parameters on the From header
PJSIP_INHERITABLE_HEADER() 20.19.0, 22.9.0, 23.3.0 Lets caller-side values become headers on child outbound PJSIP channels

If you maintain older LTS systems, check the function page for that exact Asterisk branch before using newer helpers. The pre-dial handler pattern with PJSIP_HEADER() is the most portable way to add outbound request headers.

Read Inbound Headers

This reads headers from the inbound INVITE that reached Asterisk.

[from-carrier]
exten => _X.,1,NoOp(Inbound call from ${CALLERID(all)})
same => n,Set(inbound_pai=${PJSIP_HEADER(read,P-Asserted-Identity)})
same => n,Set(inbound_diversion=${PJSIP_HEADER(read,Diversion)})
same => n,Verbose(1,Inbound PAI: ${inbound_pai})
same => n,Verbose(1,Inbound Diversion: ${inbound_diversion})
same => n,Goto(from-internal,${EXTEN},1)

If a header can appear more than once, pass the instance number as the third argument:

same => n,Set(first_via=${PJSIP_HEADER(read,Via,1)})
same => n,Set(second_via=${PJSIP_HEADER(read,Via,2)})

A trailing * matches headers that begin with the prefix:

same => n,Set(first_x_header=${PJSIP_HEADER(read,X-*,1)})

For discovery, use PJSIP_HEADERS() to list header names without values:

same => n,Verbose(1,Custom inbound headers: ${PJSIP_HEADERS(X-)})
same => n,Verbose(1,All Contact-like headers: ${PJSIP_HEADERS(Contact)})

Do not trust inbound SIP headers just because they exist. Treat caller identity and diversion headers as carrier-provided data only when the call comes from a trusted trunk or authenticated endpoint.

Add Headers to an Outbound Call

To add a header to the outbound INVITE, use a callee pre-dial handler. The lowercase b() option runs after Asterisk creates the outbound channel and before it sends the call to the endpoint.

[outbound]
exten => _NXXNXXXXXX,1,NoOp(Outbound call with custom SIP header)
same => n,Dial(PJSIP/${EXTEN}@carrier,30,b(add-outbound-headers^s^1))
same => n,Hangup()

[add-outbound-headers]
exten => s,1,Set(PJSIP_HEADER(add,X-Account-Code)=${CDR(accountcode)})
same => n,Set(PJSIP_HEADER(add,X-Orig-Caller)=${CALLERID(num)})
same => n,Return()

The Return() is required because pre-dial handlers are Gosub routines. Keep the handler short. Do not run applications in the handler that change call state, answer media, bridge channels, or start long-running logic.

Add P-Asserted-Identity

Some SIP trunks expect P-Asserted-Identity when they authorize caller ID from the SIP identity header rather than only from endpoint configuration.

[outbound]
exten => _NXXNXXXXXX,1,Set(PAI_DOMAIN=pbx.example.com)
same => n,Dial(PJSIP/${EXTEN}@carrier,30,b(set-pai^s^1(${PAI_DOMAIN})))
same => n,Hangup()

[set-pai]
exten => s,1,Set(PJSIP_HEADER(add,P-Asserted-Identity)="${CALLERID(name)}" <sip:${CALLERID(num)}@${ARG1}>)
same => n,Return()

Only send asserted identity when the upstream provider expects it and the caller ID is allowed on that trunk. Many providers ignore, rewrite, or reject identity headers that do not match their policy.

Add a Diversion Header

A Diversion header is often used when a call was forwarded before being sent to an upstream provider. Provider requirements vary, so confirm the exact format with the carrier.

[forward-out]
exten => _NXXNXXXXXX,1,Set(ORIGINAL_DID=15551234567)
same => n,Dial(PJSIP/${EXTEN}@carrier,30,b(add-diversion^s^1(${ORIGINAL_DID})))
same => n,Hangup()

[add-diversion]
exten => s,1,Set(PJSIP_HEADER(add,Diversion)=<sip:${ARG1}@pbx.example.com>;reason=unconditional)
same => n,Return()

If your carrier requires different parameters, change the header value in the pre-dial handler rather than trying to rewrite it later in the call.

Update or Remove Headers

Use update only for a header that already exists in the same header scope. If the header does not exist, the function fails.

[add-then-update]
exten => s,1,Set(PJSIP_HEADER(add,X-Trace-ID)=${UNIQUEID})
same => n,Set(PJSIP_HEADER(update,X-Trace-ID)=${LINKEDID})
same => n,Return()

Use remove for headers added earlier through PJSIP_HEADER() in the same session. It returns the number of removed headers, so you can log what happened:

[cleanup-headers]
exten => s,1,Set(removed=${PJSIP_HEADER(remove,X-Debug-*)})
same => n,Verbose(1,Removed ${removed} X-Debug headers)
same => n,Return()

PJSIP_HEADER(remove,*) removes all headers previously added through PJSIP_HEADER() in that scope. It does not remove every header Asterisk or the SIP stack would otherwise generate.

Use Inheritable Headers When Available

PJSIP_INHERITABLE_HEADER() can write SIP headers on a PJSIP or non-PJSIP caller channel so they are inherited by outbound PJSIP channels. It is useful when the caller channel has the information you need before the outbound PJSIP channel is created.

[outbound]
exten => _NXXNXXXXXX,1,Set(PJSIP_INHERITABLE_HEADER(add,X-Trace-ID)=${UNIQUEID})
same => n,Set(PJSIP_INHERITABLE_HEADER(add,X-Account-Code)=${CDR(accountcode)})
same => n,Dial(PJSIP/${EXTEN}@carrier,30)
same => n,Hangup()

Headers added with PJSIP_INHERITABLE_HEADER() are separate from headers added with PJSIP_HEADER(). Update or remove inheritable headers with PJSIP_INHERITABLE_HEADER(), not with PJSIP_HEADER().

If you support older Asterisk versions, keep the pre-dial handler pattern as the portable approach.

Read Headers from the 200 OK Response

PJSIP_HEADER() reads request headers. To read headers from the successful 200 OK response on an outbound channel, use PJSIP_RESPONSE_HEADER() in a pre-connect handler with the uppercase U() option.

[outbound]
exten => _NXXNXXXXXX,1,Dial(PJSIP/${EXTEN}@carrier,30,U(read-answer-headers^s^1))
same => n,Hangup()

[read-answer-headers]
exten => s,1,Set(answer_contact=${PJSIP_RESPONSE_HEADER(read,Contact)})
same => n,Verbose(1,Answer Contact: ${answer_contact})
same => n,Return()

This is useful when you need information returned by the far end after the outbound INVITE is answered.

Header Names and Values

SIP header names are case-insensitive by protocol, but you should use the conventional spelling expected by your carrier or equipment. Common examples are P-Asserted-Identity, Privacy, Diversion, Remote-Party-ID, History-Info, and custom X- headers.

Follow these rules:

Good:

same => n,Set(PJSIP_HEADER(add,X-Tenant-ID)=acme)

Wrong:

same => n,Set(PJSIP_HEADER(add,X-Tenant-ID:)=X-Tenant-ID: acme)

Troubleshooting

First confirm the function module is loaded:

*CLI> module show like res_pjsip_header_funcs

Then capture the SIP message Asterisk actually sends:

*CLI> pjsip set logger on

Make the test call, inspect the INVITE or response headers, then turn logging off:

*CLI> pjsip set logger off

Common symptoms:

Symptom Likely cause Fix
Header is visible in Verbose() but not in the outbound INVITE Header was set on the caller channel Move the PJSIP_HEADER(add,...) call into a lowercase b() pre-dial handler
Function returns empty on a call you expected to be SIP Dialplan is running on a non-PJSIP channel or the wrong side of the call Check ${CHANNEL(channeltype)}, avoid testing from a Local channel unless you understand where the PJSIP leg is created
update fails The header does not already exist in that scope Use add, or add the header before updating it
Header appears on one dialed endpoint but not another Multiple outbound channels are created Use the b() handler so it runs for each callee channel
You need a value before the outbound channel exists Scope mismatch Use PJSIP_INHERITABLE_HEADER() on supported versions or pass values as handler arguments
You are trying to read a 200 OK header with PJSIP_HEADER() Wrong function Use PJSIP_RESPONSE_HEADER() in a U() pre-connect handler
Carrier ignores or rejects the call Header syntax or authorization policy mismatch Compare pjsip set logger on output with the carrier's required format
Header value contains unsafe user input Dialplan value is copied directly into SIP Filter or normalize the value before adding it

When testing, change one header at a time and compare the raw SIP trace before and after the dialplan change. The trace is the source of truth.

Complete Example: Outbound Trunk Headers

This example carries caller-side values through the dialplan and adds outbound headers in the correct channel scope.

[from-internal]
exten => _NXXNXXXXXX,1,NoOp(Outbound call with PJSIP headers)
same => n,Set(__TRACE_ID=${UNIQUEID})
same => n,Set(__TENANT_ID=${CDR(accountcode)})
same => n,Dial(PJSIP/${EXTEN}@carrier,30,b(outbound-header-handler^s^1))
same => n,Hangup()

[outbound-header-handler]
exten => s,1,NoOp(Add outbound SIP headers)
same => n,Set(PJSIP_HEADER(add,X-Trace-ID)=${TRACE_ID})
same => n,Set(PJSIP_HEADER(add,X-Tenant-ID)=${TENANT_ID})
same => n,Set(PJSIP_HEADER(add,P-Asserted-Identity)="${CALLERID(name)}" <sip:${CALLERID(num)}@pbx.example.com>)
same => n,Return()

The double underscore on __TRACE_ID and __TENANT_ID causes those channel variables to inherit to outbound channels. That gives the pre-dial handler access to values collected on the caller channel without relying on global variables.

A solid choice for hosting Asterisk.

High-performance cloud compute starting at $2.50/mo. Deploy a VPS in seconds.

Get $100 Free Credit

Referral link. Helps support this site.

User Notes

Know a tip or gotcha for this topic? Share it below and help others.

Contribute a note

Share a tip, gotcha, or practical example. Keep it under 2000 characters. No questions (use the Asterisk community forums for support). Wrap code in backticks.

Moderated before publishing. Email never shown.
Related Snippets