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.
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.
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.
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.
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.
The Terms, Conditions, and Privacy Statement are displayed and must be approved during the account creation process. However, you can view them at any time in the app's side panel. For any other questions or concerns, you can create a ticket under your account or contact us at:
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.
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.
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.
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.
#!/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