Duplicate calls with Asterisk-Manager

So I’m using Asterisk to initiate outbound calls through my external SIP, it works and the call goes outbound and I do receive it. But whenever I pick up the call it calls me again while I’m on the call and plays the ringing audio from the other call and whatever other sounds come into play.

So, I’m wondering how on earth I’m meant to stop this. I’ve tried 2 libraries and they both have this issue so it’s not specific to the libraries. Below is my code and configs:

const AMI = require("asterisk-manager");

const config = {`Preformatted text`
  host: "",
  port: 5038,
  login: "",
  password: "",
};

const ami = new AMI(
  config.port,
  config.host,
  config.login,
  config.password,
  true
);
ami.keepConnected();

ami.on("connect", () => {
  console.log("AMI is connected");
});

ami.on("error", (err) => {
  console.error("AMI Connection Error:", err);
});

module.exports = async (number) => {
  if (!ami.connected) {
    console.log("Waiting for AMI to connect...");
    await waitForConnection();
  }
  const actionId = `call-${number}-${Date.now()}`;

  ami.action(
    {
      action: "Originate",
      channel: `SIP/privatesip/${number}`,
      context: "outbound",
      exten: number,
      priority: 1,
      actionid: actionId,
    },
    (err, res) => {
      if (err) {
        console.error("Originate Error:", err);
      } else {
        console.log("Originate Response:", res);
      }
    }
  );
};

function waitForConnection() {
  return new Promise((resolve) => {
    const check = setInterval(() => {
      if (ami.connected) {
        clearInterval(check);
        resolve();
      }
    }, 1000);
  });
}

sip.conf

[general]
externip = MY_SERVER_IP
localnet = 192.168.1.0/255.255.255.0
nat = yes
externrefresh = 180
session-timers = refuse

[privatesip]
type = peer
host = MY_HOST
username = MY_USERNAME
secret = MY_PASSWORD
fromuser = MY_USERNAME
fromdomain = MY_HOST
context = outbound
insecure = port,invite
nat = force_rport,comedia
disallow = all
allow = ulaw
canreinvite = no

extensions.conf

[general]
autofallthrough=yes            ; Automatically fall through if no match found

[outbound]
exten => _X.,1,Dial(SIP/${EXTEN}@privatesip,60)
exten => _X.,n,Hangup()

That is what you’ve told Originate to do. You have told it to:

  1. Dial SIP/privatesip/number
  2. Upon answer send it to extension in context “outbound” at priority 1
  3. This extension then dials SIP/number@privatesip

So, upon answer it calls itself.

What is it SUPPOSED to do when the dialed channel answers?

Ah I see, so when I remove the extension field it then returns

Originate Error: {
response: “Error”,
actionid: “call-17604289561-1741599874617”,
message: “Extension does not exist.”,
}

Right, because you have to specify what happens when the outgoing call is answered.

Okay, so it seems like i’ve fixed that issue. But another issue has arised.

(the code updated)

  ami.action(
    {
      action: "Originate",
      channel: `SIP/privatesip/${number}`,
      context: "outbound",
      exten: number,
      priority: 1,
      actionid: actionId,
    },
    (err, res) => {
      if (err) {
        console.error("Originate Error:", err);
      } else {
        console.log("Originate Response:", res);
      }
    }
  );
[general]
autofallthrough=yes            ; Automatically fall through if no match found

[outbound]
exten => _X.,1,Answer()              ; Answer the call
exten => _X.,n,Dial(SIP/privatesip,60) ; Dial the number via privatesip
exten => _X.,n,Hangup()              ; Hang up the call when done

The call automatically hangs up after around 10 seconds of being on the call (ranges from 10-12 seconds).

What is calling “SIP/privatesip” supposed to do? You haven’t specified a phone number to call.

What is the over all goal here?

I’m just testing out trying to call a phone number through my external SIP with asterisk-manager and Asterisk.

