Toggle menu
We pull stuff out of your documents
Contact Us
Terms & Conditions
Privacy Policy

Contact Us

a ticket.
Mail: support@gristle.com

(must be logged in to create tickets, which yields faster responses)


Terms and Conditions

  1. General Terms
    By using our document parsing service you agree to be bound by these Terms, as well as our Privacy Policy. If you do not agree to these Terms, please do not access or use our services.
  2. Services and Payment
    Our document services run on a subscription period model, with varying capabilities dependent on the account type. Payments for these services are due according to the billing cycle associated with your account.
  3. No Refunds for Current Period
    Once a payment has been made for the current billing period, we do not offer any refunds. This applies to all payments, including those for subscriptions, regardless of whether the service is fully utilized during that period. By making a payment, you acknowledge and agree that no refunds will be granted for the current period.
  4. Cancellation of Services
    While we do not offer refunds, you may cancel your subscription or service at any time. Upon cancellation, your access to the services will continue until the end of the current billing period, but no further charges will be applied after the cancellation.
  5. Billing and Renewals
    By agreeing to these Terms, you authorize us to automatically charge your account for any renewals or subsequent billing periods, unless you cancel your service before the next billing cycle. We will notify you prior to any renewal.
  6. Modifications to the Terms and Services
    We reserve the right to modify these Terms and the services provided at any time. Any changes to these Terms will be posted on this page with an updated "Effective Date." Continued use of the services after such modifications constitutes your acceptance of the updated Terms.
  7. Documents and Materials
    The Client represents and warrants that all documents, images, content, or other materials provided to Gristle for the purposes of this Agreement are either owned by the Client or that the Client has secured all necessary permissions, licenses, and rights to use such materials.
    • If the Client provides materials they do not have permission to use or that infringe upon the rights of any third party, Gristle shall not be held liable for any claims, losses, or damages arising from such materials.
    • The Client shall indemnify, defend, and hold Gristle harmless from any claims, damages, or liabilities, including legal fees, arising from the Client’s use of such unauthorized materials.
  8. Termination
    Gristle reserves the right to terminate this Agreement immediately, without notice, if the Client:
    • Fails to comply with any of the terms set forth in this Agreement;
    • Provides false, misleading, or unauthorized information;
    • Provides any materials, including but not limited to documents, images, or intellectual property, that they do not have proper authorization or rights to use;
    • Exceeds usage beyond the bounds of the account intention, which is defined at the discretion of Gristle.
    In the event of such termination, the Client shall remain liable for any and all fees or costs incurred up to the date of termination, including the current subscription period.
  9. Effect of Termination:
    Termination of this Agreement will not relieve the Client of any outstanding payment obligations for services rendered or costs incurred up to the termination date. All rights and licenses granted under this Agreement will immediately cease, and the Client must promptly return or destroy any materials related to Gristle as per the instructions given.
  10. Limitation of Liability
    Gristle shall not be liable for any claims, losses, or damages arising out of the Client’s use of materials for which they do not have proper authorization. This includes, but is not limited to, claims from third parties regarding intellectual property infringement, breach of privacy, or misuse of content. The Client assumes full responsibility for any legal consequences or actions resulting from their provision of unauthorized materials.
  11. Governing Law
    These Terms are governed by and construed in accordance with the laws of the United States. Any dispute arising from these Terms shall be resolved in the competent courts of the United States.
  12. Contact Information
    If you have any questions about these Terms or our services, please contact us.

Privacy Policy

  1. Information We Collect
    We do not collect personal information from users browsing our website. However, to provide access to our API, we may collect a unique identification token or key that is used for authentication purposes. This identification is necessary to grant access to our API and may be associated with your account or session. This information is used solely for the purpose of providing API access and is not shared with third parties.
  2. Use of API Keys
    When accessing our API, we issue a unique API key that serves as a form of authentication. This key is used to identify your requests and ensure that only authorized users can access the API. The API key is stored locally on your system and is not stored or retained on our servers after the session ends. We do not collect or retain any personally identifiable information in relation to the API key.
  3. No Use of Cookies
    Our website does not use cookies, web beacons, or any other tracking technologies to collect data about your browsing activity. We do not track or retain any data related to your visit to our website.
  4. No Data Retention
    We do not retain or store any personal information from website visitors. The only information that is retained temporarily is the local API key, which is only used to authenticate access to the API and is not shared or stored beyond the duration of your session.
  5. Third-Party Services
    Our website may contain links to third-party websites, tools, or services. Please be aware that we are not responsible for the privacy practices of these external sites. We encourage you to review the privacy policies of any third-party services you engage with.
  6. Security
    We take reasonable measures to protect your API key and any information transmitted through our website. However, please be aware that no method of online communication or data storage can be guaranteed to be 100% secure. While we strive to ensure the security of your information, we cannot guarantee its absolute security.
  7. Children’s Privacy
    Our website and API are not intended for use by children under the age of 13. We do not knowingly collect any personal information from children. If you believe we have inadvertently collected information from a child, please contact us immediately so we can remove it.
  8. Changes to This Privacy Policy
    We may update this Privacy Policy periodically. Any changes will be posted on this page with an updated "Effective Date." We encourage you to review this Privacy Policy regularly to stay informed about how we protect your privacy.
  9. Contact Us
    If you have any questions or concerns about this Privacy Policy, open a ticket (issue) under your account or contact us at support@gristle.com.
