TRACY: Hey, good afternoon, good morning, whoever is joining us. My name is Tracy and you guys' webinar host for today. Welcome to our developing with Bandwidth miniseries. So I'll go over a few housekeeping items before we get started. We've muted all attendees just to prevent any weird background noise. I know a lot of us are working from home so we probably have a lot of extra background noise going on here and there.
And we'll have time for Q&A at the end of the webinar. But we will be taking questions throughout the webinar so we can have a more informal approach on it. And yeah. So you should have the Q&A box on the console right there on your screen. So feel free to type in any of those. We can always interject during the webinar and ask any of your questions.
The last couple of weeks, we've been running this series. And we've covered Node.js, Java, C-Sharp, and Ruby. So if you guys are interested in seeing any of those, feel free to head over to Bandwidth.com/resources and you can watch on demand. And you can also sign up for any of our upcoming webinars there as well.
So today we're going to be covering Python with Bandwidth's Technical Product Owner, Dan Tolbert. Dan does API things at Bandwidth and has for quite some time. And during his time with us, he's worked really closely with customers of all sizes during their integrations to Bandwidth. And he currently sits on the technology team helping drive Bandwidth Developer Experience Team forward.
So with that, I will go ahead and turn it over to Dan to get us started.
DAN TOLBERT: Thanks Tracy. So hopefully you all can hear me all right. Looks like it. So just going to go through, give you an idea of where everything is located, and talk about what we're going to be building today with our Python SDK. The first thing I just wanted to make sure everyone knows about is the documentation site. That'll be at dev.Bandwidth.com. Anything about how our APIs work and just to grab information about SDKs are going to be loaded in here.
We've got just some boilerplate sample code here that we'll be elaborating on in a minute. And we're going to be using the Bandwidth dashboard. So hopefully you all have a Bandwidth account. And if not, you can go to Bandwidth.com and click Contact Us or sign up for free trial and we'll be reaching out for you there. And other than that, we'll just go through and just hop in and build a little app today. I will be working from a boilerplate, I'm not going to write everything from scratch just to make sure that live demos are kept under control.
So with that said, I'm going to go into our Examples repo here on GitHub. I'm going to head to the Python, and then I'm going to head to Webinar Example. And this is going to be where I'm going to be copying and pasting code from today and kind of showing everything off as far as the Python SDK goes. For today.
So we've got a couple of pre-reqs. Obviously, you need a Bandwidth account. You are going to need a Bandwidth environment variable set up. You don't necessarily need to set these as part of your global environment variables. I have done that. It just makes it easier for me to run all these different apps. So we do have your account ID, your API user and your API password.
And then for messaging and voice, we do need an application ID. I'll show you how to create that here in a minute. And we do have a requirement to have a server-side generated token and secret to be used as basic auth credentials for your messaging API. We're working on getting that all aligned under a single credentials pair, but today you do need both an API user and a messaging token, as well as a messaging secret.
So I'm just going to talk through a little bit about our app today. Relatively rudimentary app. It's going to receive a text message, it's going to look at the text of that text message, if the text was the word "dog" and nothing else, we're going to send a picture of a dog, send an MMS, back to the person that texted the word "dog." And then if you text anything other than that, we're just going to send a response. It actually is not with the current date-time, it's just going to be a wave, "Hello from Bandwidth," just to kind of demonstrate how to inspect that inbound text and respond to an inbound text message.
So we're not going to be doing any conversation tracking. Very transactional. Send a text message, get a response. Send a different text message with a keyword, get a different response. And then we're going to enable that phone number to receive inbound phone calls. And then as part of that inbound call, we're going to do a little arithmetic game. Basically asking the user to press a couple of digits. We're going to check those digits that are pushed via the phone's keypad, see if they match our arithmetic. If they do, we're going to play a success WAV file celebrating their basic arithmetic skills. And if they for some reason do not get it right, we are going to play a sad trombone sound and show them like hey, you should probably know your basic arithmetic.
So I will be using Python 3.7.6 for this demo along with a nice little app. I'm not sure if you all have seen it, it's going to be called pipenv. It's a nice Python workflow development for humans, which I am, so that's pretty great. And it just really helps me manage my packages and everything there. And I use an IDE called IntelliJ with the Python package installed. You can also use PyCharm and you just get the same functionalities and features there. Mainly using that to get some deeper code inspection. To give me type hints, and to run the app in debug mode so I can inspect stuff without littering everything with print statements, even though there are some in here.
So overall, as I said, our prerequisites, we do need a Bandwidth account. Our SDKs, moving forward, do not support Python 2 anymore. We only target Python 3. So be sure that you do run the Python 3 in your environment. And then I use ngrok to receive web hooks to my localhost. There are plenty of other options out there. However, I find that ngrok is very nice and tidy to use. It's going to allow Bandwidth's servers up in the cloud to send callback and webhook events to the ngrok URL that's provided, which then pipes to my localhost. It allows me to iterate very quickly without having to deploy my app to a server cloud or to somewhere else that has a publicly addressable URL.
And so before we get too into the code, let's go through just the-- pardon me, the Bandwidth dashboard setup. So I'll show you where everything is. I already have my resources created. So I'm not going to be creating new resources, but I'm going to show you how to do that and where everything is located. So first thing I'm going to do is make sure I'm logged into the Bandwidth dashboard. That's dashboard.Bandwidth.com.
Then once I'm logged in here, I'm just going to navigate to My Account. And then I'm going to navigate to my locations. And I do need a location here. A location is going to be the container for phone numbers. So you can kind of see under mine what I'm going to be using today. I got that over almost a year ago. And it's got seven phone numbers assigned to that location. And as part of that, it has been enabled for HTTP voice and it has been enabled for HTTP messaging. And so I happen to have two applications assigned to it there. I'll go into that in just a second. But overall, you do need a location, you do need the protocols set to HTTP, and you just need some phone numbers on that location.
So now that I've got just the location scaffold in place, I'm going to head to the Applications tab. And we're going to see a couple of things here. The first thing you'll see is your messaging URI. This is going to be for your messaging endpoint. The big thing we need from this is this account ID. You can also see it populated up here. This is going to let the Bandwidth API know where in the world these requests are coming to and who to bill, basically.
And so we do need the account ID set so we can create those APIs requests. If we head to the API credentials tab right here, we do need to make sure that our application type is messaging. So you'll just use messaging here, here. And when you click API credentials, it's going to take you to this tokens and secrets page. And this is what you need to access your messaging API to send text messages.
So if I click Create New, you'll see I generated a new token and secret pair. And unfortunately, now that I've exposed them, I need to delete them after this little demo. But to kind of go through, once you do generate your token and secret pair, that token is always going to be available for you to see. However, the secret does disappear as soon as you close this window. So it's unrecoverable from our end, and so if you do not save it off to the side, you will lose it, basically. There's no way to recover that secret so you need to either delete it or create a new pair to move on. To make sure that you have the ability to access that API. So just to go ahead and clear that out so I don't forget and give people access to my account. Close that out.
And then the next thing we're going to do is just take a look at our ngrok running here. And you can see I've been doing some testing there earlier. It does give me this nice web portal, this web interface, that I can show off. And we'll use this in a minute. You can see what the callbacks and the response that I sent from my little server here that are. So we have, like, this is an inbound request. And here's the response that my server generated. We only need that-- right now, we do need this ngrok URL.
And so if I were to be creating an application from scratch to run this, it does say that my inbound voice callback URL is going to be voice slash inbound. And for messaging, it'll be callbacks slash messaging. And so I do need to go back to the dashboard. I would just say something like, Python Messaging Webinar so I'll remember what this was. Paste this in here. And then copy this URL. Clean that up. And I would create that application. And at this point, any time I would associate this application with the location, any of the phone numbers that are in that location will start sending events for messaging to the URL specified in the application.
Similarly for voice, if I were to create a new one. Now select voice. You do see that the voice URI changes up here to what your voice URL is. And your API credentials tell you that you need to go generate an API credential pair, which we'll do in just a minute. So I do need to go ahead and say this is Python Voice Webinar. Same thing as before. I will need my URL. I will need to go ahead and copy voice inbound and clear that out.
So keep in mind, there are two different callback URLs that you have for a voice application. The call initiated callback URL, this is the URL that we send inbound call events, or we call them call initiated events. You can kind of see what that looks like. If you go to the voice section under documentation. Go to callbacks and look at initiate. This is going to be a JSON representation of what we send. It's only going to be JSON. So you can kind of see what that looks like, and all the fields. You have your event type, you get some meta information, "to" and "from," like, where is this phone call coming from and to what Bandwidth number was it created? Obviously the direction is going to be inbound. You get call ID, and you get the gist of it here.
Next, this call status callback URL. This is going to be for, like, disconnect events. I call them unactionable events. So this is the URL that we send events that you cannot respond with BXML. So we look at this as a initiated event. It's asking you for instructions, whereas call status callback URL is really only giving you information about the call. So when that call does end, if you have a call status callback URL set, you'll know that, oh, the duration was 20 minutes. This is what happened on the call. And you kind of get an idea of what that looks like right here. You can see why the call was ended, some of the error messages, if there was one, along with an error ID, which is an internal ID that we can reference if you need it by creating a support ticket. OK.
Go ahead and click Create. Oh, what did I do? News callback. Nope. Go ahead and click Create there. And you can see now, for both of my voice webinar, I have a nice application ID I can copy there. And for my messaging webinar, I have an ID I can copy there. So at this point, I have my token, I have my secret, I have my account ID, I have my voice application ID, I have my messaging application ID. But I don't have my API credentials. You can use your username and password that you have to log in to the dashboard.
However, we do recommend creating an API user or an API service account. You can do this under the Account tab, under Users. And you can see I have one created right here. I just call it API User as my first and last name. And I have my Bandwidth dashboard access method set to API. And this is exclusive. I can set to both or GUI if I want just to log in. What you do benefit with having access to API, we do not require the password reset every 90 days. So if you don't want to update that password on your server-side every 90 days, when we send you an email about your passwords about to expire, you can create this API user, set the password and username.
Right. Username BGTall. Don't come after me, don't hack me. A password. I'm not going to talk about what that is. But basically, we have everything set up here so that I have my username and password, I have my token, secret, account ID, voice application ID, and messaging application ID. I have all my keys to the Bandwidth kingdom, so to speak. Then I can actually start building this app that sends dog pictures and does quick arithmetic games. OK.
So I'm going to head to my little editor here. I'm going to shrink this down so that we have both on the screen here. And I am going to be using a little app called flask. It's a package or framework for Python. Very, very straightforward HTTP framework. Allows us to define routes at the app level, gives us a couple of methods. And then we can respond back and forth to those events as we see fit.
So we can go ahead and just fire this up now. Go ahead and click the Play button. And if I go to my ngrok URL, I'll just demonstrate how that's set up. You can see I get the "Hello, World" request. If I wanted to change this, say, "Hello, World. Stop, rerun." Then refresh that. You can see we are editing this page live. So I have a nice flask setup.
I've already defined my imports. You can kind of see I'm pulling in a lot of different things from the Bandwidth SDK. I use these to kind of show you explicitly what we're going to-- what the types are, how they interact with the service. But you could import just splotched import star. Not really recommended for Python based on my research, however, you can do that if you need. I've got these put out explicitly just to kind of show everything nice and tidy.
All right. So we've got our server running, I'm going to close that out. So the first thing that we need to do, we're going to build our message callback response, or our message responder. So we do need to go ahead and grab all of our Bandwidth credentials from the environment variables. And I'm just going to go ahead and define the Bandwidth Client. This is going to be the entry point for our SDK at the global level. Just again, this is a sample app, in production, you may want to do something a little bit different.
So we're just going to be checking here that we have all those keys to the Bandwidth kingdom that I spoke to earlier locked and loaded in your environment variables. So that way I can copy and paste this code around and everyone can do that without sharing their information. And then IntelliJ is fussing about PEP8 requirements, so save that. So at this point, we just have our environment credentials set up and we have a Bandwidth Client. We don't have anything more than that right now.
So let's actually handle those inbound callbacks. So I've got a route here defined for handling inbound message callbacks. So let's go ahead. And we do need to grab the callback JSON. So what was the actual event? So I've gotten here using our API helper as part of the SDK. It doesn't include a deserializer, so it'll take that raw data and parse it into a dictionary for us. It is worth noting that for our messaging callbacks, if I look at an inbound group message or inbound normal message, each callback is set a list. However, there's only ever one object per list.
It is still a list, so I'm just going to pop the top one off of that. You could build your server to handle that as there could be more than one event. Right now Bandwidth will never send more than one object per list, but it could be something that we upgrade to in the future. So if you were to build it out as accepting a list, you would be able to be future-proof in that regard. But we would not be doing any of that change without ample of communication ahead of time.
Then I'm going to actually go ahead and build my Bandwidth callback message. I'm going to use this callback message from dictionary method to pass in that raw dict that I have earlier. And that's going to give me the messaging callback object. That's going to be this inner object right here. And our SDK has the nice methods to access each one of those fields. So instead of having to treat it like a dictionary, you can just grab that information there. And then I'm going to grab the Bandwidth callback message, the internal message right here. So it's got a little bit more information. Like the ID, who was it from, and who it was to?
So at this point, I have my messaging callback, my global event here. And I have my inner event pulled out separately. So that way I can access the methods a little bit easier. And I've got my type hints in here for Python, for my interpreter to be a little nice and clean there for us. So the next thing we need to do is we need to do check if it's a DLR. So for the balanced messaging API, we send both inbound and outbound status event to the same URL.
And we can look at the difference between those if I actually get my tabs right. We can look at the difference where the direction here for an inbound SMS, is going to be in. And then for an outbound DLR, letting you know status of that message, the direction is going to be out. You can also look at the type. You can see a message received, or a message failed, or a message delivered based on the callback that you would have. So if you have a failure, you'd get the type message failed, whereas an inbound message is always going to be message received. I personally just checked the direction. To me it makes sense. You can build this however you would need. And so I have a nice little check here if it is a DLR. If the message direction is out. So we know this is a logged event. We're going to do that exactly. We're just going to log that message to the screen and then move on with our lives.
Next thing we need to do is we're going to need to build the array. So this is a little bit of a overkill depending on your app, but I did want to build this to handle both group messages as well as individual SMS conversations. So when we look at a group message, that is a many to many scenario, where you have multiple people talking to each other. You can kind of look at this as like a group chat with your family about where do we go for dinner tonight. Everyone's responding, you have that single thread on your phone. So we want to make sure that if someone texts "dog," in this Bandwidth number too from that thread, it responds to the entire thread instead of just the user that sent that message.
So if we take a look back at the documentation here, and we look at incoming group message. I'm going to make this a little wider so that the page breaks a little cleaner. We see that the "to" field does come in as an array. So these are the phone numbers that were involved with the message along with "from" number. So in this example, these three phone numbers are in a conversation about whatever.
And then you can see down in here, there's this owner field. This owner field is going to be the Bandwidth number. You can see that it's present in this "to" array, as well as the owner field. So if I wanted to respond to this group, I need to take the owner out of this array and push "from" number back in there. So you can have up to 10 recipients into a group message. So the same thing if this was one person, two people, three, all the way up to 10. The logic there is the same.
So the first thing I do is I copy that array. So I don't modify the original one. I strip the owner out of it. And then I append the "from" number there. It is worth noting that "from" is a keyword in Python. And so every time we have from, we'll clarify our in ProM so that we can use that exact phrase there in Python. OK.
So at this point, we've got our "to" array. We know who we're going to be sending the phone number from, right? We're going to always be responding from that Bandwidth number. And we've got a nice "to" array. And we're pretty much ready to build the message request. What are we going to send to Bandwidth? So I'll just tab this out. There we are. So I'm going to go ahead and just start building that message request.
This take that application ID that we have said earlier. It's going to be to that array that we just made. And then as I said before, the from is always going to be from that Bandwidth number, that owner. And at this point, we need to make sure if it was "dog," right? That's was the whole point of the app. If you send the word "dog," we send you a dog picture. So let's do that logic now.
So we have our message text. I'll slim that to lower case and strip off any trailing or preceding white spaces so you can type "dog" with all caps or not and we're still going to match here. Right? So we normalize that. So if it is a "dog" text, we're going to set the text of that message to the little dog emoji, and then we're going to set the media to a list of this image here. Again, keeping back up with our API method to Create Message, any media is sent as a list. So you could attach multiple images here if you wanted to. And then if it's not a dog, anything else, we don't care, we're just going to reply "Hello from Bandwidth." Pretty boring. Always text "dog" because dog pictures are awesome. OK.
So now let's actually build that client and then send the message and return a respond back to that Bandwidth callback. OK. So a little bit of unpacking here. We have our Bandwidth Client as we defined up here earlier. And with the way Bandwidth is set up, we have multiple APIs for each one of our products. So the voice API is separate than the message API, separate from the phone number API. So we do have a client off of the Bandwidth client to get that. And so you can access that with the messaging client. And then finally, you have your real client there that has the methods like Create Message hanging off of that. And that's going to be an API controller type.
So the next thing we do is we're actually just going to send that message. We're going to use this Create Message reque-- oh, my. There we go. Can't see. There we go. So we're going to create that message. I'm going to go ahead and give it the account ID that we have set there. And then the message request that we've built up here, and then in our if else statement. So we're going to be sending it from this ID. Here's our full message request. Then respond with an API response. Within the API response, we have the Bandwidth message which is going to have our ID. So if we look at the response here, this is our API response dot body. It's sitting here. The API response would contain things like the status code, the content type, all the metadata. We're just looking at the body of the request right here. It's going to be the message response. And then we're just going to log that to screen. We're just going to say, hey, we sent this message.
And we're going to return to the Bandwidth API, like, hey, we handled that message callback. And so if we go through all together, there is what our message handler looks like. We're going to grab the JSON, throw it into a Bandwidth callback message do deserializer, grab that metadata or that information about the direction. If it's a callback event, log it to screen, move on. Otherwise, let's build that "to" array so that we respond to group messages correctly. We're going to build that message request. If the inbound text was "dog," we're going to set that request to a dog emoji and send a dog picture. Otherwise, we're going to wave, "Hello from Bandwidth." We're going to create that API controller, we're going to send that message, and then log the message request out to screen.
So if I go ahead and click Play here, we can see that our server is running. I'm going to pull up my dial pad app. And if I send the word, "hey," it's success. Good. Perfect. We get "Hello from Bandwidth." And then you can see in my log statements we sent message with this ID, and then we received the callback for message ID that and the status is OK. Awesome. Now let's send the word, "dog." We sent that message with ID. You can see the difference between the status between MMS and SMS. Our status callback message is "Message delivered to a carrier," instead of "OK." We have a different message ID, which obviously makes sense. And so there you have it.
So we are now inspecting inbound text and responding appropriately. So while I take a drink of water, are there any questions anyone has about what we've done so far?
TRACY: I don't see any questions on the screen yet. But again, don't hesitate to ask anything. We can take some extra time at the end just in case you have extra questions. But feel free to go ahead and drop them in if you've got some.
DAN TOLBERT: All right. I'm just going to keep going. And keep an eye on the chat here for me please, Tracy. OK. So at this point, we've got our text message handler pretty much built. Let's start handling inbound voice-- of the route predefined here, that we did create in our application. So any time this phone number is called, we're going to hit this endpoint right here.
So let's go ahead and think about what we want to do. So we do want to play a little arithmetic game. We're going to ask our inbound caller to please give us the sum, what is nine plus two? And then we're going to be checking those digits and playing our message based on what they respond. So we're just going to go ahead and grab our BXML, or just the sentence. Now we want to speak to people. And then we're going to be using our BXML text to speech engine. And if we take a look at the documentation there, callbacks and if I look at SpeakSentence, I do need to specify a voice. And so I'm going to be choosing Kate just to play. That's the gender and the dialect used for the text speech engine. You can use any of these combinations as you see fit. I'm just going to keep it relatively basic.
And then go ahead and create that verb. So we now have a SpeakSentence verb object. Now we do need to create our gather event-- sorry, our gather request, apologies. So under our gather, we can see that we can nest SpeakSentence, and we will be doing that. But a gather is basically a way to tell the Bandwidth service to please expect someone to enter digits. So you'll get this as, "Please dial your party's extension." You'll get this, "Press 1 for pizza, 2 for tacos." Voicemail access codes. Anything. Really. Anytime you ask a user for input, in telco, that's going to be referred to as a gather.
And we want to make sure that we put our sentence within that gather response so that we don't have to wait for the entire sentence to be spoken before we allow users to put in input. So you can look at that as anytime you've been interacting with an IVR, and it's like, please, enter your account number. And then it's got a really long message, you start dialing your account number before the sentence is over, and then it doesn't register any of that input, and you have to do it all over again, you don't know actually what digits you pressed and what was accepted, yeah, that is all going to be solved by nesting the SpeakSentence verb within that gather verb.
So if we look at the way we do that in Python, relatively straightforward. We're going to have our gather be a new gather. We're going to set it up so that we only accept the first two digits. So we're not going let you get super wrong, right? You can't dial 1,001 here. You can only press two digits. We're going to give you 10 seconds to answer that. So we're going to time out after 10 seconds.
When we get that request finished, and when the user has pressed either two digits or the timeout, we're going to send that event to a relative URL called callbacks voice gather. So we now know we need to create a new endpoint to accept that gather response. And then finally, we're going to nest that SpeakSentence within that gather verb. And this is all generated. It will look very similar to how this BXML is in here. Keeping in mind that BXML does go in order. So if you were to put any verbs before the gather, it's going to execute that to its completion and then it'll move to the gather verb.
To continue over with the Python, we do need to create a response. It's a very simple response. We'll just go ahead and add that verb. And we're going to add the gather verb. And then just get this over here. I'm going to copy and paste. So don't type anything. And so now we're going to go ahead and call the to BXML method on that response object. I'm just going to print it to screen so we get an idea of what it looks like. And then I want to turn BXML, not the string BXML.
So at this point, if we were to call that number, we're going to be asked to play a game, what is nine plus two. We're going to press some digits. It'll gather those digits and send it to the callback URL that we have specified here. And it's going to fail because we don't have that handled. So let's a look at what a callback for gather looks like. So there we go.
It's going to be sending this event to our newly created end point. It's going to say, here's your event type. It's going to be a gather. And then the key thing we're looking for are the digits. It comes in as a string. So let's actually go ahead and create that endpoint. Paste this here. Cool. And here we go.
So now we do have an endpoint to accept that gather request. But this time we actually do care what that callback was. Right? Up here, we didn't actually inspect anything about the callback. We're going to always respond with this, let's play a game, what is nine plus two, inside of a gather to everybody that calls this number. We don't inspect who it was from, who it was to, we don't care. We're always going to reply with that same thing.
You can look at this as a press one to forward or any type of real world use case that's not asking someone the sum of two digits here. So I'm just going to grab the digits out of that callback using the JSON object within Python. What's it fussing at me for? Neither digits. Nothing right now. Unindent. Oh, I see what I've done. That was certainly not it. There we go. Now it's not fussing at me. OK. Yeah, we're good. All right.
So we're going to grab the digits. So at this point, we have our string loaded in here. I've got two files just hosted up on the internet that we can grab for free. One is a success sound, one is a sad trombone or failure WAV. And then we're going to go ahead and assign that variable to an audio URI. So if the digits are equal to one one, so they did get their math right, 11, we're going to play success or have that saved to our audio URI variable. Otherwise, we're going to fail it.
And then our play audio verb is going to be pretty simple. We're just going to push that audio URI to the audio verb. If we take a look at the docs, call control, play audio. Pretty trivial. Just an empty tag, really, with no attributes, with the URI the WAV file you want to play. And then we're going to create a hang up. So we don't really want to let that call linger. After we play our success or failure, we're just going to hang that call up. We don't need to wait. We're just going to play nice Bandwidth citizen and clean up after ourselves. Let's copy this here.
And then I'm going to add that verb, play audio, to our response. We need to create the response. Let's actually do that. So we're going to add that to our newly minted response. We're then going to add that hang up event or that hang up request. Generate that BXML, print it to screen, and then send that back to the Bandwidth API. So I'm going to head in. Am I still running? Yeah. I'm going to stop my server, fire it back up, no errors, head back to my dial pad. Let's make sure our dog responder's still works.
There we are. So we did get our callback event message delivered. Here we are. Dog event. Nice. And we should very quickly get our response back here. Yep. So it is worth noting that MMS does take a little bit longer to get delivered than a normal text message. So it's just a fun little side effect. So now let's play our game. Hopefully, you can hear my computer audio. So what's nine plus two? 33. We get our fail.wav, and then the call is going to hang up. OK. Let's actually try again. Right? We can do better than that. And we get the tada.wav file. OK.
So that's a very fast 35 minute demo of what building an autoresponder with Python flask and Bandwidth could look like. So all this is out on the web under this Bandwidth Examples repo on GitHub, specifically the webinar example. This is using our Python SDK for our V2 APIs. And it's free out there for you to grab and run. So at this point, we've built our app, we've set up our Bandwidth dashboard, we're quizzing people about basic arithmetic and sending them dog pictures.
So any questions about the Bandwidth ecosystem, Python, or anything else with our webinar before we end it today?
TRACY: Thanks, Dan. Yeah, we'll open it up to questions. So if you guys have any, again, don't hesitate to throw any in there. We'll leave it for another minute or so, and if we haven't gotten any of them we'll go ahead and close you guys out. But for anyone who leaves before the minute is up, we appreciate you joining us today amidst everything that's happening right now in the world. So yeah. Definitely come back next week if you guys are interested in the PHP webinar. And again, this recording, you guys will actually receive to your email probably sometime tomorrow morning.
And feel free to go to Bandwidth.com/resources if you're interested in seeing any of the past developings with Bandwidth webinars or any of our future webinars moving forward. We've got some cool ones with Voice 101 that we're about to run. And we've got a couple of 9-1-1 webinars as well. And I still haven't seen any questions come in. So if you guys do have anything that pops up, don't hesitate to reach out to us. And yeah. Thanks again, Dan, for presenting for us. And thank you guys for joining.
DAN TOLBERT: Yup. Thank you all for watching. Appreciate it.
TRACY: Have a good one.