So I don’t get it.

  ami.action(
    {
      action: "Originate",
      channel: `SIP/privatesip`,
      context: "outbound",
      exten: number,
      priority: 1,
      actionid: actionId,
    },
    (err, res) => {
      if (err) {
        console.error("Originate Error:", err);
      } else {
        console.log("Originate Response:", res);
      }
    }
  );
[outbound]

exten => _X.,1,Dial(SIP/${EXTEN}@privatesip,60)

exten => _X.,n,Hangup()

I just get

Originate Error: {
response: “Error”,
actionid: “call-17604289561-1741601722037”,
message: “Originate failed”,
}

I know that I’m making some sort of stupid mistake or am missing something here but to my understanding. I want to specify the extension as the number to call which then gets called through the extensions block and uses the privatesip thing in sip.conf. I also specify the channel as SIP/privatesip but I’m receiving some sort of error when trying to initiate the call.

Step back. You call a phone number. It answers. What should happen then?

I haven’t gotten to that part yet. My goal at the moment is just having it answer and successfully be connected to the call. After I have this down I would probably play a .wav file to the answering phone.

You have to do both sides. It has to go somewhere upon answer.

And calling SIP/privatesip is unlikely to work because it probably requires you to specify a phone number to dial.

Well, I’ve managed to get it working as it calls the number and plays the Hello-World default file when connected.

[outbound]
exten => _X.,1,Answer()                           ; Answer the call
exten => _X.,n,Playback(hello-world)              ; Play a default sound for testing
exten => _X.,n,Hangup()                           ; Hang up after the sound is played

the only issue is that the call still hangs up after 10 seconds

The call will hang up after playing “hello-world”.

How could I make it so that it hangs up after a specified duration, say 60 seconds?

There is a Wait application that can be used to wait for a period of time in the dialplan, after which it would continue to the next step which could be hangup.

Are you using an LLM to write this?

No, only when I have a question that I can’t find the answer for I’ll ask a question to it. Anyways, I got it all working fine but now there’s a new issue and I’ve been looking around and can’t seem to find the answer to it.

So say that I place 30 calls with asterisk-manager (the NPM package) and send them to asterisk. Asterisk will only do 1-2 outbound calls at a time and backs up a sort of queue.

Here are my configs.

[general]
autofallthrough=yes            ; Automatically fall through if no match found

[outbound]
exten => _X.,1,Answer()
exten => _X.,n,Playback(intro)
exten => _X.,n,WaitExten(10)

; If '1' is pressed
exten => 1,1,Playback(outro)
exten => 1,2,Hangup()

; If no input or an invalid input (timeout or wrong key pressed)
exten => t,1,Hangup()                             ; Hang up the call after timeout
exten => e,1,Hangup()                             ; Hang up the call after invalid input

exten => _X.,n,Hangup()                           ; Default action: hang up if no match
[general]
externip = REDACTED
localnet = 192.168.1.0/24
nat = force_rport,comedia

[main]
type = peer
host = REDACTED
username = REDACTED
secret = REDACTED
fromuser = REDACTED
fromdomain = REDACTED
insecure = invite       ; Only use if required
disallow = all
allow = ulaw
allow = alaw            ; (Optional: Add for better compatibility)
directmedia = no
qualify = yes

Also, just to add on. I receive events a bit slower sometimes too

You have two priority 2s for extension _X.

You can’'t answer outbound calls (although this is harmless).

That’s what you expect when you do synchronous originates. The queue is in the network subsystem of the kernel, not Asterisk.

I think that is actually “yes”. The question was because your coding errors were typical of the code produced by ChatGPT and similar.

You might also want to note the last paragraph of:

I’m handling a bigger call volume. So how would I go about making it asynchronous or is the entire Originate concept synchronous? And if so what would I do to get a certain concurrency of outbound calls?

By reading the documentation.

If you are handling much larger volumes, you definitely need to read the other thread.