I Agree
Close
HomeAdmin
Issues
    Parsers
      Developers
      Support
      Documents come in...Docs in...
      ...and the data comes out...Data out

      We cut the gristle out of document ingestion, extracting and normalizing your data.

      Is your company wasting time tediously transcribing data from the same third-party documents day in and day out? If so, you're not alone. Whether it's PDFs, Word documents, or even images, we understand that you want ordered and predictable data extracted from your documents to streamline your workflows—and that's exactly what we do.

      We take on the gristly task of transforming unstructured documents into structured data, enabling automated extraction and eliminating the need for manual transcription. Our solution includes an intuitive UI, a robust API, and clear ingestion examples, along with multiple automated export options for seamless integration.

      Where does it fall in the AI landscape?

      Querying LLMs to pull arbitrary, one-time data from documents is where AI truly shines. However, when it comes to procedural and routine extraction from similarly structured documents, using AI quickly becomes impractical. This is not only because it wastes computational credits on your AI accounts to perform the same repetitive tasks, but also because it removes the ability for controlled and granular fine-tuning—the essence of what our service provides.

      What's the process?

      From experience, we've realized that there is a wide spectrum of document formats, and depending on the structure of your documents, cookie-cutter solutions may not always be the answer. With that in mind, we approach the problem with an expansive, two-pronged strategy:

      1. Start with our mapping editor, which enables you to map the data points on your own. This allows you to determine if you can handle it independently or create a ticket using it as a blueprint to indicate the support you need.

      2. Escalate to custom support for your parser if the do-it-yourself approach is too overwhelming, generates too many false positives, or is inadequate to accommodate the complexity of your needs.

      How do I get started?

      Click Start App to begin creating your account. After email verification, a read-only example parser will be shared with your account. This basic parser includes the example PDFs used to create the Map within its description, along with several successfully parsed outputs on the Parse tab.

      After reviewing this parser to determine if we are the right tool for your needs, we recommend creating your first parser, where tooltips will guide you throughout the process. If you need assistance, create an Issue (ticket), attach any relevant files, and we'll get back to you as soon as possible.

      What's the backstory?

      Our service was born out of necessity when onboarding our clients' traffic instructions from the television industry. We've provided this capability to countless clients for nearly two decades, and now we've made it available to the masses.

      Where are the Terms and Conditions?

      support@gristle.com

      Username
      Human? (repeat the 6 letters below)
      Create an account - Forgot password
      Account: [USER] (email not verified)

      E-Mail ✓
      Change Password
      Confirm
      Contact (First, Last)
      Quota Parameters
      Options






      Current Plan

      Free

      $0/mo

      Provides access to demonstrational material. No actual parsing is available on the free tier; however, the mapping editor is available to a limited extent to help determine if we are the right tool for your needs, and support can be consulted.

      • Read-only parsers
      • No resources
      • Consultatory support
      • Limited editor access
      Current Plan

      Basic (DIY)

      $12/mo

      Complete access to create and maintain your own parsers. Strict resource limitations may lead to the termination of large or complex files. Support is provided at our discretion and may require upgrading to a higher account tier.

      • 5 parsers
      • Low resource allotment
      • Discretionary support
      • Full editor access
      Current Plan

      Managed

      $48/mo

      The first tier of continuous support for mapping your parsers, with additional resources allocated. This option should only be selected if you have been approved for support and directed to choose it.

      • 25 parsers
      • High resource allotment
      • Continuous support
      • Full editor access
      Current Plan

      Enterprise

      $98/mo

      The premier tier of continuous support for mapping your parsers, offering expedited support resolution and the highest resource allocation. This option should only be selected if you have been explicitly instructed to.

      • 100+ parsers
      • Highest resource allotment
      • Expedited support
      • Full editor access
      1Information
      2Map Data
      3Parse Files
      Name: shared parser [read-only]
      Input Type:
      Input Profile:
      Input Structure:
      Output Type:
      Output Encoding:
      Output Structure:
      Description (optional):
      Filename matching:
      Send Outputs to E-mail (optional):
      Send Outputs to URL (optional; protocols = ftp, ftps, sftp, http, https):

        ←  select group, single, or exclusion selection mode to add or modify columns
      #?
       SEP: 
        ←  activate positional ruler and text filtering
      Number
      Decimal
      Alphabet
      Alpha-Numerical
      Money
      Date
      Time
      URL
      E-Mail address
      [ Awaiting data... ]
      All snippets require the appropriate USER, PASS and PARSER_ID fields populated.
      Bash (Shell)
      C#
      Go
      Java
      Javascript (Node)
      Lua
      PHP
      PowerShell
      Python
      Ruby
      📋
      #!/bin/bash
      
      # Push a file to Gristle for parsing, with the option of waiting to download the parsed output.
      #
      # USAGE:
      #   ./this.sh <input_file.ext> [output_file.ext]
      
      G_USER="~G_USER~"
      G_PASS="~G_PASS~"
      G_PARSER_ID="~G_PARSER_ID~" # ~G_PARSER_NAME~ (leave blank to infer)
      
      # --- EDIT ABOVE AS NEEDED ---
      G_IN_FILE="$1"
      G_OUT_FILE="$2"
      G_ATTEMPTS=10
      G_AUTH="$G_USER:$G_PASS"
      [ -z "$G_IN_FILE" ] && echo "$0 <input_file.ext> [output_file.ext]" && exit
      [ ! -r "$G_IN_FILE" ] && echo "! Local file inaccessible or not found: $G_IN_FILE" && exit
      [ ! -z "$G_OUT_FILE" ] && [ -e "$G_OUT_FILE" ] && echo "! Output file already exists: $G_OUT_FILE" && exit
      G_TMP=$(mktemp /tmp/gristle.XXXXXX);
      echo "* Attempting to send file: $G_IN_FILE"
      curl --basic -u "$G_AUTH" -F "file=@$G_IN_FILE" -sD- "https://api.gristle.com/parse/$G_PARSER_ID" 1>"$G_TMP" 2>&1
      G_ERROR=$(grep -i "^Gristle-Error:" "$G_TMP" 2>/dev/null | cut -d" " -f2-)
      [ ! -z "$G_ERROR" ] && echo "! Error: $G_ERROR" && rm -f "$G_TMP" && exit
      G_PARSE_ID=$(grep -i "^Gristle-Parse-Id:" "$G_TMP" 2>/dev/null | awk '{print $2}' | tr -d "[:space:]")
      [ -z "$G_PARSE_ID" ] && echo "! UNEXPECTED RESPONSE, PLEASE REPORT:" && cat "$G_TMP" && rm -f "$G_TMP" && exit
      G_PARSER_ID=$(grep -i "^Gristle-Parser-Id:" "$G_TMP" 2>/dev/null | awk '{print $2}' | tr -d "[:space:]")
      [ -z "$G_PARSER_ID" ] && echo "! UNEXPECTED RESPONSE, PLEASE REPORT:" && cat "$G_TMP" && rm -f "$G_TMP" && exit
      G_AUTH=$(grep -i "^Gristle-Token:" "$G_TMP" 2>/dev/null | awk '{print $2}' | tr -d "[:space:]")
      [ -z "$G_AUTH" ] && echo "! Error: No authorization token returned." && rm -f "$G_TMP" && exit
      rm -f "$G_TMP"
      
      echo "* File successfully sent and awaiting the output file for ID: $G_PARSE_ID"
      [ -z "$G_OUT_FILE" ] && echo -e "* No output file was specified, manual URL:\n\t$ curl https://api.gristle.com/parse/$G_PARSER_ID/$G_PARSE_ID?$G_AUTH" && exit
      sleep 10
      G_ERROR=""
      while true; do
        G_STATUS=$(curl -o/dev/null -sIw%{http_code} "https://$G_AUTH@api.gristle.com/parse/$G_PARSER_ID/$G_PARSE_ID")
        case $G_STATUS in
          304)
            echo "* Waiting ... ($G_ATTEMPTS)"
            if [ $G_ATTEMPTS -lt 1 ]; then
              G_ERROR="Unexpectedly long wait time processing file: $G_PARSE_ID"
            else
              G_ATTEMPTS=$((G_ATTEMPTS - 1))
              sleep 15 && continue
            fi
            ;;
          422) G_ERROR="No data could be extracted from file" ;;
          2*) G_ERROR="" ;;
          *) G_ERROR="HTTP Status: $G_STATUS (not a downloadable response)" ;;
        esac
        [ ! -z "$G_ERROR" ] && echo "! ERROR: $G_ERROR" && exit
        break
      done
      
      echo "* Attempting to download..."
      curl -o "$G_OUT_FILE" "https://$G_AUTH@api.gristle.com/parse/$G_PARSER_ID/$G_PARSE_ID"
           
      # GristlePush.ps1
      #
      # Push a file to Gristle for parsing via the "-PushFile" argument, a download url is shown upon success.
      #
      # USAGE:
      #   powershell -ExecutionPolicy Bypass -File "GristlePush.ps1" -PushFile "C:\Path\To\UploadFile.ext"
      
      param ( [string]$PushFile = "" )
      
      $G_USER = "~G_USER~"
      $G_PASS = "~G_PASS~"
      $G_PARSER_ID = "~G_PARSER_ID~" # ~G_PARSER_NAME~
      
      If ([string]::IsNullOrEmpty($PushFile)) {
        Write-Output "Please include -PushFile path-to-file"
        Break
      }
      If (![System.IO.File]::Exists($PushFile)) {
        Write-Output "Could not find file: $PushFile"
        Break
      }
      
      $url = "https://api.gristle.com/parse/$G_PARSER_ID"
      $boundary = [System.Guid]::NewGuid().ToString();
      $auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$G_USER`:$G_PASS"))
      $filename = (Get-Item $PushFile).Name
      $content = Get-Content -Raw -Path $PushFile
      $body = (
        "--$boundary",
        "Content-Disposition: form-data; name=file; filename=`"$filename`"",
        "Content-Type: application/octet-stream; charset=windows-1252",
        "Content-Transfer-Encoding: binary`r`n",
        $content,
        "--$boundary--`r`n"
      ) -join "`r`n"
      
      $response = Invoke-RestMethod -Headers @{Authorization="Basic $auth"} -Uri $url -Method Post `
        -ContentType "multipart/form-data; charset=windows-1252; boundary=`"$boundary`"" -Body $body
      
      If ([string]::IsNullOrEmpty($response)) { Write-Output "No response returned" }
      ElseIf (![string]::IsNullOrEmpty($response.error)) { Write-Output "Gristle returned error: $response.error" }
      ElseIf ([string]::IsNullOrEmpty($response.auth)) { Write-Output "No authorization token returned" }
      ElseIf ([string]::IsNullOrEmpty($response.id)) { Write-Output "No parse id returned" }
      Else { Write-Output "https://api.gristle.com/parse/$G_PARSER_ID/$($response.id)?$($response.auth)" }
      
      #!/usr/bin/env python3
      
      # USAGE:
      #   try: downloadUrl = gristlePush('uploadFile.ext')
      #   except Exception as e: print('ERROR:', e)
      
      import requests
      import time
      
      G_USER = '~G_USER~'
      G_PASS = '~G_PASS~'
      G_PARSER_ID = '~G_PARSER_ID~' # ~G_PARSER_NAME~
      G_ATTEMPTS = 10
      
      # Returns download url, if 'outFile' is specified it will poll until it can download the file.
      def gristlePush(inFile, outFile=None):
      
        # Push file to gristle for parsing.
        url = 'https://api.gristle.com/parse/{parser_id}'.format(parser_id=G_PARSER_ID);
        response = requests.post(url, files={'file':open(inFile,'rb')}, auth=(G_USER,G_PASS)).json()
        if not response: raise Exception('No JSON returned from API')
        if response['error']: raise Exception(response['error'])
        authToken = response['auth']
        parseId = response['id']
        url = 'https://api.gristle.com/parse/{parser_id}/{parse_id}?{auth_id}'.format(parser_id=G_PARSER_ID, parse_id=parseId, auth_id=authToken)
      
        # No output file specified, so return a url to download at a later time.
        if not outFile:
          return url
      
        # Poll API to see if file is ready. (within a reasonable amount of time)
        time.sleep(10)
        attempts = G_ATTEMPTS
        while attempts > 0:
          attempts -= 1
          response = requests.head(url)
          status = response.status_code
          if 304 == status:
            if attempts <= 0:
              raise Exception('Unexpectedly long wait time processing file: {parse_id}'.format(parse_id=parseId))
            time.sleep(15)
          elif 422 == status:
            raise Exception('No data could be extracted from file')
          elif 200 <= status <= 299:
            break
          else:
            raise Exception('HTTP Status: {status} (not a downloadable response)'.format(status=status))
      
        # Download file.
        response = requests.get(url)
        with open(outFile, 'wb') as file:
          file.write(response.content)
        return url
      
      <?php
      
      // USAGE:
      //   try { $downloadUrl = gristlePush("uploadFile.ext"); }
      //   catch(Exception $e) { print "Error: {$e->getMessage()}\n"; }
      
      define('G_USER', '~G_USER~');
      define('G_PASS', '~G_PASS~');
      define('G_PARSER_ID', '~G_PARSER_ID~'); // ~G_PARSER_NAME~
      define('G_ATTEMPTS', 10);
      
      // Returns download url, if $outFile is specified it will poll until it can download the file.
      function gristlePush($inFile, $outFile=null) {
        if(!is_readable($inFile))
          throw new Exception("Local file inaccessible or not found: $inFile");
        if(!empty($outFile) && file_exists($outFile))
          throw new Exception("Output file already exists: $outFile");
        if(!($ch=curl_init()))
          throw new Exception("curl_init() failed");
        curl_setopt_array($ch, [
          CURLOPT_URL => 'https://api.gristle.com/parse/'.G_PARSER_ID,
          CURLOPT_HTTPHEADER => ['Authorization: Basic '.base64_encode(G_USER.':'.G_PASS)],
          CURLOPT_RETURNTRANSFER => true,
          CURLOPT_POST => true,
          CURLOPT_POSTFIELDS => ['file' => @curl_file_create($inFile)]
        ]);
      
        $result = @json_decode(@curl_exec($ch),true);
        curl_close($ch);
      
        if(curl_errno($ch)) $error = curl_error($ch);
        else if(empty($result)) $error = "No result returned from";
        else if(!empty($result['error'])) $error = $result['error'];
        else if(!($authToken=@$result['auth'])) $error = "No authorization token returned";
        else if(!($parseId=@$result['id'])) $error = "No parse id returned";
        else $error = null;
        if($error)
          throw new Exception($error);
      
        $downloadUrl = "https://api.gristle.com/parse/".G_PARSER_ID."/$parseId?$authToken";
      
        // No output file specified, so return a url to download at a later time.
        if(empty($outFile))
          return($downloadUrl);
      
        // --------------------- OUTPUT/DOWNLOAD MODE ONLY FROM HERE ON ---------------------
        sleep(10);
      
        // Poll API to see if file is ready. (within a reasonable amount of time)
        $opts = [
          CURLOPT_CUSTOMREQUEST => 'HEAD',
          CURLOPT_URL => "https://api.gristle.com/parse/".G_PARSER_ID."/$parseId",
          CURLOPT_HTTPHEADER => ["Authorization: Bearer $authToken"],
        ];
        for($i=G_ATTEMPTS; $i >= 0; $i--) {
          if(!($ch=curl_init()))
            throw new Exception("curl_init() failed checking status");
          curl_setopt_array($ch, $opts);
          if(!@curl_exec($ch))
            throw new Exception("curl_exec() failed checking status");
          $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
          curl_close($ch);
      
          if(304 == $status) {
            if($i <= 1)
              throw new Exception("Unexpectedly long wait time processing file: $parseId");
            sleep(15);
          }
          else if(422 == $status)
            throw new Exception("No data could be extracted from file");
          else if(200 <= $status && 299 >= $status)
            break;
          else
            throw new Exception("HTTP Status: $status (not a downloadable response)");
        }
      
        // Download file.
        if(!($fp=fopen($outFile, 'w+')))
          throw new Exception("Could not open file to write to: $outFile");
        $opts[CURLOPT_CUSTOMREQUEST] = 'GET';
        $opts[CURLOPT_FILE] = $fp;
        if(!($ch=curl_init()))
          throw new Exception("curl_init() download failed");
        curl_setopt_array($ch, $opts);
        if(!@curl_exec($ch) || !($status=curl_getinfo($ch, CURLINFO_HTTP_CODE)))
          throw new Exception("Download failed");
        curl_close($ch);
        fclose($fp);
        if(200 > $status || 299 < $status)
          throw new Exception("Failed to download to output file: $outFile");
        return($downloadUrl);
      }
      
      ?>
      
      #!/usr/bin/env ruby
      
      # USAGE:
      #   downloadUrl = gristlePush "uploadFile.ext"
      
      require 'json'
      require 'net/http'
      
      $G_USER = '~G_USER~'
      $G_PASS = '~G_PASS~'
      $G_PARSER_ID = '~G_PARSER_ID~' # ~G_PARSER_NAME~
      $G_ATTEMPTS = 10
      
      # Returns download url, if 'outFile' is specified it will poll until it can download the file.
      def gristlePush(inFile, outFile=nil)
        begin
          downloadUrl = authToken = parseId = nil
      
          # Push file to gristle for parsing.
          File.open(inFile, 'rb') do |file|
            Net::HTTP.start('api.gristle.com', 443, use_ssl:true) do |https|
              request = Net::HTTP::Post.new "/parse/#{$G_PARSER_ID}"
              request.set_form [['file', file]], 'multipart/form-data'
              request.basic_auth $G_USER, $G_PASS
              response = JSON.parse https.request(request).body
              authToken = response['auth']
              parseId = response['id']
            end
          end
          downloadUrl = "https://api.gristle.com/parse/#{$G_PARSER_ID}/#{parseId}?#{authToken}"
      
          # No output file specified, so return a url to download at a later time.
          return downloadUrl if outFile.nil?
      
          # Poll API to see if file is ready. (within a reasonable amount of time)
          sleep 5
          attempts = $G_ATTEMPTS
          Net::HTTP.start('api.gristle.com', 443, use_ssl:true) do |https|
            request = Net::HTTP::Head.new "/parse/#{$G_PARSER_ID}/#{parseId}"
            request.basic_auth authToken, ''
            while attempts > 0
              attempts -= 1
              response = https.request request
              status =  response.code.to_i
              if 304 == status
                raise Exception.new "Unexpectedly long wait time processing file: #{parseId}" if attempts <= 0
                sleep 15
              elsif 422 == status
                raise Exception.new 'No data could be extracted from file'
              elsif status.between?(200,299)
                break
              else
                raise Exception.new "HTTP Status: #{status} (not a downloadable response)"
              end
            end
          end
      
          # Download file.
          File.open(outFile, 'wb') do |file|
            Net::HTTP.start('api.gristle.com', 443, use_ssl:true) do |https|
              request = Net::HTTP::Get.new "/parse/#{$G_PARSER_ID}/#{parseId}"
              request.basic_auth authToken, ''
              file.write https.request(request).body
            end
          end
      
        rescue Exception => e
          puts "ERROR: #{e.message}"
        ensure
          return downloadUrl
        end
      end
      
      // Gristle.java
      
      // USAGE:
      //   try { String downloadUrl = Gristle.Push("uploadFile.ext"); }
      //   catch (Exception e) { System.out.println("Error: " + e.getMessage()); }
      
      // No 3rd-party dependencies required.
      import java.io.File;
      import java.io.PrintWriter;
      import java.io.OutputStream;
      import java.io.OutputStreamWriter;
      import java.io.InputStreamReader;
      import java.io.FileInputStream;
      import java.nio.charset.StandardCharsets;
      import java.net.URL;
      import java.net.URLConnection;
      import java.net.HttpURLConnection;
      import java.util.Base64;
      
      // Static class container that is not intended to be instanced.
      public class Gristle {
      
        public static final String USER = "~G_USER~";
        public static final String PASS = "~G_PASS~";
        public static final String PARSER_ID = "~G_PARSER_ID~"; // ~G_PARSER_NAME~
      
        // Pushes a file to Gristle for parsing and returns a download url, to be used after the file has been processed.
        public static String Push(String inFile) throws Exception {
          String downloadUrl = null;
          try {
            String boundary = Long.toHexString(System.currentTimeMillis()), error, authToken, parseId;
            String authHeader = "Basic " + Base64.getEncoder().encodeToString((Gristle.USER+":"+Gristle.PASS).getBytes(StandardCharsets.UTF_8));
            File file = new File(inFile);
            URLConnection connection = new URL("https://api.gristle.com/parse/" + Gristle.PARSER_ID).openConnection();
            connection.setDoOutput(true);
            connection.setRequestProperty("Authorization", authHeader);
            connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
            OutputStream output = connection.getOutputStream();
            PrintWriter writer = new PrintWriter(new OutputStreamWriter(output, "UTF-8"));
            writer.append("--" + boundary + "\r\n");
            writer.append("Content-Disposition: form-data; name=file; filename=\"" + file.getName() + "\"\r\n");
            writer.append("Content-Type: " + URLConnection.guessContentTypeFromName(inFile) + "; charset=UTF-8\r\n");
            writer.append("Content-Transfer-Encoding: binary\r\n\r\n");
            writer.flush();
            FileInputStream fileStream = new FileInputStream(file);
            byte[] buffer = new byte[4096];
            int bytesRead = -1;
            while((bytesRead = fileStream.read(buffer)) > 0)
              output.write(buffer, 0, bytesRead);
            output.flush();
            fileStream.close();
            writer.append("\r\n--" + boundary + "--\r\n");
            writer.flush();
            writer.close();
            if((error=connection.getHeaderField("Gristle-Error")) != null)
              throw new Exception(error);
            else if((authToken=connection.getHeaderField("Gristle-Token")) == null)
              throw new Exception("No authorization token returned");
            else if((parseId=connection.getHeaderField("Gristle-Parse-Id")) == null)
              throw new Exception("No parse id returned");
            downloadUrl = "https://api.gristle.com/parse/" + Gristle.PARSER_ID + "/" + parseId + "?" + authToken;
          }
          catch(Exception e) { throw new Exception(e.getMessage()); }
          return downloadUrl;
        }
      }
      
      // Gristle.cs
      
      // USAGE:
      //   try { string downloadUrl = Gristle.Push("uploadFile.ext"); }
      //   catch (Exception e) { Debug.WriteLine("Error: "+ e.Message); }
      
      using System;
      using System.Text;
      using System.IO;
      using System.Net.Http;
      using System.Net.Http.Headers;
      
      namespace Gristle {
        internal class Gristle {
      
          public const string USER = "~G_USER~";
          public const string PASS = "~G_PASS~";
          public const string PARSER_ID = "~G_PARSER_ID~"; // ~G_PARSER_NAME~
      
          // Pushes a file to Gristle for parsing and returns a download url, to be used after the file has been processed.
          public static string Push(string inFile) {
            HttpClient client = new HttpClient();
            string auth = Convert.ToBase64String(Encoding.UTF8.GetBytes(Gristle.USER + ":" + Gristle.PASS));
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", auth);
            MultipartFormDataContent form = new MultipartFormDataContent();
            FileStream fileStream = new FileStream(inFile, FileMode.Open);
            form.Add(new StreamContent(fileStream), "file", Path.GetFileName(inFile));
            HttpResponseMessage response = client.PostAsync("https://api.gristle.com/parse/" + Gristle.PARSER_ID, form).Result;
            client.Dispose();
            fileStream.Close();
            IEnumerable<string> values;
            if(response.Headers.TryGetValues("Gristle-Error", out values))
              throw new Exception(values.First());
            if(!response.Headers.TryGetValues("Gristle-Token", out values))
              throw new Exception("No authorization token returned");
            string authToken = values.First();
            if(!response.Headers.TryGetValues("Gristle-Parse-Id", out values))
              throw new Exception("No parse id returned");
            string parseId = values.First();
            return "https://api.gristle.com/parse/" + Gristle.PARSER_ID + "/" + parseId + "?" + authToken;
          }
        }
      }
           
      // USAGE:
      //   Gristle.Push('uploadFile.ext',
      //     url => console.log('Download URL: ' + url),
      //     error => console.log('ERROR: ' + error)
      //   );
      
      const fs = require('fs');
      const FormData = require('form-data');
      const Gristle = {
      
        USER: '~G_USER~',
        PASS: '~G_PASS~',
        PARSER_ID: '~G_PARSER_ID~', // ~G_PARSER_NAME~
      
        Request: (formData, options) => new Promise((resolve, reject) => {
          const req = formData.submit(options, (err, res) => {
            if(err)
              return reject(new Error(err.message));
            if(res.statusCode < 200 || res.statusCode > 299)
              return reject(new Error(`HTTP status code ${res.statusCode}`));
            const body = [];
            res.on('data', chunk => body.push(chunk));
            res.on('end', () => resolve(Buffer.concat(body).toString()));
          })
        }),
        Push: (file, successCB, errorCB) => {
          if(!fs.existsSync(file))
            return(errorCB('File not found: ' + file));
          const formData = new FormData(), options = {
            host: 'api.gristle.com',
            path: '/parse/' + Gristle.PARSER_ID,
            method: 'POST',
            protocol: 'https:',
            headers: { Authorization: 'Basic ' + Buffer.from(Gristle.USER + ':' + Gristle.PASS).toString('base64') }
          };
          formData.append('file', fs.createReadStream(file))
          Gristle.Request(formData, options)
            .then(json => {
              const response = JSON.parse(json);
              if(response && response.id && response.auth)
                successCB('https://api.gristle.com/parse/' + Gristle.PARSER_ID + '/' + response.id + '?' + response.auth);
              else
                errorCB('Response from Gristle did not contain a parseId or authentication token');
            })
            .catch(e => errorCB(e.message));
        }
      }
           
      // USAGE:
      //   url, err := gristlePush("uploadFile.ext")
      //   if err != nil {
      //     fmt.Println("ERROR:", err)
      //   } else {
      //     fmt.Println("Download URL:", url)
      //   }
      
      import (
        "errors"
        "bytes"
        "io"
        "os"
        "fmt"
        "net/http"
        "mime/multipart"
      )
      
      var G_USER string = "~G_USER~"
      var G_PASS string = "~G_PASS~"
      var G_PARSER_ID string = "~G_PARSER_ID~" // ~G_PARSER_NAME~
      
      // Pushes a file to Gristle for parsing and returns a download url, to be used after the file has been processed.
      func gristlePush(inFile string) (string, error) {
        body := new(bytes.Buffer)
        mp := multipart.NewWriter(body)
        file, err := os.Open(inFile)
        if err != nil { return "", err }
        part, err := mp.CreateFormFile("file", inFile)
        if err != nil { return "", err }
        io.Copy(part, file)
        file.Close()
        mp.Close()
        url := fmt.Sprintf("https://%s:%s@api.gristle.com/parse/%s", G_USER, G_PASS, G_PARSER_ID)
        response, err := http.Post(url, mp.FormDataContentType(), body)
        if err != nil { return "", err }
        authToken := response.Header.Get("Gristle-Token")
        if authToken == "" { return "", errors.New("No authentication token returned: " + response.Status) }
        parseId := response.Header.Get("Gristle-Parse-Id")
        if parseId == "" { return "", errors.New("No parse id returned: " + response.Status) }
        downloadUrl := fmt.Sprintf("https://api.gristle.com/parse/%s/%s?%s", G_PARSER_ID, parseId, authToken)
        return downloadUrl, nil
      }
      
      #!/usr/bin/env lua
      
      -- USAGE:
      --   downloadUrl = gristlePush("uploadFile.ext")
      
      local https = require('ssl.https')
      local mime = require('mime')
      
      G_USER = '~G_USER~'
      G_PASS = '~G_PASS~'
      G_PARSER_ID = '~G_PARSER_ID~' -- ~G_PARSER_NAME~
      
      local function gristlePush(inFile)
        local boundary = tostring(os.time())
        local file = assert(io.open(inFile, "rb"), 'Could not access file for uploading: ' .. inFile)
        local request = "--" .. boundary .. "\r\n"
          .. "Content-Disposition: form-data; name=file; filename=\"" .. inFile .. "\"\r\n"
          .. "Content-Type: application/octet-stream\r\n"
          .. "Content-Transfer-Encoding: binary\r\n\r\n"
          .. file:read("*all")
          .. "--" .. boundary .. "--\r\n\r\n"
        file:close()
        local r, code, headers = https.request{
          url = "https://api.gristle.com/parse/" .. G_PARSER_ID,
          method = "POST",
          headers = {
            ['Authorization'] = "Basic " .. mime.b64(G_USER .. ":" .. G_PASS),
            ['Content-Type'] = 'multipart/form-data; boundary=' .. boundary,
            ['Content-Length'] = #request
          },
          source = ltn12.source.string(request)
        }
        assert(headers, 'No headers returned')
        assert(headers['gristle-error'] == nil, headers['gristle-error'])
        assert(200 <= code and 299 >= code, 'Unexpected HTTP status: ' .. code)
        assert(headers['gristle-token'], 'No authorization token returned')
        assert(headers['gristle-parse-id'], 'No parse id returned')
        return "https://api.gristle.com/parse/" .. G_PARSER_ID .. "/" .. headers['gristle-parse-id'] .. "?" .. headers['gristle-token']
      end