diff --git a/api/.htaccess b/api/.htaccess index e73e2eb3b9c3f1204223b4426274817c0200e279..f460420d6d2f55f362fb420788b6751afe38bbba 100644 --- a/api/.htaccess +++ b/api/.htaccess @@ -1,8 +1,11 @@ -RewriteEngine On +<IfModule mod_rewrite.c> -RewriteBase /api/ +RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d +RewriteCond %{REQUEST_URI} (.*/api) + +RewriteRule ^(.*)$ %1/http.php/$1 [L] -RewriteRule ^(.*)$ http.php/$1 [L] +</IfModule> diff --git a/api/pipe.php b/api/pipe.php index 29dfcff1d1aa10a20386589927830ffed8211911..699e7400001c8f497a5bbd30b5d921ca353abe2b 100644 --- a/api/pipe.php +++ b/api/pipe.php @@ -90,31 +90,38 @@ $var['header']=$parser->getHeader(); $var['priorityId']=$cfg->useEmailPriority()?$parser->getPriority():0; $ticket=null; -if(preg_match ("[[#][0-9]{1,10}]",$var['subject'],$regs)) { +if(preg_match ("[[#][0-9]{1,10}]", $var['subject'], $regs)) { $extid=trim(preg_replace("/[^0-9]/", "", $regs[0])); - $ticket= new Ticket(Ticket::getIdByExtId($extid)); - //Allow mismatched emails?? For now hell NO. - if(!is_object($ticket) || strcasecmp($ticket->getEmail(),$var['email'])) - $ticket=null; + if(!($ticket=Ticket::lookupByExtId($extid, $var['email'])) || strcasecmp($ticket->getEmail(), $var['email'])) + $ticket = null; } + $errors=array(); $msgid=0; -if(!$ticket) { //New tickets... - $ticket=Ticket::create($var,$errors,'email'); - if(!is_object($ticket) || $errors) { - api_exit(EX_DATAERR,'Ticket create Failed '.implode("\n",$errors)."\n\n"); - } +if($ticket) { + //post message....postMessage does the cleanup. + if(!($msgid=$ticket->postMessage($var['message'], 'Email',$var['mid'],$var['header']))) + api_exit(EX_DATAERR, 'Unable to post message'); +} elseif(($ticket=Ticket::create($var, $errors, 'email'))) { // create new ticket. $msgid=$ticket->getLastMsgId(); +} else { // failure.... -} else { - //post message....postMessage does the cleanup. - if(!($msgid=$ticket->postMessage($var['message'], 'Email',$var['mid'],$var['header']))) { - api_exit(EX_DATAERR, 'Unable to post message'); + // report success on hard rejection + if(isset($errors['errno']) && $errors['errno'] == 403) + api_exit(EX_SUCCESS); + + // check if it's a bounce! + if($var['header'] && TicketFilter::isAutoBounce($var['header'])) { + $ost->logWarning('Bounced email', $var['message'], false); + api_exit(EX_SUCCESS); } + + api_exit(EX_DATAERR, 'Ticket create Failed '.implode("\n",$errors)."\n\n"); } + //Ticket created...save attachments if enabled. -if($cfg->allowEmailAttachments() && ($attachments=$parser->getAttachments())) { +if($ticket && $cfg->allowEmailAttachments() && ($attachments=$parser->getAttachments())) { foreach($attachments as $attachment) { if($attachment['filename'] && $ost->isFileTypeAllowed($attachment['filename'])) $ticket->saveAttachment(array('name' => $attachment['filename'], 'data' => $attachment['body']), $msgid, 'M'); diff --git a/include/api.ticket.php b/include/api.ticket.php index 4cb4eb02d59a8e0e9ef283abde909cc74628d4c6..7fd5ba713c8237aad483c4a44b08989cf9b17c3f 100644 --- a/include/api.ticket.php +++ b/include/api.ticket.php @@ -15,7 +15,7 @@ class TicketController extends ApiController { "attachments" => array("*" => array("name", "type", "data", "encoding") ), - "message", "ip" + "message", "ip", "priorityId" ); if ($format == "xml") return array("ticket" => $supported); else return $supported; @@ -45,7 +45,7 @@ class TicketController extends ApiController { if (!($info["data"] = base64_decode($info["data"], true))) Http::response(400, sprintf( "%s: Poorly encoded base64 data", - $filename)); + $info['name'])); } $info['size'] = strlen($info['data']); } diff --git a/include/class.api.php b/include/class.api.php index 8a15c04a493c9a71cc3ae2c137ce1cc65b064fbc..15b54ebeff14eaa5240c9c81fa09436d990379c1 100644 --- a/include/class.api.php +++ b/include/class.api.php @@ -208,7 +208,7 @@ class ApiController { function validate($data, $structure, $prefix="") { foreach ($data as $key=>$info) { if (is_array($structure) and is_array($info)) { - $search = isset($structure[$key]) ? $key : "*"; + $search = (isset($structure[$key]) && !is_numeric($key)) ? $key : "*"; if (isset($structure[$search])) { $this->validate($info, $structure[$search], "$prefix$key/"); continue; @@ -243,16 +243,21 @@ class ApiXmlDataParser extends XmlDataParser { } else if ($key == "autorespond") { $value = (bool)$value; } else if ($key == "attachments") { - foreach ($value as &$info) { - $info["data"] = $info[":text"]; - unset($info[":text"]); + if(!isset($value['file'][':text'])) + $value = $value['file']; + + if($value && is_array($value)) { + foreach ($value as &$info) { + $info["data"] = $info[":text"]; + unset($info[":text"]); + } + unset($info); } - unset($info); - } - if (is_array($value)) { + } else if(is_array($value)) { $value = $this->fixup($value); } } + return $current; } } @@ -279,7 +284,7 @@ class ApiJsonDataParser extends JsonDataParser { # PHP5: fopen("data://$data[5:]"); if (substr($data, 0, 5) != "data:") { $info = array( - "data" => $data, + "data" => $data, "type" => "text/plain", "name" => key($info)); } else { @@ -288,11 +293,17 @@ class ApiJsonDataParser extends JsonDataParser { list($type, $extra) = explode(";", $meta); $info = array( "data" => $contents, - "type" => $type, + "type" => ($type) ? $type : "text/plain", "name" => key($info)); if (substr($extra, -6) == "base64") $info["encoding"] = "base64"; - # TODO: Handle 'charset' hint in $extra + # Handle 'charset' hint in $extra, such as + # data:text/plain;charset=iso-8859-1,Blah + # Convert to utf-8 since it's the encoding scheme + # for the database. Otherwise, assume utf-8 + list($param,$charset) = explode('=', $extra); + if ($param == 'charset' && function_exists('iconv')) + $contents = iconv($charset, "UTF-8", $contents); } } unset($value); diff --git a/include/class.filter.php b/include/class.filter.php index c7f1d70c015405a322a9bd3ed17de21d77cf4981..b6f5305959ab64c9be3df7789c7fe2cb6bd9ae2a 100644 --- a/include/class.filter.php +++ b/include/class.filter.php @@ -870,6 +870,10 @@ class TicketFilter { * http://msdn.microsoft.com/en-us/library/ee219609(v=exchg.80).aspx */ /* static */ function isAutoResponse($headers) { + + if($headers && !is_array($headers)) + $headers = Mail_Parse::splitHeaders($headers); + $auto_headers = array( 'Auto-Submitted' => 'AUTO-REPLIED', 'Precedence' => array('AUTO_REPLY', 'BULK', 'JUNK', 'LIST'), @@ -879,21 +883,56 @@ class TicketFilter { 'X-Autoresponse' => '', 'X-Auto-Reply-From' => '' ); + foreach ($auto_headers as $header=>$find) { - if ($value = strtoupper($headers[$header])) { - # Search text must be found at the beginning of the header - # value. This is especially import for something like the - # subject line, where something like an autoreponse may - # appear somewhere else in the value. - if (is_array($find)) { - foreach ($find as $f) - if (strpos($value, $f) === 0) - return true; - } elseif (strpos($value, $find) === 0) { - return true; - } + if(!isset($headers[$header])) continue; + + $value = strtoupper($headers[$header]); + # Search text must be found at the beginning of the header + # value. This is especially import for something like the + # subject line, where something like an autoreponse may + # appear somewhere else in the value. + + if (is_array($find)) { + foreach ($find as $f) + if (strpos($value, $f) === 0) + return true; + } elseif (strpos($value, $find) === 0) { + return true; } } + + # Bounces also counts as auto-responses. + if(self::isAutoBounce($headers)) + return true; + + return false; + } + + function isAutoBounce($headers) { + + if($headers && !is_array($headers)) + $headers = Mail_Parse::splitHeaders($headers); + + $bounce_headers = array( + 'From' => array('<MAILER-DAEMON@MAILER-DAEMON>', 'MAILER-DAEMON', '<>'), + 'Subject' => array('DELIVERY FAILURE', 'DELIVERY STATUS', 'UNDELIVERABLE:'), + ); + + foreach ($bounce_headers as $header => $find) { + if(!isset($headers[$header])) continue; + + $value = strtoupper($headers[$header]); + + if (is_array($find)) { + foreach ($find as $f) + if (strpos($value, $f) === 0) + return true; + } elseif (strpos($value, $find) === 0) { + return true; + } + } + return false; } diff --git a/include/class.mailer.php b/include/class.mailer.php index 2a965eb371cf3d3cf952e48be313f533bdd084a5..a227b8fc52951c563d56ccd4d59684149ae15336 100644 --- a/include/class.mailer.php +++ b/include/class.mailer.php @@ -92,9 +92,9 @@ class Mailer { require_once (PEAR_DIR.'Mail/mime.php'); // PEAR Mail_Mime packge //do some cleanup - $to=preg_replace("/(\r\n|\r|\n)/s",'', trim($to)); - $subject=stripslashes(preg_replace("/(\r\n|\r|\n)/s",'', trim($subject))); - $body = stripslashes(preg_replace("/(\r\n|\r)/s", "\n", trim($message))); + $to = preg_replace("/(\r\n|\r|\n)/s",'', trim($to)); + $subject = preg_replace("/(\r\n|\r|\n)/s",'', trim($subject)); + $body = preg_replace("/(\r\n|\r)/s", "\n", trim($message)); /* Message ID - generated for each outgoing email */ $messageId = sprintf('<%s%d-%s>', Misc::randCode(6), time(), diff --git a/include/class.mailfetch.php b/include/class.mailfetch.php index aa43ce89e960231bdf2e25bf0f622a24d2b44741..fce15f537eb6125b5689ebef37879d267e90f850 100644 --- a/include/class.mailfetch.php +++ b/include/class.mailfetch.php @@ -378,7 +378,7 @@ class MailFetcher { //Is the email address banned? if($mailinfo['email'] && TicketFilter::isBanned($mailinfo['email'])) { //We need to let admin know... - $ost->logWarning('Ticket denied', 'Banned email - '.$mailinfo['email']); + $ost->logWarning('Ticket denied', 'Banned email - '.$mailinfo['email'], false); return true; //Report success (moved or delete) } @@ -417,6 +417,16 @@ class MailFetcher { } elseif (($ticket=Ticket::create($var, $errors, 'Email'))) { $msgid = $ticket->getLastMsgId(); } else { + //Report success if the email was absolutely rejected. + if(isset($errors['errno']) && $errors['errno'] == 403) + return true; + + # check if it's a bounce! + if($var['header'] && TicketFilter::isAutoBounce($var['header'])) { + $ost->logWarning('Bounced email', $var['message'], false); + return true; + } + //TODO: Log error.. return null; } diff --git a/include/class.ticket.php b/include/class.ticket.php index c8d0e8c640f1b918a70166934320ddd431fec1ac..b4b7952b5f5740ab71301f324059a6eb7730eb98 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -976,7 +976,7 @@ class Ticket { $autorespond=$dept->autoRespONNewMessage(); - if(!$autorespond && !$cfg->autoRespONNewMessage()) return; //no autoresp or alerts. + if(!$autorespond || !$cfg->autoRespONNewMessage()) return; //no autoresp or alerts. $this->reload(); @@ -2019,6 +2019,7 @@ class Ticket { //Make sure the email address is not banned if(TicketFilter::isBanned($vars['email'])) { $errors['err']='Ticket denied. Error #403'; + $errors['errno'] = 403; $ost->logWarning('Ticket denied', 'Banned email - '.$vars['email']); return 0; } @@ -2044,6 +2045,7 @@ class Ticket { if($ticket_filter && ($filter=$ticket_filter->shouldReject())) { $errors['err']='Ticket denied. Error #403'; + $errors['errno'] = 403; $ost->logWarning('Ticket denied', sprintf('Ticket rejected ( %s) by filter "%s"', $vars['email'], $filter->getName())); diff --git a/include/class.variable.php b/include/class.variable.php index f83ba61be9063d385943b4fe4f4fdc751a58f422..7d49ce592855e3d65bb5d41165bcbad6b5346bc9 100644 --- a/include/class.variable.php +++ b/include/class.variable.php @@ -98,7 +98,7 @@ class VariableReplacer { if(!($vars=$this->_parse($input))) return $input; - return preg_replace($this->_delimit(array_keys($vars)), array_values($vars), $input); + return str_replace(array_keys($vars), array_values($vars), $input); } function _resolveVar($var) { @@ -134,14 +134,5 @@ class VariableReplacer { return $vars; } - - //Helper function - will be replaced by a lambda function (PHP 5.3+) - function _delimit($val, $d='/') { - - if($val && is_array($val)) - return array_map(array($this, '_delimit'), $val); - - return $d.$val.$d; - } } ?> diff --git a/include/class.xml.php b/include/class.xml.php index 854f182372c94f57338b8b45a94d155f69f5bd33..56baf4fbccaf65985921e081afd8e5f475c95c07 100644 --- a/include/class.xml.php +++ b/include/class.xml.php @@ -77,10 +77,14 @@ class XmlDataParser { $this->content = array_pop($this->stack); $i = 1; if (array_key_exists($name, $this->content)) { - while (array_key_exists("$name$i", $this->content)) $i++; - $name = "$name$i"; - } - $this->content[$name] = $prev; + if(!isset($this->content[$name][0])) { + $current = $this->content[$name]; + unset($this->content[$name]); + $this->content[$name][0] = $current; + } + $this->content[$name][] = $prev; + } else + $this->content[$name] = $prev; } function content($parser, $data) { diff --git a/include/client/open.inc.php b/include/client/open.inc.php index 2fd076100388888765970d1a561dc3c5235a8b90..275e856dea07f8726f06b3fa6641392e459bb2e4 100644 --- a/include/client/open.inc.php +++ b/include/client/open.inc.php @@ -57,7 +57,7 @@ $info=($_POST && $errors)?Format::htmlchars($_POST):$info; <td class="required">Help Topic:</td> <td> <select id="topicId" name="topicId"> - <option value="" selected="selected">— Select a Help Topics —</option> + <option value="" selected="selected">— Select a Help Topic —</option> <?php if($topics=Topic::getPublicHelpTopics()) { foreach($topics as $id =>$name) { @@ -82,7 +82,7 @@ $info=($_POST && $errors)?Format::htmlchars($_POST):$info; <tr> <td class="required">Message:</td> <td> - <div><em>Please provide as much details as possible so we can best assist you.</em> <font class="error">* <?php echo $errors['message']; ?></font></div> + <div><em>Please provide as much detail as possible so we can best assist you.</em> <font class="error">* <?php echo $errors['message']; ?></font></div> <textarea id="message" cols="60" rows="8" name="message"><?php echo $info['message']; ?></textarea> </td> </tr> diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php index 534a358bfcc01bbb771340f78db2b5fcd7924cbd..341867b3ea2c9829d9c4a0e8e1f489c369a4d7f8 100644 --- a/include/staff/tickets.inc.php +++ b/include/staff/tickets.inc.php @@ -195,6 +195,8 @@ if($_REQUEST['sort'] && $sortOptions[$_REQUEST['sort']]) $order_by =$sortOptions[$_REQUEST['sort']]; elseif(!strcasecmp($status, 'open') && !$showanswered && $sortOptions[$_SESSION['tickets']['sort']]) { $_REQUEST['sort'] = $_SESSION['tickets']['sort']; + $_REQUEST['order'] = $_SESSION['tickets']['order']; + $order_by = $sortOptions[$_SESSION['tickets']['sort']]; $order = $_SESSION['tickets']['order']; } diff --git a/l.php b/l.php index 5e605c73cb3d48c32409c8ff89f727199bcbbba3..286a17299cd1e51850b3760ed4e6d264244f7974 100644 --- a/l.php +++ b/l.php @@ -21,7 +21,7 @@ if (!$url || !Validator::is_url($url)) exit('Invalid url'); <html> <head> <meta http-equiv="content-type" content="text/html; charset=utf-8"/> - <meta http-equiv="refresh" content="0;<?php echo $url; ?>"/> + <meta http-equiv="refresh" content="0;URL=<?php echo $url; ?>"/> </head> <body/> </html> diff --git a/scp/css/scp.css b/scp/css/scp.css index 59964d4e61561ba78bc704e3e6ddb2d1a73af964..67e6212512c0bf617d934fbf1feabd369784fd80 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -311,6 +311,7 @@ a.Icon:hover { .Icon.webTicket { background:url(../images/icons/ticket_source_web.gif) 0 0 no-repeat; } .Icon.emailTicket { background:url(../images/icons/ticket_source_email.gif) 0 0 no-repeat; } .Icon.phoneTicket { background:url(../images/icons/ticket_source_phone.gif) 0 0 no-repeat; } +.Icon.apiTicket { background:url(../images/icons/ticket_source_other.gif) 0 0 no-repeat; } .Icon.otherTicket { background:url(../images/icons/ticket_source_other.gif) 0 0 no-repeat; } .Icon.overdueTicket { background:url(../images/icons/overdue_ticket.gif) 0 0 no-repeat; } .Icon.assignedTicket { background:url(../images/icons/assigned_ticket.gif) 0 0 no-repeat; } diff --git a/scp/l.php b/scp/l.php index 93fff3a24894612017f53ff0bb0a119b656f8b9d..dec8c0a6a52dd62c0d02f3edebfeeeaa79c0c8fa 100644 --- a/scp/l.php +++ b/scp/l.php @@ -21,7 +21,7 @@ if (!$url || !Validator::is_url($url)) exit('Invalid url'); <html> <head> <meta http-equiv="content-type" content="text/html; charset=utf-8"/> - <meta http-equiv="refresh" content="0;<?php echo $url; ?>"/> + <meta http-equiv="refresh" content="0; URL=<?php echo $url; ?>"/> </head> <body/> </html> diff --git a/setup/doc/api.md b/setup/doc/api.md new file mode 100644 index 0000000000000000000000000000000000000000..7509616aa2406b842f56cacd7a6c0693b23415ca --- /dev/null +++ b/setup/doc/api.md @@ -0,0 +1,29 @@ +osTicket API +============ + +The osTicket API is implemented as (somewhat) simple XML or JSON over HTTP. +For now, only ticket creation is supported, but eventually, all resources +inside osTicket will be accessible and modifiable via the API. + +Authentication +-------------- + +Authentication via the API is done via API keys configured inside the +osTicket admin panel. API keys are created and tied to a source IP address, +which will be checked against the source IP of requests to the HTTP API. + +API keys can be created and managed via the admin panel. Navigate to Manage +-> API keys. Use *Add New API Key* to create a new API key. Currently, no +special configuration is required to allow the API key to be used for the +HTTP API. All API keys are valid for the HTTP API. + +Wrappers +-------- + +Currently, there are no wrappers for the API. If you've written one and +would like it on the list, submit a pull request to add your wrapper. + +Resources +--------- + +- [Tickets](api/tickets.md) diff --git a/setup/doc/api/tickets.md b/setup/doc/api/tickets.md new file mode 100644 index 0000000000000000000000000000000000000000..fc79b1aead833a9f4cf9fd3a214635dcba9e52fc --- /dev/null +++ b/setup/doc/api/tickets.md @@ -0,0 +1,128 @@ +Tickets +======= +The API supports ticket creation via the HTTP API (as well as via email, +etc.). Currently, the API support creation of tickets only -- so no +modifications and deletions of existing tickets is possible via the API for +now. + +Create a Ticket +--------------- + +Tickets can be created in the osTicket system by sending an HTTP POST to +`api/tickets.xml` or `api/tickets.json` depending on the format of the +request content. + +### Fields ###### + +* __email__: *required* Email address of the submitter +* __name__: *required* Name of the submitter +* __subject__: *required* Subject of the ticket +* __message__: *required* Initial message for the ticket thread +* __alert__: If unset, disable alerts to staff. Default is `true` +* __autorespond__: If unset, disable autoresponses. Default is `true` +* __ip__: IP address of the submitter +* __phone__: Phone number of the submitter +* __phone_ext__: Phone number extension -- can also be embedded in *phone* +* __priorityId__: Priority *id* for the new ticket to assume +* __source__: Source of the ticket, default is `API` +* __topicId__: Help topic *id* associated with the ticket +* __attachments__: An array of files to attach to the initial message. + Each attachment must have some content and also the + following fields: + * __name__: *required* name of the file to be attached. Multiple files + with the same name are allowable + * __type__: Mime type of the file. Default is `text/plain` + * __encoding__: Set to `base64` if content is base64 encoded + +### XML Payload Example ###### + +* `POST /api/tickets.xml` + +The XML data format is extremely lax. Content can be either sent as an +attribute or a named element if it has no sub-content. + +In the example below, the simple element could also be replaced as +attributes on the root `<ticket>` element; however, if a `CDATA` element is +necessary to hold special content, or difficult content such as double +quotes is to be embedded, simple sub-elements are also supported. + +Notice that the phone extension can be sent as the `@ext` attribute of the +`phone` sub-element. + +``` xml +<?xml version="1.0" encoding="UTF-8"?> +<ticket alert="true" autorespond="true" source="API"> + <name>Angry User</name> + <email>api@osticket.com</email> + <subject>Testing API</subject> + <phone ext="123">318-555-8634</phone> + <message><![CDATA[Message content here]]></message> + <attachments> + <file name="file.txt" type="text/plain"><![CDATA[ + File content is here and is automatically trimmed + ]]></file> + <file name="image.gif" type="image/gif" encoding="base64"> + R0lGODdhMAAwAPAAAAAAAP///ywAAAAAMAAwAAAC8IyPqcvt3wCcDkiLc7C0qwy + GHhSWpjQu5yqmCYsapyuvUUlvONmOZtfzgFzByTB10QgxOR0TqBQejhRNzOfkVJ + +5YiUqrXF5Y5lKh/DeuNcP5yLWGsEbtLiOSpa/TPg7JpJHxyendzWTBfX0cxOnK + PjgBzi4diinWGdkF8kjdfnycQZXZeYGejmJlZeGl9i2icVqaNVailT6F5iJ90m6 + mvuTS4OK05M0vDk0Q4XUtwvKOzrcd3iq9uisF81M1OIcR7lEewwcLp7tuNNkM3u + Nna3F2JQFo97Vriy/Xl4/f1cf5VWzXyym7PHhhx4dbgYKAAA7 + </file> + </attachments> + <ip>123.211.233.122</ip> +</ticket> +``` + +### JSON Payload Example ### + +* `POST /api/tickets.json` + +Attachment data for the JSON content uses the [RFC 2397][] data URL format. +As described above, the content-type and base64 encoding hints are optional. +Furthermore, a character set can be optionally declared for each attachment +and will be automatically converted to UTF-8 for database storage. + +Notice that the phone number extension can be embedded in the `phone` value +denoted with a capital `X` + +Do also note that the JSON format forbids a comma after the last element in +an object or array definition, and newlines are not allowed inside strings. + +``` json +{ + "alert": true, + "autorespond": true, + "source": "API", + "name": "Angry User", + "email": "api@osticket.com", + "phone": "3185558634X123", + "subject": "Testing API", + "ip": "123.211.233.122", + "message": "MESSAGE HERE", + "attachments": [ + {"file.txt": "data:text/plain;charset=utf-8,content"}, + {"image.png": "data:image/png;base64,R0lGODdhMAA..."}, + ] +} +``` + +[rfc 2397]: http://www.ietf.org/rfc/rfc2397.txt "Data URLs" + +### Response ###### + +If successful, the server will send `HTTP/201 Created`. Otherwise, it will +send an appropriate HTTP status with the content being the error +description. Most likely offenders are + +* Required field not included +* Data type mismatch (text send for numeric field) +* Incorrectly encoded base64 data +* Unsupported field sent +* Incorrectly formatted content (bad JSON or XML) + +Upon success, the content of the response will be the external ticket id of +the newly-created ticket. + + Status: 201 Created + 123456