├── .gitignore ├── README.md ├── client-examples.md ├── codelets.md ├── codelets ├── form.md ├── github.md ├── one-way-book-sync.md └── slackbot.md ├── images ├── api-base-url.png ├── api-button.png ├── api-explorer.png ├── codelet-form-sheet.png ├── codelet-form-tacit.png ├── codelets-tab.png ├── copy-book-menu-item.png ├── generate-api-key-button.png ├── github-add-webhook.png ├── github-config-webhook.png ├── github-example-book.png ├── github-settings.png ├── manage-api-menu-item.png ├── manage-api-modal.png ├── new-api-key.png ├── slackbot-final.png └── slackbot-hello.png ├── metadata.md ├── quick-start.md ├── reference.md └── snapshot-format.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Fieldbook API docs 2 | ================== 3 | 4 | The Fieldbook API lets you read and write records in your books. Each book has its own API, based on the structure of that book. 5 | 6 | Use cases 7 | --------- 8 | 9 | Use the Fieldbook API to... 10 | 11 | * **Store content or configuration for your app:** Create a Fieldbook to hold app configuration or content, in lieu of a custom admin interface. Then read it through the Fieldbook API from your production app. 12 | 13 | * **Prototype and demo:** Use Fieldbook as a quick back end for prototyping workflows and client apps. 14 | 15 | * **Glue together systems and processes:** Dump structured output from one program, review and revise the results by hand, then feed it into the next stage of the process. 16 | 17 | Documentation 18 | ------------- 19 | 20 | * [Quick start](quick-start.md) 21 | * [Client examples](client-examples.md) in JavaScript, Ruby, Python, etc. 22 | * [Reference](reference.md) 23 | * [Metadata API](metadata.md) 24 | * [Codelets](codelets.md) 25 | * [Snapshot format (JSON)](snapshot-format.md) 26 | 27 | Official Clients 28 | ---------------- 29 | 30 | * Node: [fieldbook-client](https://github.com/fieldbook/fieldbook-client) 31 | 32 | Community 33 | --------- 34 | 35 | Unofficial client projects: 36 | 37 | * [node-fieldbook](https://www.npmjs.com/package/node-fieldbook), a Node client contributed by [@connormckelvey](https://github.com/connormckelvey) 38 | * [fieldbook-sharp](https://github.com/dvdsgl/fieldbook-sharp), a C#/.NET client from [@dvdsgl](https://github.com/dvdsgl) 39 | * [fieldbook-apy](https://github.com/cgm616/fieldbook-apy), a very basic Python 3 client from [@cgm616](https://github.com/cgm616) 40 | * [fieldbook-py](https://github.com/mattstibbs/fieldbook_py), another basic Python 3 client available on pypi from [@mattstibbs](https://github.com/mattstibbs) 41 | * [node-fieldbook-promise](https://github.com/thetmkay/node-fieldbook-promise), a promise-based Node client from [@thetmkay](https://github.com/thetmkay) 42 | * [Fieldbook-SwiftSDK](https://github.com/ChrisMash/Fieldbook-SwiftSDK), a Swift SDK for iOS from [@chrismash](https://github.com/ChrisMash/) 43 | * [phieldbook](https://github.com/Joeventures/phieldbook), a PHP client from [@Joeventures](https://github.com/Joeventures) 44 | * [django-fieldbook](https://github.com/bsab/django-fieldbook), a Django client from [@bsab](https://github.com/bsab) based on fieldbook-py 45 | * [go-fieldbook](https://github.com/trexart/go-fieldbook), a Go client from [@trexart](https://github.com/trexart) 46 | * [AlanKitap](https://bitbucket.org/coelmay/alankitap), a PHP client from [Coel May](https://bitbucket.org/coelmay/) 47 | 48 | Other projects: 49 | 50 | * [highlight-fieldbook-extension](https://github.com/thetmkay/highlight-fieldbook-extension), a Chrome extension for saving links and web clippings to a Fieldbook, by [@thetmkay](https://github.com/thetmkay) 51 | * [fieldbook-cli](https://github.com/fiatjaf/fieldbook-cli), a command-line interface for your Fieldbook data from [@fiatjaf](https://github.com/fiatjaf) 52 | * [fieldbook-download](https://github.com/hay/fieldbook-download), a helper that downloads all sheets and images in a Fieldbook, by [@hay](https://github.com/hay) 53 | * [fieldbookforgravityforms](https://github.com/Joeventures/fieldbookforgravityforms), an add-on for the Gravity Forms WordPress plugin that sends form submissions to Fieldbook sheets, by [@Joeventures](https://github.com/Joeventures) 54 | -------------------------------------------------------------------------------- /client-examples.md: -------------------------------------------------------------------------------- 1 | Fieldbook API Client Examples 2 | ============================= 3 | 4 | Here are examples of how to access the Fieldbook API using different clients. 5 | 6 | In all the example below, substitute: 7 | 8 | * your API key for `$KEY` 9 | * its secret for `$SECRET` 10 | * a sheet URL for the example one used, `https://api.fieldbook.com/v1/56789abc0000000000000001/tasks` 11 | 12 | curl 13 | ---- 14 | 15 | List records: 16 | 17 | ```bash 18 | $ curl -H "Accept: application/json" -u $KEY:$SECRET https://api.fieldbook.com/v1/56789abc0000000000000001/tasks 19 | ``` 20 | 21 | Create a record: 22 | 23 | ```bash 24 | $ curl -H "Accept: application/json" -H "Content-Type: application/json" -u $KEY:$SECRET \ 25 | https://api.fieldbook.com/v1/56789abc0000000000000001/tasks \ 26 | -d '{"name":"New task","owner":"Alice","priority":1}' 27 | ``` 28 | 29 | jQuery 30 | ------ 31 | 32 | ```js 33 | // List records: 34 | 35 | $.ajax({ 36 | url: 'https://api.fieldbook.com/v1/56789abc0000000000000001/tasks', 37 | headers: { 38 | 'Accept': 'application/json', 39 | 'Authorization': 'Basic ' + btoa('$KEY:$SECRET') 40 | }, 41 | success: function (data) { 42 | console.log(data.length + ' items'); 43 | }, 44 | error: function (error) { 45 | console.log('error', error); 46 | } 47 | }); 48 | 49 | // Create a record: 50 | 51 | var record = { 52 | name: "New task", 53 | owner: "Alice", 54 | priority: 1 55 | }; 56 | 57 | $.ajax({ 58 | method: 'POST', 59 | url: 'https://api.fieldbook.com/v1/56789abc0000000000000001/tasks', 60 | headers: { 61 | 'Accept': 'application/json', 62 | 'Content-Type': 'application/json', 63 | 'Authorization': 'Basic ' + btoa('$KEY:$SECRET') 64 | }, 65 | data: JSON.stringify(record), 66 | success: function (data) { 67 | console.log('created record', data); 68 | }, 69 | error: function (error) { 70 | console.log('error', error); 71 | } 72 | }); 73 | ``` 74 | 75 | Note: This uses `btoa` to do base-64 encoding for the authorization header. This will work in [browsers other than IE 8 and 9](http://caniuse.com/#search=btoa); for older versions of IE you can [use a polyfill](https://github.com/davidchambers/Base64.js). 76 | 77 | WARNING: If you do this in a web page, anyone who loads the web page will have your API key and secret and will be able to access the entire book. 78 | 79 | Node 80 | ---- 81 | 82 | Using [the npm `request` module](https://github.com/request/request): 83 | 84 | ```js 85 | var request = require('request'); 86 | 87 | // List records 88 | 89 | var options = { 90 | url: 'https://api.fieldbook.com/v1/56789abc0000000000000001/tasks', 91 | json: true, 92 | auth: { 93 | username: '$KEY', 94 | password: '$SECRET' 95 | } 96 | }; 97 | 98 | request(options, function (error, response, body) { 99 | if (error) { 100 | console.log('error making request', error); 101 | } else if (response.statusCode >= 400) { 102 | console.log('HTTP error response', response.statusCode, body.message); 103 | } else { 104 | console.log(body.length + ' items'); 105 | } 106 | }); 107 | 108 | // Create a record 109 | 110 | var record = { 111 | name: "New task", 112 | owner: "Alice", 113 | priority: 1 114 | }; 115 | 116 | options = { 117 | url: 'https://api.fieldbook.com/v1/56789abc0000000000000001/tasks', 118 | json: true, 119 | body: record, 120 | auth: { 121 | username: '$KEY', 122 | password: '$SECRET' 123 | } 124 | }; 125 | 126 | request.post(options, function (error, response, body) { 127 | if (error) { 128 | console.log('error making request', error); 129 | } else if (response.statusCode >= 400) { 130 | console.log('HTTP error response', response.statusCode, body.message); 131 | } else { 132 | console.log('created record', body); 133 | } 134 | }); 135 | ``` 136 | 137 | Ruby 138 | ---- 139 | 140 | Using Net::HTTP (as per [this cheat sheet](http://www.rubyinside.com/nethttp-cheat-sheet-2940.html)): 141 | 142 | ```rb 143 | require "net/http" 144 | require "uri" 145 | require "json" 146 | 147 | uri = URI.parse("https://api.fieldbook.com/v1/56789abc0000000000000001/tasks") 148 | 149 | http = Net::HTTP.new(uri.host, uri.port) 150 | request = Net::HTTP::Get.new(uri.request_uri) 151 | request.basic_auth("$KEY", "$SECRET") 152 | http.use_ssl = true 153 | response = http.request(request) 154 | 155 | items = JSON.parse(response.body) 156 | puts "#{items.length} items" 157 | ``` 158 | 159 | Python 160 | ------ 161 | 162 | Using [the Requests library](http://docs.python-requests.org/en/latest/): 163 | 164 | ```python 165 | import requests 166 | request = requests.get('https://api.fieldbook.com/v1/56789abc0000000000000001/tasks', 167 | auth=("$KEY", "$SECRET")) 168 | print len(request.json()), "items" 169 | ``` 170 | 171 | PHP 172 | --- 173 | 174 | Using [PHP cURL](http://php.net/manual/en/ref.curl.php): 175 | 176 | ```php 177 | $ch = curl_init(); 178 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 179 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 180 | curl_setopt($ch, CURLOPT_USERPWD, $KEY . ':' . $SECRET); 181 | curl_setopt($ch, CURLOPT_URL, 'https://api.fieldbook.com/v1/56789abc0000000000000001/tasks'); 182 | $result = curl_exec($ch); 183 | curl_close($ch); 184 | 185 | $obj = json_decode($result); 186 | echo count($obj) . ' items'; 187 | ``` 188 | 189 | Google Apps Script 190 | ------------------ 191 | 192 | Using [UrlFetchApp Class](https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app) 193 | 194 | ```js 195 | UrlFetchApp.fetch('https://api.fieldbook.com/v1/56789abc0000000000000001/tasks', { 196 | method: 'post', 197 | headers: { 198 | "Accept": "application/json", 199 | "Authorization": "Basic " + Utilities.base64Encode(key + ":" + secret) 200 | }, 201 | payload: JSON.stringify({ 202 | task_name_or_identifier: "task1" 203 | }) 204 | }); 205 | ``` 206 | -------------------------------------------------------------------------------- /codelets.md: -------------------------------------------------------------------------------- 1 | Fieldbook Codelets 2 | ================== 3 | 4 | What are Codelets? 5 | ------------------ 6 | 7 | Fieldbook lets you extend the API of any book with little snippets of code – 8 | *codelets* – that define custom endpoints. With Codelets, you can do in a few 9 | minutes and a couple dozen lines of code what would previously have required an 10 | entire API server hosted separately. 11 | 12 | ### Examples 13 | 14 | See these tutorial examples for how to use Codelets to: 15 | 16 | * [Create a custom Slackbot for your Fieldbook that responds to slash commands](codelets/slackbot.md) 17 | * [Keep a book in sync with GitHub pull requests by handling webhook callbacks](codelets/github.md) 18 | * [Add a custom form-submission endpoint to your Fieldbook](codelets/form.md) 19 | 20 | Getting started 21 | --------------- 22 | 23 | To access codelets on your book, open the API modal: 24 | 25 | ![api-button](images/api-button.png) 26 | 27 | Then open the Codelets tab: 28 | 29 | ![codelets-tab](images/codelets-tab.png) 30 | 31 | From this pane, you can edit the code for your custom endpoint. Using curl or a 32 | browser, you can fire a request against the given URL to run the codelet. 33 | 34 | The code should define an `exports.endpoint` function that takes a request, a 35 | response, and optionally a done callback: 36 | 37 | ``` 38 | exports.endpoint = function (request, response, done) { 39 | // codelet body here 40 | } 41 | ``` 42 | 43 | Codelet requests 44 | ---------------- 45 | 46 | Your codelet URL will respond to any of these HTTP methods: GET, POST, PUT, 47 | PATCH or DELETE. The request object passed to the function will have: 48 | 49 | * `request.method` 50 | * `request.headers` 51 | * `request.body` 52 | * `request.query` 53 | * `request.params` (a combination of body and query, with query taking precedence) 54 | 55 | Requests will accept JSON body parameters if the Content-Type of the request is 56 | `application/json`, and similarly will accept form parameters of the 57 | Content-Type is `application/x-www-form-urlencoded`. 58 | 59 | Its easy to inpect what is available on the request object, just return it from 60 | a codelet like so: 61 | 62 | ```javascript 63 | exports.endpoint = function (request) { 64 | return request; 65 | } 66 | ``` 67 | 68 | And then request it with some parameters: 69 | 70 | ``` 71 | $ curl "$CODELET_URL?foo=bar" -d '{"zip":"hello"}' -H 'Content-Type: application/json' 72 | ``` 73 | 74 | Example response: 75 | 76 | ``` 77 | { 78 | "headers": { 79 | "host": "fieldbook.com", 80 | "user-agent": "curl/7.43.0", 81 | "accept": "*/*", 82 | "content-type": "application/json", 83 | "x-request-id": "92cf3346-f407-4d2e-a1be-35f83af9d532", 84 | "x-forwarded-for": "XXX.XXX.XXX.XXX", 85 | "x-forwarded-proto": "https", 86 | "x-forwarded-port": "443", 87 | "x-request-start": "1455144616294", 88 | "content-length": "16" 89 | }, 90 | "body": { 91 | "zip": "hello" 92 | }, 93 | "query": { 94 | "foo": "bar" 95 | }, 96 | "params": { 97 | "zip": "hello", 98 | "foo": "bar" 99 | }, 100 | "method": "POST" 101 | } 102 | ``` 103 | 104 | Codelet responses 105 | ----------------- 106 | 107 | For convenience, there are multiple ways to generate a response: 108 | 109 | ### Return an object 110 | 111 | A JSON object or string will be directly translated into a response body: 112 | 113 | ```javascript 114 | exports.endpoint = function (req) { 115 | return {hello: 1}; 116 | } 117 | ``` 118 | 119 | ### Return a promise 120 | 121 | You can also return a promise for a response object: 122 | 123 | ```javascript 124 | var Q = require('q'); 125 | exports.endpoint = function (req) { 126 | return Q.delay(10).then(function () { 127 | return {hello: 1}; 128 | }) 129 | } 130 | ``` 131 | 132 | ### Invoke the done callback 133 | 134 | Invoke the callback as `done(error, result)`. Pass null for error if there is 135 | none: 136 | 137 | ```javascript 138 | exports.endpoint = function (req, res, done) { 139 | setTimeout(function () { 140 | done(null, {hello: 1}) 141 | }, 500); 142 | } 143 | ``` 144 | 145 | ### Use the response object 146 | 147 | Directly invoke the response object for more control over response headers and 148 | such: 149 | 150 | ```javascript 151 | exports.endpoint = function (req, res) { 152 | res.type('text/plain'); 153 | res.send('Hello World'); 154 | } 155 | ``` 156 | 157 | Methods on the response object: 158 | 159 | #### res.send(STRING or OBJECT) 160 | 161 | Sends a response. If no Content-Type header is set, the type will be determined 162 | by the argument. A string will be sent as `text/plain`; an object will be 163 | stringified to JSON and sent as `application/json`. 164 | 165 | If a Content-Type has already been set, the argument will be handled 166 | accordingly. For instance, if `text/plain` has been set, and an object is passed 167 | to send(), then `object.toString()` will be invoked to create a text response. 168 | 169 | When called with no arguments, will send the prepared body (by the write() 170 | method). 171 | 172 | Calling this method ends the request and further calls to this or any other 173 | response method will result in an error. 174 | 175 | #### res.write(STRING) 176 | 177 | Call to incrementally append data to the body of the response. 178 | 179 | If you ever call write or send on the response object, you may still return a 180 | promise from your endpoint, but the result of the promise will be ignored. If 181 | you call write and also use the done callback to return a result, an error will 182 | be thrown. 183 | 184 | #### res.status(CODE) 185 | 186 | Sets the HTTP status code of the response. 187 | 188 | #### res.setHeader(NAME, VALUE) 189 | 190 | Set a header value (however, you may not set cookies; see below). 191 | 192 | #### res.type(CONTENT_TYPE) 193 | 194 | Convenience method for setting the Content-Type header. 195 | 196 | #### res.location(URL) 197 | 198 | Convenience method for setting the Location header. 199 | 200 | #### res.redirect(CODE, URL) or redirect(URL) 201 | 202 | Shorthand for setting the status code and URL for a redirect. CODE defaults to 203 | 302 if not passed. 204 | 205 | Accessing your Fieldbook data 206 | ----------------------------- 207 | 208 | The pre-initialized `client` object provides access to the book the codelet is 209 | on. It's an instance of the 210 | [fieldbook-client](https://github.com/fieldbook/fieldbook-client) Node module. 211 | Here is an example using this client to return all names from the “People” sheet 212 | of a book: 213 | 214 | ```javascript 215 | exports.endpoint = function (request) { 216 | return client.list('people').then(function (records) { 217 | return records.map(function (record) { 218 | return record.name; 219 | } 220 | }; 221 | } 222 | ``` 223 | 224 | ES6 225 | --- 226 | 227 | Codelets are run on Node 5.5.0 with the `--harmony` flag. This means you can use 228 | a number of great ES6 features, like fat arrow syntax and generators. Here the 229 | same example rewritten to use `yield` and fat arrows: 230 | 231 | ```javascript 232 | var Q = require('q'); 233 | exports.endpoint = Q.async(function * (request) { 234 | var people = yield client.list('people'); 235 | return people.map(p => p.name); 236 | }) 237 | ``` 238 | 239 | Restrictions 240 | ------------ 241 | 242 | * A codelet should take no more than a few seconds to run. Longer codelets may 243 | be terminated in the middle of their execution. Timeout happens around 50 244 | seconds. 245 | 246 | * You may not set cookies, and trying to will result in an error when your code 247 | is run. 248 | 249 | Available modules 250 | ----------------- 251 | 252 | You can `require()` Node modules in your codelets. The following modules are 253 | currently supported: 254 | 255 | * amazon-product-api (0.3.8) 256 | * async (1.5.2) 257 | * aws-sdk (2.2.33) 258 | * bcrypt (0.8.5) 259 | * bitly (4.1.1) 260 | * bluebird (3.2.1) 261 | * body-parser (1.15.0) 262 | * bunyan (1.6.0) 263 | * chalk (1.1.1) 264 | * cheerio (0.20.0) 265 | * clone (1.0.2) 266 | * co (4.6.0) 267 | * colors (1.1.2) 268 | * connect (3.4.1) 269 | * cors (2.7.1) 270 | * cradle (0.7.1) 271 | * dropbox (0.10.3) 272 | * ebay-api (1.12.0) 273 | * elasticsearch (10.1.3) 274 | * fb (1.0.2) 275 | * fieldbook-client (1.0.4) 276 | * firebase (2.4.0) 277 | * flickrapi (0.3.36) 278 | * formidable (1.0.17) 279 | * github (0.2.4) 280 | * glob (7.0.0) 281 | * googleapis (2.1.7) 282 | * handlebars (4.0.5) 283 | * heroku (0.1.3) 284 | * hoek (3.0.4) 285 | * instagram-node (0.5.8) 286 | * intrusive (1.0.1) 287 | * irc (0.4.1) 288 | * joi (8.0.1) 289 | * jsdom (8.0.2) 290 | * jshint (2.9.1) 291 | * json-socket (0.1.2) 292 | * jsonwebtoken (5.5.4) 293 | * koa (1.1.2) 294 | * lazy (1.0.11) 295 | * lodash (4.1.0) 296 | * lodash.assign (4.0.2) 297 | * lru-cache (4.0.0) 298 | * mandrill-api (1.0.45) 299 | * marked (0.3.5) 300 | * merge (1.2.0) 301 | * mime (1.3.4) 302 | * moment (2.11.1) 303 | * mongodb (2.1.7) 304 | * mongoose (4.4.3) 305 | * natural (0.2.1) 306 | * node-fieldbook (1.0.7) 307 | * node-foursquare (0.3.0) 308 | * node-linkedin (0.5.3) 309 | * node-uuid (1.4.7) 310 | * node-wikipedia (0.0.2) 311 | * node-xmpp-client (3.0.0) 312 | * nodemailer (2.1.0) 313 | * numeric (1.2.6) 314 | * once (1.3.3) 315 | * papaparse (4.1.2) 316 | * passport (0.3.2) 317 | * pg (4.4.5) 318 | * pinterest-api (1.1.4) 319 | * q (1.4.1) 320 | * ramda (0.19.1) 321 | * redis (2.4.2) 322 | * redux (3.3.1) 323 | * request (2.69.0) 324 | * requestify (0.1.17) 325 | * restify (4.0.4) 326 | * science (1.9.3) 327 | * sequelize (3.19.2) 328 | * shortid (2.2.4) 329 | * slack (5.2.2) 330 | * split (1.0.0) 331 | * spotify-web-api-node (2.2.0) 332 | * stream-buffers (3.0.0) 333 | * stripe (4.3.0) 334 | * superagent (1.7.2) 335 | * syntax-error (1.1.5) 336 | * through (2.3.8) 337 | * through2 (2.0.1) 338 | * traverse (0.6.6) 339 | * tumblr (0.4.1) 340 | * twilio (2.9.0) 341 | * twitter (1.2.5) 342 | * underscore (1.8.3) 343 | * underscore.string (3.2.3) 344 | * us-census-api (0.0.5) 345 | * validator (4.8.0) 346 | * vimeo (1.1.4) 347 | * winston (2.1.1) 348 | * wordpress (1.1.2) 349 | * wundergroundnode (0.9.0) 350 | * xlsx (0.8.0) 351 | * xml2js (0.4.16) 352 | * yahoo-finance (0.2.12) 353 | * yargs (4.1.0) 354 | * yelp (1.0.1) 355 | 356 | Is your favorite module missing? Let us know using the “Message us” button in 357 | the app. 358 | -------------------------------------------------------------------------------- /codelets/form.md: -------------------------------------------------------------------------------- 1 | Easy-peasy record creation via Codelets 2 | ======================================= 3 | 4 | This [Codelet](../codelets.md) example will show you how to create a 5 | super-simple HTML form to create new rows in your Fieldbook database. 6 | 7 | We're going to start with a simple sheet that just tracks people who have 8 | seen this example. Check out [this 9 | book](https://fieldbook.com/books/56cccbd72ba55103004f278d). It just has 10 | Name and Country. Feel free to copy that book and play with it yourself! 11 | 12 | Here is a screenshot: 13 | 14 | ![people-sheet](../images/codelet-form-sheet.png) 15 | 16 | We are going to create a small codelet that, when retrieved, renders an HTML form, 17 | and on POST, creates a record. 18 | 19 | Code 20 | ---- 21 | 22 | First, let's create the endpoint function that will dispatch based on the HTTP 23 | method (is this request getting the form to display, or is it submitting data?) 24 | 25 | ```javascript 26 | exports.endpoint = function (req, res) { 27 | if (req.method === 'GET') { 28 | return renderForm(req, res); 29 | } else if (req.method === 'POST') { 30 | return postForm(req, res); 31 | } else { 32 | throw new Error('Bad method: ' + req.method); 33 | } 34 | } 35 | ``` 36 | 37 | Then we need to render the form, which just requires setting the 38 | Content-Type and returning a string (see the 39 | [codelets response documentation](../codelets.md#codelet-responses) for more 40 | info). 41 | 42 | ```javascript 43 | var renderForm = function (req, res) { 44 | res.type('text/html') 45 | return ` 46 | 47 | 48 |
49 | Name: 50 | Country: 51 | 52 |
53 | 54 | `; 55 | } 56 | ``` 57 | 58 | Here we are using the cool little [Tacit CSS](//yegor256.github.io/tacit/) 59 | library, and also ES6's [template strings](//developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals). 60 | 61 | And finally, we need to create the record from the resulting post: 62 | 63 | ```javascript 64 | var Q = require('q'); 65 | var postForm = Q.async(function * (req, res) { 66 | yield client.create('people', req.params); 67 | return 'Thanks!' 68 | }) 69 | ``` 70 | 71 | The `client` object is a pre-initialized JavaScript object for talking to 72 | Fieldbook's REST API. It is an instance of the 73 | [fieldbook-client](//github.com/fieldbook/fieldbook-client) module. We are 74 | also using 75 | [generators](//developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*) 76 | from ES6, as well as Q's 77 | [async](//github.com/kriskowal/q/wiki/API-Reference#qasyncgeneratorfunction) 78 | tool for creating promised functions from generators. 79 | 80 | And that's it. Three simple functions, and you can have a form submitting 81 | data to your Fieldbook. 82 | 83 | If you go to the URL for your codelet you should see a nice form: 84 | 85 | ![codelet-form-tacit](../images/codelet-form-tacit.png) 86 | 87 | Extensions 88 | ---------- 89 | 90 | There are a lot of additions that could be added to this tiny codelet: 91 | 92 | * We don't do any validation of country; we could 93 | force the form to validate the Country name out of a list. 94 | 95 | * We could also consult external services using `requestify`. 96 | 97 | * If we had multiple linked sheets, we could use the data from the POST 98 | to set up records in multiple sheets and link them together. 99 | 100 | All the code 101 | ------------ 102 | 103 | Here is all the code in one place suitable for pasting into your own book. 104 | 105 | ```javascript 106 | var Q = require('q'); 107 | exports.endpoint = function (req, res) { 108 | if (req.method === 'GET') { 109 | return renderForm(req, res); 110 | } else if (req.method === 'POST') { 111 | return postForm(req, res); 112 | } else { 113 | throw new Error('Bad method: ' + req.method); 114 | } 115 | } 116 | 117 | var renderForm = function (req, res) { 118 | res.type('text/html') 119 | return ` 120 | 121 | 122 |
123 | Name: 124 | Country: 125 | 126 |
127 | 128 | `; 129 | } 130 | 131 | var postForm = Q.async(function * (req, res) { 132 | yield client.create('people', req.params); 133 | return 'Thanks!' 134 | }) 135 | ``` 136 | -------------------------------------------------------------------------------- /codelets/github.md: -------------------------------------------------------------------------------- 1 | # Using Fieldbook Codelets to handle webhooks from GitHub 2 | 3 | At Fieldbook, we dogfood our own app for task tracking. All of our stories go 4 | into a book that looks [something like 5 | this](https://fieldbook.com/books/56c3aa4d1faa5a030071abf8): 6 | 7 | ![Stories book](../images/github-example-book.png) 8 | 9 | For a long time, we were fully manual with updating that "Stories" sheet, 10 | and the pull request process was pretty cumbersome, involving copying and 11 | pasting links between GitHub and Fieldbook. 12 | 13 | It's easy to forget these steps, and as a result, our Stories sheet often ended 14 | up out of sync — missing PR links, statuses out of date, etc. 15 | 16 | This irked me. Why are engineers manually executing a rote process? 17 | 18 | # The Old Way: A tiny Heroku app 19 | 20 | GitHub has a fantastic set of webhook triggers, including "pull request changed 21 | state". So I initially solved this problem by creating a small Heroku app. 22 | 23 | That worked pretty well. I set up a little [Express](http://expressjs.com/) 24 | server that could field the requests from GitHub, parse out the record ID and 25 | PR status, and update the record through our API. With this in place, we simply 26 | had to paste a Fieldbook link to the original story into a GitHub pull request, 27 | and Fieldbook would automatically be updated with a link back to the PR, and the 28 | status. 29 | 30 | But that solution involved a lot of boilerplate. I had to: 31 | 32 | 1. Set up a new project 33 | 2. Install node modules 34 | 3. Set up a bunch of Express boilerplate (routing, middleware, etc.) 35 | 4. Add an API key to the book 36 | 5. Configure the Fieldbook client in my app 37 | 6. Write the code that actually does something 38 | 7. Deploy to Heroku 39 | 8. Set up the webhook on GitHub 40 | 41 | Ugh. All of that, and the only part that's actually interesting is step 6. 42 | 43 | # The New Way: Codelets! 44 | 45 | We recently introduced [codelets](../codelets.md), which let you instantly 46 | create a one-off webserver with a single endpoint. As we'll see, we can 47 | eliminate all of those steps besides 6 and 8. 48 | 49 | # Creating a codelet 50 | 51 | We'll [add a codelet](../codelets.md#getting-started), clear out the default 52 | example and type: 53 | 54 | ```js 55 | exports.endpoint = function (request, response) { 56 | return 'Oh, hello there!'; 57 | } 58 | ``` 59 | 60 | The `exports.endpoint` function is our request handler. If you publish now, and 61 | copy the url into a new tab, you'll see the response "Oh, hello there!" 62 | 63 | In other words, steps 1-5 and 7 from above have been eliminated with a couple 64 | of clicks. 65 | 66 | ## Getting the content of the request 67 | 68 | Codelets automatically parse the request body and give you an object on 69 | `request.body`. Let's grab that and pull some stuff off of it: 70 | 71 | ```js 72 | exports.endpoint = function (request, response) { 73 | var data = request.body; 74 | 75 | if (!data.action) return 'Nothing to do; not an action'; 76 | var pr = data.pull_request; 77 | 78 | /* Do something with the PR data */ 79 | } 80 | ``` 81 | 82 | When we first hook up the webhook, GitHub is going to immediately send it a 83 | request to make sure it works. They won't include an `action` parameter though, 84 | so we'll just check that and return early if it's missing. 85 | 86 | (Note that the only reason we return a string is that we have to return 87 | *something*. GitHub keeps a log of its webhook requests, so returning an 88 | informative string makes it really easy to see what happened when you look at 89 | that log.) 90 | 91 | ## Parsing out the record link 92 | 93 | GitHub includes the PR description when they send the webhook, and we want to 94 | find the record URL in that. We want to be able to include other stuff too, so 95 | we'll use a regex to find it. Let's add a helper function to do that: 96 | 97 | ```js 98 | function getRecordIdFromBody(body) { 99 | // Find something that looks like a link to a record, and extract the id from it 100 | var recordLinkPattern = /^http.*\/records\/([0-9a-fA-F]{24})\b/m; 101 | var match = body.match(recordLinkPattern); 102 | if (match) return match[1]; 103 | } 104 | ``` 105 | 106 | Now that we can grab the record ID off the description, we need to check if 107 | there even is one. If not, we'll just stop here: 108 | 109 | ```js 110 | // Find out what record is linked from the PR 111 | var recordId = getRecordIdFromBody(pr.body); 112 | 113 | // If there's no record ID, just don't do anything 114 | if (!recordId) return 'Nothing to do; no record link'; 115 | ``` 116 | 117 | ## Figure out the new status 118 | 119 | Next, we need to figure out what status to set on the record. If the PR is 120 | merged, we mark the record as "done"; otherwise we mark it as "pull request". 121 | 122 | ```js 123 | // If the PR is merged, mark the record as done. 124 | // If not, mark it as having a pull request 125 | var status; 126 | if (pr.merged) { 127 | status = 'done'; 128 | } else { 129 | status = 'pull request'; 130 | } 131 | 132 | /* update the record */ 133 | ``` 134 | 135 | ## Actually update the record 136 | 137 | Finally, we're going to use the Fieldbook API to update the record. When you 138 | create a codelet, you automatically get a 139 | [Fieldbook API client](https://github.com/fieldbook/fieldbook-client) 140 | initialized with your book. You don't have to set up any API keys or anything. 141 | It's just there as the global `client`. 142 | 143 | ```js 144 | // Update the record with the status and PR url, then return 'OK'. 145 | return client.update('stories', recordId, {status: status, pr: pr.html_url}) 146 | .thenResolve(`Record ${recordId} updated`); 147 | ``` 148 | 149 | This says "tell Fieldbook to update the record, setting the status and PR 150 | fields". 151 | 152 | The `client.update(...)` call will return a promise. Codelets are fine with 153 | that; they'll wait for the promise to fulfill, and send the result as the 154 | response. We use `thenResolve` to make the output nicer, so we can look in the 155 | GitHub logs and see what happened. 156 | 157 | (And oh yeah, that's an ES6 template string. Codelets run on Node 5.5.0 with 158 | the `--harmony` flag, so you can make use of many ES6 features.) 159 | 160 | ## The end result 161 | 162 | We're done! We now have a little web server with a single endpoint capable of 163 | handling webhook requests from GitHub. 164 | 165 | Here's our finished codelet: 166 | 167 | ```js 168 | exports.endpoint = function (request, response) { 169 | var data = request.body; 170 | 171 | if (!data.action) return 'Nothing to do; not an action'; 172 | var pr = data.pull_request; 173 | 174 | // Find out what record is linked from the PR 175 | var recordId = getRecordIdFromBody(pr.body); 176 | 177 | // If there's no record ID, just don't do anything 178 | if (!recordId) return 'Nothing to do; no record link'; 179 | 180 | // If the PR is merged, mark the record as done. If not, mark it as having a pull request 181 | var status; 182 | if (pr.merged) { 183 | status = 'done'; 184 | } else { 185 | status = 'pull request'; 186 | } 187 | 188 | // Update the record with the status and PR url, then return 'OK'. 189 | return client.update('stories', recordId, {status, pr: pr.html_url}) 190 | .thenResolve(`Record ${recordId} updated`); 191 | } 192 | 193 | function getRecordIdFromBody(body) { 194 | // Find something that looks like a link to a record, and extract the id from it 195 | var recordLinkPattern = /^http.*\/records\/([0-9a-fA-F]{24})\b/m; 196 | var match = body.match(recordLinkPattern); 197 | if (match) return match[1]; 198 | } 199 | ``` 200 | 201 | # Hooking it up to GitHub 202 | 203 | Now all we have to do is add this webhook to our GitHub repo. Go to the 204 | "Settings" panel of your repo: 205 | 206 | ![GitHub settings](../images/github-settings.png) 207 | 208 | Click on "Webhooks & services", and then click "Add webhook". 209 | 210 | ![Add webhook](../images/github-add-webhook.png) 211 | 212 | Paste your codelet URL into the "Payload URL" field, then click "Let me select 213 | individual events." Make sure that only "Pull Request" is checked, and click 214 | "Add Webhook". 215 | 216 | ![Configure webhook](../images/github-config-webhook.png) 217 | 218 | And we're done! 219 | 220 | # In conclusion... 221 | 222 | How easy is that? When I first did this as a Heroku app, the whole thing took 223 | about an hour. When I ported it to a codelet, I rewrote it from scratch, and 224 | the whole thing took about 10 minutes. 225 | 226 | Make a copy of [the demo book](https://fieldbook.com/books/56c3aa4d1faa5a030071abf8), 227 | hook it up to your own repo, and hack on it as you like! 228 | -------------------------------------------------------------------------------- /codelets/one-way-book-sync.md: -------------------------------------------------------------------------------- 1 | # Syncing sheets across books (one-way) 2 | 3 | These instructions will guide you through setting up a codelet (and webhook) that will take changes to a sheet in one book, and apply those same changes in a sheet in a second book. This way, the second sheet "mirrors" the first sheet, keeping them in sync. 4 | 5 | In these instructions, the books are named "primary" and "copy", and changes from "primary" are copied to "copy". These book names are not important; you can set them however you like 6 | 7 | Each book has a sheet named "Contacts", and these two sheets must have exactly the same column names. The name field of each Contacts sheet is called "Name". If you change these, you'll need to change the "sheetSlug" and "nameFieldSlug" variables in the first codelet. 8 | 9 | To protect your webhook, it's a good idea to put some random string in the URL. In the code examples here, I've used "some-secret", but you should replace that with something harder to guess. It needs to be the same string in both codelets. 10 | 11 | ## Creating the webhook handler 12 | 13 | First, go to the "copy" book, and create a new codelet. Click on the "API" button at the top-right, then click on the "Codelets" tab, and click "New Codelet". Name the codelet "sync-contacts". 14 | 15 | Now you'll want to paste in the following code: 16 | 17 | ```js 18 | var secret = 'some-secret'; 19 | var sheetSlug = 'contacts'; // change this if your sheets are named something other than "Contacts" 20 | var nameFieldSlug = 'name'; // change this if your name field is not called "Name" 21 | 22 | var Q = require('q'); 23 | var _ = require('underscore'); 24 | exports.endpoint = Q.async(function * (request, response) { 25 | // Check the secret 26 | if (request.query.secret !== secret) return {error: 'forbidden'}; 27 | 28 | // Get changes to the contacts sheet 29 | var changes; 30 | try { 31 | changes = request.body.changes[sheetSlug]; 32 | } catch (err) { 33 | console.log(err); 34 | return err.toString(); 35 | } 36 | if (!changes) return 'Nothing to do'; 37 | 38 | var i, record, id; 39 | 40 | // Handle creates 41 | var creates = changes.create || []; 42 | for (i = 0; i < creates.length; i++) { 43 | record = creates[i]; 44 | yield client.create(sheetSlug, _.omit(record, 'id', 'record_url')); 45 | } 46 | 47 | // Handle updates 48 | var updates = changes.update || []; 49 | for (i = 0; i < updates.length; i++) { 50 | record = updates[i]; 51 | id = yield findExistingRecordId(record[nameFieldSlug]); 52 | if (id == null) continue; 53 | yield client.update(sheetSlug, id, _.omit(record, 'id', 'record_url')); 54 | } 55 | 56 | // Handle deletes 57 | var deletes = changes.destroy || []; 58 | for (i = 0; i < deletes.length; i++) { 59 | record = deletes[i]; 60 | id = yield findExistingRecordId(record[nameFieldSlug]); 61 | if (id == null) continue; 62 | yield client.destroy(sheetSlug, id); 63 | } 64 | 65 | return "Synced!" 66 | }) 67 | 68 | var findExistingRecordId = function (name) { 69 | var query = {limit: 1}; 70 | query[nameFieldSlug] = name; 71 | return client.list(sheetSlug, query).then(function (result) { 72 | if (result && result.items[0]) { 73 | return result.items[0].id; 74 | } else { 75 | console.log('No record found for name', name); 76 | } 77 | }); 78 | } 79 | ``` 80 | 81 | ## Adding the webhook 82 | 83 | Next, you need to set up a webhook on the "primary" book. The easy way to do this is to create a codelet in that book. The code for this is: 84 | 85 | ```js 86 | var secret = 'some-secret'; 87 | var webhookURL = 'CODELET_URL_HERE'; 88 | 89 | exports.endpoint = function (request, response) { 90 | return client.createWebhook(`https://fieldbookcode.com/${bookId}/sync-contacts?secret=${bookId}`); 91 | } 92 | ``` 93 | 94 | (Don't forget to replace "CODELET_URL_HERE" with the URL for the sync-contacts codelet you made in the first step, and "some-secret" with the same thing you used in the first codelet) 95 | 96 | Save the codelet, then copy its URL and load it in a new tab. You should see something like: 97 | 98 | ```json 99 | {"id":"57bdf3f11af08103003b1d4b","actions":["create","update","destroy"],"url":"https://fieldbookcode.com/COPY_BOOK_ID/sync-contacts?secret=some-secret"} 100 | ``` 101 | 102 | Once you've done that, you'll no longer need this second codelet, so you can delete it. 103 | 104 | ## Conclusion 105 | 106 | Your sheets are now linked. Just remember, if you change the fields on the "primary" book's contacts, you'll need to change them in "copy" as well. 107 | -------------------------------------------------------------------------------- /codelets/slackbot.md: -------------------------------------------------------------------------------- 1 | # Creating a Slackbot in two minutes using Fieldbook 2 | 3 | Ready? Start your timer now. 4 | 5 | 1. Create a new book, or go to a book you already have where you are an owner or admin. 6 | 7 | 2. Click the "API" button. Click the "Codelets" tab. Click "New". 8 | 9 | 3. Click the editor, select all, and type: 10 | 11 | ```js 12 | exports.endpoint = request => "Hello there!"; 13 | ``` 14 | 15 | 4. Click the URL field and copy the URL. Now click "Publish". 16 | 17 | 5. Go to https://slack.com/apps/new/A0F82E8CA-slash-commands 18 | 19 | 6. Type "/hello" under "Choose a Command" and hit enter. 20 | 21 | 7. Paste the URL you copied in the URL field. Hit enter. 22 | 23 | And... time! 24 | 25 | ## No seriously, you just made a Slackbot 26 | 27 | Go into Slack, type "/hello" and your bot will respond "Hello there!". 28 | 29 | ![Hello](../images/slackbot-hello.png) 30 | 31 | Admittedly, this isn't a very helpful bot. So let's backtrack a bit. 32 | 33 | # Creating a *useful* Slackbot in ~~two~~ ten minutes using Fieldbook 34 | 35 | So let's go back to our codelet in Fieldbook, and take a look at— 36 | 37 | *Wait a sec, what's a "codelet"?* 38 | 39 | A codelet is a snippet of code that can talk to your book, and respond to HTTP 40 | requests from the outside world. More precisely, it's a tiny webserver that 41 | handles a single URL endpoint with the minimum boilerplate possible. See the 42 | [Getting Started](../codelets.md) page for more info. 43 | 44 | The entry point into a codelet is given by `exports.endpoint`, So when you 45 | create a codelet with the one-liner: 46 | 47 | ```js 48 | exports.endpoint = request => "Hello there!"; 49 | ``` 50 | 51 | you are effectively creating a webserver that will return the string "Hello 52 | there!" whenever someone hits its URL. 53 | 54 | ## Let's do something less trivial 55 | 56 | Before we dig in, I recommend you copy [this 57 | book](https://fieldbook.com/books/56cb4d987753cf030003e54c) as a starting 58 | point. The finished codelet is already on that book, but you can just create a 59 | new one if you want to follow along. Of course, you'll also want to go back and 60 | create a new slash command for the new codelet's URL (or just change the old 61 | slash command). I'll assume you made your slash command `/project`. 62 | 63 | First of all, let's ditch the arrow-function syntax, and make a little skeleton 64 | we can add to: 65 | 66 | ```js 67 | var _ = require('underscore'); 68 | var s = require('underscore.string'); 69 | 70 | exports.endpoint = function (request, response) { 71 | return 'OK'; 72 | } 73 | ``` 74 | 75 | We're requiring those modules because we'll use them later. We've included a 76 | [big list](../codelets.md#available-modules) of popular npm modules for you to 77 | use (we're always open to adding more — just give us a shout). 78 | 79 | Let's start out by looking at the text parameter Slack gives us. This 80 | represents what the user types after `/project`. 81 | 82 | ```js 83 | ... 84 | exports.endpoint = function (request, response) { 85 | var text = request.body.text; 86 | return s.reverse(text); 87 | } 88 | ``` 89 | 90 | Publish, and try it out: `/project foo`. Your bot should print out "oof". 91 | 92 | ## That was still pretty trivial 93 | 94 | This is all well and good, but we're still not doing anything with our data. 95 | One of the great things about codelets is that they provide an API client for 96 | you with zero setup. You access it with the global `client`. 97 | 98 | Let's make our bot tell us whether or not a given project exists: 99 | 100 | ```js 101 | ... 102 | exports.endpoint = exports.endpoint = function (request, response) { 103 | var projectName = request.body.text; 104 | // Get the long form name of the project 105 | var fullName = `Project ${s.capitalize(projectName)}`; 106 | 107 | // Find all records with the given name 108 | var query = {name: fullName}; 109 | return client.list('projects', query).then(function (records) { 110 | // We just want the first record 111 | var record = records[0]; 112 | 113 | if (!record) return `No project found named ${projectName}`; // Did not match any record 114 | 115 | return `Found a project named ${projectName}!` 116 | }) 117 | } 118 | ``` 119 | 120 | This simply looks in the "Projects" sheet for records named "Project 121 | [whatever]", and tells us whether it found anything. Now we're returning a 122 | promise, which the codelet will automatically handle for us. 123 | 124 | Now if we try `/project sapphire`, we'll see "Found a project named 125 | sapphire". If we try `/project corndog`, we'll see "No project found 126 | named corndog". 127 | 128 | ## Outputting more info 129 | 130 | Of course, the Fieldbook client tells us a lot more than whether the record 131 | exists. Let's have our bot print out the record's fields: 132 | 133 | ```js 134 | ... 135 | if (!record) return `No project found named ${projectName}`; // Did not match any record 136 | 137 | var client = record.client[0]; // Client is a link field, so it's going to give us an array 138 | var clientName = client ? client.name : ''; // May not have a client 139 | 140 | var employee = record.employee[0] // Same deal as for clients 141 | var employeeName = employee ? employee.name : ''; 142 | 143 | var attributes = [ 144 | {title: 'Client', value: clientName, short: true}, 145 | {title: 'Employee', value: employeeName, short: true}, 146 | {title: 'Goal', value: record.goal, short: true}, 147 | {title: 'Status', value: record.status, short: true}, 148 | {title: 'Hours', value: record.hours, short: true}, 149 | ]; 150 | 151 | return { 152 | text: "OK:", 153 | attachments: [{ 154 | fallback: record.name, 155 | title: record.name, 156 | title_link: record.record_url, 157 | fields: attributes, 158 | }] 159 | } 160 | ... 161 | ``` 162 | 163 | Now we're doing a more sophisticated response. We return an object with an 164 | attachment to display a consise summary of the record. 165 | 166 | ![Final result](../images/slackbot-final.png) 167 | 168 | Since we returned an object, the codelet will automatically switch the response 169 | type to `application/json` and stringify the object. 170 | 171 | # Just the beginning 172 | 173 | That should get you started with a basic Slackbot that can talk to your book. 174 | Of course, there's no reason to limit yourself to just looking up individual 175 | records. You can query, edit, and create new records too. 176 | 177 | Be sure to check out the [API 178 | documentation](https://github.com/fieldbook/api-docs), and the [docs for the 179 | fieldbook-client module](https://github.com/fieldbook/fieldbook-client) to see 180 | what you can do. 181 | -------------------------------------------------------------------------------- /images/api-base-url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fieldbook/api-docs/05d7e161ff59f1715e35289d4589f646e10347e9/images/api-base-url.png -------------------------------------------------------------------------------- /images/api-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fieldbook/api-docs/05d7e161ff59f1715e35289d4589f646e10347e9/images/api-button.png -------------------------------------------------------------------------------- /images/api-explorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fieldbook/api-docs/05d7e161ff59f1715e35289d4589f646e10347e9/images/api-explorer.png -------------------------------------------------------------------------------- /images/codelet-form-sheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fieldbook/api-docs/05d7e161ff59f1715e35289d4589f646e10347e9/images/codelet-form-sheet.png -------------------------------------------------------------------------------- /images/codelet-form-tacit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fieldbook/api-docs/05d7e161ff59f1715e35289d4589f646e10347e9/images/codelet-form-tacit.png -------------------------------------------------------------------------------- /images/codelets-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fieldbook/api-docs/05d7e161ff59f1715e35289d4589f646e10347e9/images/codelets-tab.png -------------------------------------------------------------------------------- /images/copy-book-menu-item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fieldbook/api-docs/05d7e161ff59f1715e35289d4589f646e10347e9/images/copy-book-menu-item.png -------------------------------------------------------------------------------- /images/generate-api-key-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fieldbook/api-docs/05d7e161ff59f1715e35289d4589f646e10347e9/images/generate-api-key-button.png -------------------------------------------------------------------------------- /images/github-add-webhook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fieldbook/api-docs/05d7e161ff59f1715e35289d4589f646e10347e9/images/github-add-webhook.png -------------------------------------------------------------------------------- /images/github-config-webhook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fieldbook/api-docs/05d7e161ff59f1715e35289d4589f646e10347e9/images/github-config-webhook.png -------------------------------------------------------------------------------- /images/github-example-book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fieldbook/api-docs/05d7e161ff59f1715e35289d4589f646e10347e9/images/github-example-book.png -------------------------------------------------------------------------------- /images/github-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fieldbook/api-docs/05d7e161ff59f1715e35289d4589f646e10347e9/images/github-settings.png -------------------------------------------------------------------------------- /images/manage-api-menu-item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fieldbook/api-docs/05d7e161ff59f1715e35289d4589f646e10347e9/images/manage-api-menu-item.png -------------------------------------------------------------------------------- /images/manage-api-modal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fieldbook/api-docs/05d7e161ff59f1715e35289d4589f646e10347e9/images/manage-api-modal.png -------------------------------------------------------------------------------- /images/new-api-key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fieldbook/api-docs/05d7e161ff59f1715e35289d4589f646e10347e9/images/new-api-key.png -------------------------------------------------------------------------------- /images/slackbot-final.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fieldbook/api-docs/05d7e161ff59f1715e35289d4589f646e10347e9/images/slackbot-final.png -------------------------------------------------------------------------------- /images/slackbot-hello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fieldbook/api-docs/05d7e161ff59f1715e35289d4589f646e10347e9/images/slackbot-hello.png -------------------------------------------------------------------------------- /metadata.md: -------------------------------------------------------------------------------- 1 | Metadata API 2 | ============ 3 | 4 | The Fieldbook metadata API lets you read the structure of a book (sheets and fields) so you can adapt to dynamic or unknown schemas. Example use cases: 5 | 6 | * Creating a custom input form for a sheet that automatically includes all its fields 7 | * Creating custom notifications that automatically include all fields 8 | * Building a third-party integration that works with arbitrary customer data 9 | 10 | Authentication 11 | -------------- 12 | 13 | See the main API reference: [Authentication](reference.md#authentication) 14 | 15 | Content types 16 | ------------- 17 | 18 | As in the main API, everything is JSON; be sure to include header `Accept: application/json`. 19 | 20 | Getting book info 21 | ----------------- 22 | 23 | ```GET https://api.fieldbook.com/v1/books/:book_id``` 24 | 25 | Retrieves basic info about a book, mainly its title. Example: 26 | 27 | ``` 28 | $ curl -u $KEY:$SECRET https://api.fieldbook.com/v1/books/58e1a67f5662a603001916eb 29 | ``` 30 | 31 | Response (HTTP 200 OK): 32 | 33 | ``` 34 | { 35 | "id": "58e1a67f5662a603001916eb", 36 | "title": "Order Tracking", 37 | "url": "https://fieldbook.com/books/58e1a67f5662a603001916eb" 38 | } 39 | ``` 40 | 41 | Getting sheet info 42 | ------------------ 43 | 44 | ```GET https://api.fieldbook.com/v1/books/:book_id/sheets``` 45 | 46 | Lists sheets on a book. The response is an array of objects, each with: 47 | 48 | * `id`: permanent globally unique sheet id 49 | * `title`: sheet display name 50 | * `slug`: what you use to read and write records through the main API 51 | * `url`: Fieldbook web link for the sheet 52 | 53 | Example: 54 | 55 | ``` 56 | $ curl -u $KEY:$SECRET https://api.fieldbook.com/v1/books/58e1a67f5662a603001916eb/sheets 57 | ``` 58 | 59 | Response (HTTP 200 OK): 60 | 61 | ``` 62 | [ 63 | { 64 | "id": "58e1a67f5662a603001916ed", 65 | "title": "Products", 66 | "slug": "products", 67 | "url": "https://fieldbook.com/sheets/58e1a67f5662a603001916ed" 68 | }, 69 | { 70 | "id": "58e1aa1ec053ce0300bd2cf1", 71 | "title": "Orders", 72 | "slug": "orders", 73 | "url": "https://fieldbook.com/sheets/58e1aa1ec053ce0300bd2cf1" 74 | }, 75 | { 76 | "id": "58e1ac185662a6030019170e", 77 | "title": "Line items", 78 | "slug": "line_items", 79 | "url": "https://fieldbook.com/sheets/58e1ac185662a6030019170e" 80 | } 81 | ] 82 | ``` 83 | 84 | Getting field info 85 | ------------------ 86 | 87 | ```GET https://api.fieldbook.com/v1/sheets/:sheet_id/fields``` 88 | 89 | Lists fields in a sheet. The response is an array of objects, each with: 90 | 91 | * `key`: permanent field id, unique only within a sheet 92 | * `name`: display name for the field 93 | * `slug`: field slug used in reading/writing records via the main API 94 | * `fieldType`: data, link or formula 95 | * `inputType`: data input type, if fieldType=data 96 | * `required`: boolean, may be omitted if false 97 | * `enum`: for pick list fields, this is the choice list in order 98 | 99 | The inputType corresponds to our [data input types](http://docs.fieldbook.com/docs/data-types), valid values: 100 | 101 | * generic 102 | * text 103 | * number 104 | * currency 105 | * percent 106 | * date 107 | * boolean 108 | * picklist 109 | * image 110 | * file 111 | * email 112 | * website 113 | * dayofyear 114 | 115 | Example: 116 | 117 | ``` 118 | $ curl -u $KEY:$SECRET https://api.fieldbook.com/v1/sheets/58e1a67f5662a603001916ed/fields 119 | ``` 120 | 121 | Response (HTTP 200 OK): 122 | 123 | ``` 124 | [ 125 | { 126 | "key": "f0", 127 | "name": "Name", 128 | "required": true, 129 | "slug": "name", 130 | "fieldType": "data", 131 | "inputType": "generic" 132 | }, 133 | { 134 | "key": "f1", 135 | "name": "Description", 136 | "slug": "description", 137 | "fieldType": "data", 138 | "inputType": "text" 139 | }, 140 | { 141 | "key": "f2", 142 | "name": "Price", 143 | "slug": "price", 144 | "fieldType": "data", 145 | "inputType": "currency" 146 | }, 147 | { 148 | "key": "f3", 149 | "name": "Status", 150 | "slug": "status", 151 | "fieldType": "data", 152 | "inputType": "picklist", 153 | "enum": [ 154 | "Available", 155 | "Out of stock", 156 | "Discontinued" 157 | ] 158 | }, 159 | { 160 | "key": "f4", 161 | "name": "Line items", 162 | "slug": "price", 163 | "fieldType": "link" 164 | } 165 | ] 166 | ``` 167 | 168 | Read-only 169 | --------- 170 | 171 | The metadata API is read-only right now; you can explore the structure of a book but you can't create new books or sheets or alter structure yet. We plan to add a read/write metadata API in the future. 172 | -------------------------------------------------------------------------------- /quick-start.md: -------------------------------------------------------------------------------- 1 | Fieldbook API Quick Start 2 | ========================= 3 | 4 | Explore 5 | ------- 6 | 7 | The Fieldbook API explorer is the the fastest way to get started experimenting and playing with the API. Just hit the API button on any book: 8 | 9 | ![api-explorer](images/api-explorer.png) 10 | 11 | Need an example book? Use [this one](https://fieldbook.com/books/5670a30956dc4b0300003272?show_api=1). 12 | 13 | The console shows you example requests in JavaScript and lets you run them and view the responses. Edit the code as much as you like to prototype and explore. The console is powered by [Tonic](https://tonicdev.com), so you can `require()` any npm module, and use ES7-style `await` to resolve promises inline. 14 | 15 | From prototype to production 16 | ---------------------------- 17 | 18 | The API explorer uses a temporary session-based API key. When you're ready to turn your prototyping into a script or production code: 19 | 20 | 1. Create a (non-temporary) API key using the “Manage API access” button in the API console. 21 | 22 | 2. Copy down the password (API secret); it will only be shown once. 23 | 24 | 3. Now you can use the API key (username) and secret (password) to write client code. 25 | 26 | Code you write in the console can be run directly as a Node script. Be sure to `npm install requestify` and any other modules you use, and set the `FIELDBOOK_USER` and `FIELDBOOK_KEY` environment variables to your API key and secret. 27 | 28 | See the [client examples](client-examples.md) and the full [API reference](reference.md). 29 | -------------------------------------------------------------------------------- /reference.md: -------------------------------------------------------------------------------- 1 | Fieldbook API 2 | ============= 3 | 4 | The Fieldbook API lets you read and write records from any book you have access to, as simple JSON records. 5 | 6 | Quick start 7 | ----------- 8 | 9 | If you just want to dive in and get started, see the [quick start guide](quick-start.md). 10 | 11 | Version 12 | ------- 13 | 14 | The version number in the URL is `v1`, but in semver terms consider this ~v0.3. We expect the API to evolve rapidly. 15 | 16 | Limitations 17 | ----------- 18 | 19 | To get a few limitations out of the way up front: 20 | 21 | * The API doesn't yet support formulas. We'll serve up your data, but if you want to do calculations you'll have to do them yourself for now. 22 | 23 | * In particular we don't support multi-field record names, or numeric ID names (see [our docs](http://docs.fieldbook.com/docs/the-name-column) for more info on these options). If you've configured one of these options, you'll have to compute the name yourself from the other info we give you. 24 | 25 | * We don't handle conflicts among field or sheet slugs. If you have two fields named the same thing in a sheet, you'll get an arbitrary one of them in your records. If you have two sheets named the same thing in a book... well, you're gonna have a bad time. Make names unique. 26 | 27 | Authentication 28 | -------------- 29 | 30 | ### The basics 31 | 32 | * Authentication is required on all API calls, except for GET requests to a book with public API access enabled. 33 | * HTTPS is enforced; non-HTTPS requests will get a redirect to an HTTPS URL. 34 | * Requests use HTTP basic auth. The username is an API key, and the password is the secret associated with that key (see below). 35 | 36 | ### API keys 37 | 38 | * Manage API keys for a book by opening up the API console and using the “Manage API access” button. 39 | * You can revoke a key by deleting it from the management UI. 40 | * Right now keys are named key-1, key-2, etc. In the future we'll allow naming of keys. 41 | 42 | ### Public (read-only) access 43 | 44 | Optionally, any book can be enabled for public (read-only) access. In this case, anyone can read data from the book without authentication. 45 | 46 | Endpoints 47 | --------- 48 | 49 | * Each book has a base URL displayed in the API management panel, like `https://api.fieldbook.com/v1/5643be3316c813030039032e`. 50 | 51 | * Each sheet has an URL based on its title, like `https://api.fieldbook.com/v1/5643be3316c813030039032e/people`. (See below for how sheet titles are converted into slugs for these URLs. Again, don't name two sheets the same thing, or there will be conflicts!) 52 | 53 | * Each record has an URL based on its sheet title and its short numeric ID, like `https://api.fieldbook.com/v1/5643be3316c813030039032e/people/3`. 54 | 55 | * You can optionally append `.json` to any URL. 56 | 57 | Sheet titles & field names 58 | -------------------------- 59 | 60 | Sheet titles and field names in the API are based on their display names, but converted to lowercase, underscored identifiers without puncutation. E.g.: 61 | 62 | * “Tasks” --> `tasks` 63 | * “First name” --> `first_name` 64 | * “City, state & zip” --> `city_state_zip` 65 | * “First-time visitor?” --> `first_time_visitor` 66 | 67 | Content types 68 | ------------- 69 | 70 | Basically, everything is JSON: 71 | 72 | * All requests must accept `application/json` responses. 73 | * All request bodies must have a `Content-Type` of `application/json`. 74 | 75 | Record objects 76 | -------------- 77 | 78 | Throughout the API, records (rows) are represented as simple JSON objects. 79 | 80 | The keys of the object are based on the field names, as described above. (Again, don't name two fields the same thing, or there will be conflicts!) 81 | 82 | ### In response bodies 83 | 84 | * Each record also has an `id` key with a short integer ID. This can be used to retrieve the record. (Again, don't name a field “ID”, or the actual ID will be shadowed.) 85 | 86 | * The values are JSON values you would expect: strings, numbers, etc. 87 | 88 | * All numeric types are numbers. A currency value like $7 is read from the API as just 7. A percent value like 30% is read as 0.3. 89 | 90 | * Dates are strings in the form YYYY-MM-DD; a day-of-year is in the form MM-DD. 91 | 92 | * Checkboxes are returned as boolean values (`true` or `false`). 93 | 94 | * Linked cells are arrays of objects. By default, the only fields included are the short ID and any fields that are part of the record name. To include all fields of linked records, pass the `expand` parameter (see below). 95 | 96 | * Empty cells are returned as `null`. 97 | 98 | * Again, formula values are not included right now. 99 | 100 | ### In request bodies 101 | 102 | * In general, values are interpreted the same way they would be if you typed them into a Fieldbook sheet, whether they are strings or numbers. 103 | 104 | * If a field has an input type set on it, that will guide the parsing and interpretation of the value. A text field will coerce all values to strings; a currency field will turn numbers into currency values. Again, this is what would happen if you typed the input into the sheet in the UI. 105 | 106 | * For a linked field, an object or a list of objects can be provided that reference linked records by id, e.g.: `[{"id": 1}, {"id": 2}]`. If an `id` is given, any other attributes are ignored. 107 | 108 | * If an object without an `id` is given for a linked field, a new record is created in the linked sheet with the given attributes. 109 | 110 | * A `null` value clears a cell. 111 | 112 | * Values for formula fields are ignored. 113 | 114 | Input validation 115 | ---------------- 116 | 117 | Create and update requests perform validation on the request bodies. An HTTP 400 Bad Request response will be returned if: 118 | 119 | * Any key in the request body doesn't match a field in the sheet. 120 | * Any value fails the validation for the input type (if any) of its field (for instance, passing a string to a number field). 121 | * Any value is missing from a required field. 122 | * An object or array is given for a non-link field. 123 | * A value other than an object or array is given for a link field. 124 | 125 | Requests 126 | -------- 127 | 128 | ### Read a sheet (list of records) 129 | 130 | ```GET https://api.fieldbook.com/v1/:book_id/:sheet_title``` 131 | 132 | Retrieves a list of all records in the sheet. Example: 133 | 134 | ``` 135 | $ curl -u $KEY:$SECRET https://api.fieldbook.com/v1/5643be3316c813030039032e/people 136 | ``` 137 | 138 | Response (HTTP 200 OK): 139 | 140 | ``` 141 | [ 142 | { 143 | "id":1, 144 | "name":"Alice", 145 | "age":23, 146 | "city":[ 147 | { 148 | "id":2, 149 | "name":"Chicago" 150 | } 151 | ] 152 | }, 153 | { 154 | "id":2, 155 | "name":"Bob", 156 | "age":38, 157 | "city":[ 158 | { 159 | "id":1, 160 | "name":"New York" 161 | } 162 | ] 163 | }, 164 | { 165 | "id":3, 166 | "name":"Carol", 167 | "age":41, 168 | "city":[ 169 | { 170 | "id":3, 171 | "name":"Los Angeles" 172 | } 173 | ] 174 | } 175 | ] 176 | ``` 177 | 178 | #### Sheet queries 179 | 180 | You can filter the list using simple `key=value` query parameters. E.g., append `?name=Alice` to the previous example: 181 | 182 | ``` 183 | $ curl -u $KEY:$SECRET \ 184 | https://api.fieldbook.com/v1/5643be3316c813030039032e/people?name=Alice 185 | ``` 186 | 187 | Response (HTTP 200 OK): 188 | 189 | ``` 190 | [ 191 | { 192 | "id":1, 193 | "name":"Alice", 194 | "age":23, 195 | "city":[ 196 | { 197 | "id":2, 198 | "name":"Chicago" 199 | } 200 | ] 201 | } 202 | ] 203 | ``` 204 | 205 | Note: 206 | 207 | * The query is a case-sensitive exact match. 208 | * For link fields, the value is matched against the display string of the linked cells (which is a comma-separated list in the case of multi-links). 209 | * Queries on formula fields are not yet supported. 210 | 211 | A more full-fledged query mechanism is coming in a future update. 212 | 213 | #### Include or exclude fields 214 | 215 | By default all fields are included in response objects. You can customize this with the `include` and `exclude` parameters. Each takes a comma-separated list of field keys. If `include` is passed, then only the named fields will be returned (plus the `id` field). If `exclude` is passed, then any named fields will be excluded. If the same field key is passed in both the include and exclude list, the exclude list will take priority. 216 | 217 | For instance: 218 | 219 | ``` 220 | $ curl -u $KEY:$SECRET \ 221 | https://api.fieldbook.com/v1/5643be3316c813030039032e/people?include=name,age 222 | ``` 223 | 224 | Response (HTTP 200 OK): 225 | 226 | ``` 227 | [ 228 | { 229 | "id":1, 230 | "name":"Alice", 231 | "age":23 232 | }, 233 | { 234 | "id":2, 235 | "name":"Bob", 236 | "age":38 237 | }, 238 | { 239 | "id":3, 240 | "name":"Carol", 241 | "age":41 242 | } 243 | ] 244 | ``` 245 | 246 | Note that the `id` field is always included unless it is specifically excluded: 247 | 248 | ``` 249 | $ curl -u $KEY:$SECRET \ 250 | https://api.fieldbook.com/v1/5643be3316c813030039032e/people?include=name&exclude=id 251 | ``` 252 | 253 | Response (HTTP 200 OK): 254 | 255 | ``` 256 | [ 257 | { 258 | "name":"Alice" 259 | }, 260 | { 261 | "name":"Bob" 262 | }, 263 | { 264 | "name":"Carol" 265 | } 266 | ] 267 | ``` 268 | 269 | #### Expand fields 270 | 271 | When linked records are included, by default the only fields populated on those records are the short ID and any fields that are part of the record name, as in the examples above. To include all fields on linked records, pass the `expand` parameter, with a comma-separated list of field keys. 272 | 273 | Example: 274 | 275 | ``` 276 | $ curl -u $KEY:$SECRET https://api.fieldbook.com/v1/5643be3316c813030039032e/people?expand=city 277 | ``` 278 | 279 | Response (HTTP 200 OK): 280 | 281 | ``` 282 | [ 283 | { 284 | "id":1, 285 | "name":"Alice", 286 | "age":23, 287 | "city":[ 288 | { 289 | "id":2, 290 | "name":"Chicago", 291 | "state":"IL", 292 | "timezone":"Central" 293 | } 294 | ] 295 | }, 296 | { 297 | "id":2, 298 | "name":"Bob", 299 | "age":38, 300 | "city":[ 301 | { 302 | "id":1, 303 | "name":"New York", 304 | "state":"NY", 305 | "timezone":"Eastern" 306 | } 307 | ] 308 | }, 309 | { 310 | "id":3, 311 | "name":"Carol", 312 | "age":41, 313 | "city":[ 314 | { 315 | "id":3, 316 | "name":"Los Angeles", 317 | "state":"CA", 318 | "timezone":"Pacific" 319 | } 320 | ] 321 | } 322 | ] 323 | ``` 324 | 325 | #### Pagination 326 | 327 | You can paginate queries using the `limit` and `offset` query parameters: 328 | 329 | * `limit=N` will limit the result object to at most N records 330 | * `offset=K` will skip the first K records before returning anything 331 | 332 | E.g., append `?limit=10` to a query to just get the first 10 records; append `?limit=10&offset=10` to get the second page of records (11–20), update the offset to 20 to get the third page, etc. 333 | 334 | When either `limit` or `offset` are supplied, the response will not be an array but an object, with keys: 335 | 336 | * `count`: The total number of records in the query result set. 337 | * `offset`: The offset of the sub-list that is being returned (echoed from the supplied offset parameter, or 0 if none was supplied). 338 | * `items`: An array of records, starting at the offset. If a limit is given, the array will be no longer than the limit. 339 | 340 | If `offset + items.length < count`, then there are more records to load beyond what was returned; update the offset to fetch the next page. 341 | 342 | Example: 343 | 344 | ``` 345 | $ curl -u $KEY:$SECRET \ 346 | https://api.fieldbook.com/v1/5643be3316c813030039032e/people?limit=2 347 | ``` 348 | 349 | Response (HTTP 200 OK): 350 | 351 | ``` 352 | { 353 | "count":3, 354 | "offset":0, 355 | "items":[ 356 | { 357 | "id":1, 358 | "name":"Alice", 359 | "age":23, 360 | "city":[ 361 | { 362 | "id":2, 363 | "name":"Chicago" 364 | } 365 | ] 366 | }, 367 | { 368 | "id":2, 369 | "name":"Bob", 370 | "age":38, 371 | "city":[ 372 | { 373 | "id":1, 374 | "name":"New York" 375 | } 376 | ] 377 | } 378 | ] 379 | } 380 | ``` 381 | 382 | ### Read a record 383 | 384 | ```GET https://api.fieldbook.com/v1/:book_id/:sheet_title/:record_id``` 385 | 386 | Retrieves a single record. Example: 387 | 388 | ``` 389 | $ curl -u $KEY:$SECRET https://api.fieldbook.com/v1/5643be3316c813030039032e/people/1 390 | ``` 391 | 392 | Response (HTTP 200 OK): 393 | 394 | ``` 395 | { 396 | "id":1, 397 | "name":"Alice", 398 | "age":23, 399 | "city":[ 400 | { 401 | "id":2, 402 | "name":"Chicago" 403 | } 404 | ] 405 | } 406 | ``` 407 | 408 | #### Include or exclude fields 409 | 410 | A GET request for a single record can take the same `include` and `exclude` parameters as a GET request for a sheet, see above. 411 | 412 | ### Create a record 413 | 414 | ```POST https://api.fieldbook.com/v1/:book_id/:sheet_title ``` 415 | 416 | With a JSON body representing a single record, creates a record in the sheet. Returns the new record. Example: 417 | 418 | ``` 419 | $ curl -u $KEY:$SECRET -H "Content-Type: application/json" \ 420 | https://api.fieldbook.com/v1/5643be3316c813030039032e/people \ 421 | -d '{"name":"Dave","age":19,"city":[{"id":1}]}' 422 | ``` 423 | 424 | Response (HTTP 201 Created): 425 | 426 | ``` 427 | { 428 | "id":4, 429 | "name":"Dave", 430 | "age":19, 431 | "city":[ 432 | { 433 | "id":1, 434 | "name":"New York" 435 | } 436 | ] 437 | } 438 | ``` 439 | 440 | The `Location` header of the response will have the new full URL of the record, which in this example is `https://api.fieldbook.com/v1/5643be3316c813030039032e/people/4`. 441 | 442 | This appends the record to the sheet; inserting at an arbitrary location is not yet supported. 443 | 444 | ### Update a record 445 | 446 | ```PATCH https://api.fieldbook.com/v1/:book_id/:sheet_title/:record_id ``` 447 | 448 | With a JSON body containing any attributes for a record, updates the record. Only the keys included in the body are updated; other keys are left alone. Returns the full updated record. Example: 449 | 450 | ``` 451 | $ curl -u $KEY:$SECRET -H "Content-Type: application/json" -X PATCH \ 452 | https://api.fieldbook.com/v1/5643be3316c813030039032e/people/1 \ 453 | -d '{"age":24,"city":[{"name":"Boston"}]}' 454 | ``` 455 | 456 | Response (HTTP 200 OK): 457 | 458 | ``` 459 | { 460 | "id":1, 461 | "name":"Alice", 462 | "age":24, 463 | "city":[ 464 | { 465 | "id":4, 466 | "name":"Boston" 467 | } 468 | ] 469 | } 470 | ``` 471 | 472 | Update via `PUT` is not currently supported. 473 | 474 | ### Delete a record 475 | 476 | ```DELETE https://api.fieldbook.com/v1/:book_id/:sheet_title/:record_id``` 477 | 478 | Deletes a record. Example: 479 | 480 | ``` 481 | $ curl -u $KEY:$SECRET -X DELETE \ 482 | https://api.fieldbook.com/v1/5643be3316c813030039032e/people/1 483 | ``` 484 | 485 | Response: HTTP 204 No Content (empty body). 486 | 487 | Webhooks 488 | -------- 489 | 490 | Fieldbook supports webhook callbacks on record create, update and delete events. 491 | 492 | ### Key things to know 493 | 494 | * Webhooks can be registered on a per-sheet basis, or a per-book basis (listening to all sheets). 495 | 496 | * Webhooks are sent about record data changes. There are no meta-data webhooks yet. 497 | 498 | * Each registered webhook can listen for create, update, and/or delete events. However, each callback may include multiple types of changes, across multiple records. In general, we try to send one callback for each action a user takes in the system, so bulk actions can cause multiple types of updates at once, and to multiple records. For instance, pasting a block of values into a sheet can update multiple records and also create multiple new records, in a single event. 499 | 500 | ### Registering a webhook 501 | 502 | To register a webhook, POST a webhook body to the webhooks collection for a book: 503 | 504 | ```POST https://api.fieldbook.com/v1/:book_id/meta/webhooks ``` 505 | 506 | The request body: 507 | 508 | * Must have an `url` key with a callback URL. 509 | * May optionally have an `actions` key containing an array of actions to listen for. The actions are `create`, `update`, and `destroy`. If omitted, the callback will fire on all actions. 510 | * May optionally have a `sheet` key with a sheet slug or id. If provided, the webhook will only fire for changes in this sheet. If omitted, it will fire for changes in any sheet in the book. 511 | 512 | Example: 513 | 514 | ``` 515 | $ curl -u $KEY:$SECRET -H "Content-Type: application/json" -X POST \ 516 | https://api.fieldbook.com/v1/5643be3316c813030039032e/meta/webhooks \ 517 | -d '{"url":"https://you.com/your/callback","actions":["create", "update"]}' 518 | ``` 519 | 520 | Response (HTTP 200 OK): 521 | 522 | ``` 523 | { 524 | "id": "56b01529cf979181cfe28941", 525 | "url": "https://you.com/your/callback", 526 | "actions": [ 527 | "create", 528 | "update" 529 | ], 530 | } 531 | ``` 532 | 533 | ### Callback format 534 | 535 | On each relevant event, a callback will be POSTed to the callback URL for each webhook. The request body contains: 536 | 537 | * The `webhookId` that the callback is for 538 | * A `user` hash with basic details about the user who made the change 539 | * A `changes` hash. If the webhook was not created with a sheet specified, then this has one key per sheet that was affected, each of which has: 540 | - Optionally, a `create` array of created records 541 | - Optionally, an `update` array of updated records 542 | - Optionally, a `destroy` array of deleted records. 543 | 544 | Example: 545 | 546 | ``` 547 | { 548 | "webhookId": "56b01529cf979181cfe28941", 549 | "user": { 550 | "email": "jason@fieldbook.com", 551 | "name": "Jason Crawford", 552 | "id": "56a97a97242dce2ee012a9ad" 553 | }, 554 | "changes": { 555 | "people": { 556 | "create": [ 557 | { 558 | "id": 4, 559 | "name": "Dave", 560 | "age": 54, 561 | "city": [ 562 | { 563 | "id": 1, 564 | "name": "New York" 565 | } 566 | ] 567 | } 568 | ], 569 | "update": [ 570 | { 571 | "id":3, 572 | "name":"Carol", 573 | "age":42, 574 | "city":[ 575 | { 576 | "id":3, 577 | "name":"Los Angeles" 578 | } 579 | ] 580 | } 581 | ] 582 | } 583 | } 584 | } 585 | ``` 586 | 587 | If the webhook *was* registered for a particular sheet, then the `changes` hash *only* contains changes for the one sheet, like this: 588 | 589 | ``` 590 | { 591 | "webhookId": "56b01529cf979181cfe28941", 592 | "user": { 593 | "email": "jason@fieldbook.com", 594 | "name": "Jason Crawford", 595 | "id": "56a97a97242dce2ee012a9ad" 596 | }, 597 | "changes": { 598 | "create": [ 599 | { 600 | "id": 4, 601 | "name": "Dave", 602 | "age": 54, 603 | "city": [ 604 | { 605 | "id": 1, 606 | "name": "New York" 607 | } 608 | ] 609 | } 610 | ], 611 | "update": [ 612 | { 613 | "id":3, 614 | "name":"Carol", 615 | "age":42, 616 | "city":[ 617 | { 618 | "id":3, 619 | "name":"Los Angeles" 620 | } 621 | ] 622 | } 623 | ] 624 | } 625 | } 626 | ``` 627 | 628 | ### Delivery caveats 629 | 630 | Some caveats on the delivery of webhook callbacks: 631 | 632 | * Delivery order is not guaranteed. 633 | 634 | * The record data in each callback is the latest data for that record at the time the callback was generated. This will usually correspond to the action taken by the user, but in some conditions may not. For instance, if a record is created by one user and then quickly updated by another user, it is possible that the create callback will contain the data as edited by the second user. 635 | 636 | * If the callback request receives an error response, we will retry at least once. However, we don't currently guarantee any particular number of retries or timing of the retries. 637 | 638 | * Callbacks are designed to notify of changes to record data, not metadata. They should have reasonable behavior in the face of metadata changes, but we do not advise relying too closely on any particular webhook behavior around metadata. 639 | 640 | ### Deleting (de-registering) a webhook 641 | 642 | To de-register a webhook, just DELETE it. Example: 643 | 644 | ``` 645 | $ curl -u $KEY:$SECRET -H "Content-Type: application/json" -X DELETE \ 646 | https://api.fieldbook.com/v1/5643be3316c813030039032e/meta/webhooks/56b01529cf979181cfe28941 647 | ``` 648 | 649 | The response will be HTTP 204 No Content. 650 | 651 | ### Listing webhooks 652 | 653 | To list all webhooks on a book, just GET the collection. Example: 654 | 655 | ``` 656 | $ curl -u $KEY:$SECRET https://api.fieldbook.com/v1/5643be3316c813030039032e/meta/webhooks 657 | ``` 658 | 659 | Response (HTTP 200 OK): 660 | 661 | ``` 662 | [ 663 | { 664 | "id": "56b01529cf979181cfe28941", 665 | "url": "https://you.com/your/callback", 666 | "actions": [ 667 | "create", 668 | "update" 669 | ], 670 | }, 671 | { 672 | "id": "564125b564b72f4fd3ed9dba", 673 | "url": "https://you.com/other/callback", 674 | "actions": [ 675 | "destroy" 676 | ], 677 | } 678 | ] 679 | ``` 680 | 681 | ### Webhook security 682 | 683 | If you want to protect your webhook receiving endpoints, you can add basic auth parameters to the callback URL, like this: 684 | 685 | ``` 686 | https://user:password@example.com/your/callback 687 | ``` 688 | 689 | Callback URLs are encrypted when stored in our database, in order to protect these credentials. 690 | 691 | HTTPS is required when using this format. (However, even if you're not using a username/password, we recomend using HTTPS in callback URLs.) 692 | 693 | Metadata API 694 | ------------ 695 | 696 | To read the book schema (sheets and fields), see the [metadata API](metadata.md). 697 | 698 | Method override 699 | --------------- 700 | 701 | If you're using an HTTP client that for some reason doesn't support all HTTP methods (such as PATCH), you can do a POST instead and specify the X-HTTP-Method-Override header. Example: 702 | 703 | ``` 704 | $ curl -u $KEY:$SECRET -H "Content-Type: application/json" \ 705 | -H "X-HTTP-Method-Override: PATCH" -X POST \ 706 | https://api.fieldbook.com/v1/5643be3316c813030039032e/people/1 \ 707 | -d '{"age":24,"city":[{"name":"Boston"}]}' 708 | ``` 709 | 710 | This will be interpreted as a PATCH. 711 | 712 | The method override header only works with POST requests. 713 | 714 | Rate limits 715 | ----------- 716 | 717 | To prevent runaway API clients from causing sudden shocks to the system and degrading performance for everyone, we limit the API to 5 simultaneous outstanding requests from any given API key. Beyond that limit, requests may receive an HTTP 429 Too Many Requests error response. 718 | 719 | * If you are writing a script that uses the API to process many records in batch, we recommend that you serialize your API calls: wait for one call to finish before firing off the next call. This is the simplest way to avoid any rate limit errors. 720 | 721 | * If you have a server or codelet that uses our API and needs a higher rate limit, contact us: support@fieldbook.com 722 | 723 | Future work 724 | ----------- 725 | 726 | There are a lot of things we're thinking about supporting in the future; shoot us a note at support@fieldbook.com to let us know which of these are most important to your needs: 727 | 728 | * Support for full [queries](http://docs.fieldbook.com/docs/queries) 729 | * Including [formulas](http://docs.fieldbook.com/docs/formulas) (calculated/derived values) in responses 730 | * Read-only API keys 731 | -------------------------------------------------------------------------------- /snapshot-format.md: -------------------------------------------------------------------------------- 1 | Fieldbook Snapshot Format 2 | ========================= 3 | 4 | Overview 5 | -------- 6 | 7 | The Fieldbook snapshot format captures almost everything about a book (see what is included below). 8 | 9 | Audience 10 | -------- 11 | 12 | This documentation is for programmers who would like to parse and work with a Fieldbook snapshot. It may be too technical for other users. If you don't have technical skills or a technical person to help you, it will be easier for you to work with your data in Excel format, which you can also download. 13 | 14 | What is included 15 | ---------------- 16 | 17 | * Structure of sheets, columns, types, links and formulas 18 | * Saved searches and forms 19 | * All rows and links 20 | * Links to attachments 21 | 22 | Not captured: 23 | 24 | * Webhooks 25 | * Codelets 26 | * Zapier integrations 27 | * Users who the book was shared with 28 | * The attachments themselves, which are separate files 29 | 30 | JSON format 31 | ----------- 32 | 33 | The snapshot is a JSON file and can be parsed with any JSON parser. The rest of this document describes the objects you will find, their keys and values. 34 | 35 | Terminology 36 | ----------- 37 | 38 | The JSON file uses its own set of terminology. Some terms you might not recognize from the app: 39 | 40 | * “Field”: synonym for column 41 | * “Record”: synonym for row 42 | * “Join”: represents the fact of two sheets being linked 43 | * “Symref”: a link chip/bubble in a cell, that links to a row in another sheet (short for “symmetric reference”) 44 | * “Nav item”: a saved search or form, as shows up in the navigation section of the left sidebar 45 | * “Query”: a saved search 46 | * “Subsheet”: the table view of a linked sheet that appears on the detail view 47 | * “Enum”: the list of values in a pick list (short for “enumerated values”) 48 | 49 | IDs 50 | --- 51 | 52 | Many objects have an `_id` key. This is usually of the form `$_ObjectId_1`. This is used as a reference by other objects. For instance, a join will contain the ids of the two sheets it links. 53 | 54 | The ID key will not be explicitly mentioned below in every object where it appears. 55 | 56 | Top-level keys 57 | -------------- 58 | 59 | The top-level object keys are: 60 | 61 | * `book`: All meta-data about the book: its title, the list of sheets, etc. 62 | * `recordSets`: A list of lists of records, one per sheet 63 | * `symrefSets`: A list of lists of symrefs, one per join. See more about joins and symrefs below. 64 | 65 | Book 66 | ---- 67 | 68 | The `book` top-level key contains: 69 | 70 | * `title`: book title 71 | * `sheets`: a list of metadata about sheets, one per sheet, in the order that they appear in the book 72 | * `joins`: a list of joins (sheet links), see below 73 | * `localeSet`: info about the book's locale settings 74 | - `number`: 'us' for comma separator and decimal point (1,234.5); 'eu' for the reverse (1.234,5) 75 | - `date`: 'us' for mm/dd/yyyy format; 'eu' for dd/mm/yyyy format 76 | - `timezone`: time zone for interpreting the today() function 77 | 78 | Sheets 79 | ------ 80 | 81 | Each object in the book's `sheets` list contains: 82 | 83 | * `title`: sheet title 84 | * `description`: sheet description 85 | * `fields`: a list of field (column) objects, in they order that they appear in the sheet (see below) 86 | * `navItems`: a list of navigation items (saved searches and forms), in the order they appear in the navigation (see below) 87 | * `subsheets`: a list of subsheets, in the order they appear on the detail view, each with: 88 | - `sheet`: an object containing one key, the id of the sheet that is referenced by this subsheet 89 | - `field`: an object containing one key, the key of the field in that sheet that is the opposite side of this link 90 | 91 | Fields 92 | ------ 93 | 94 | Each object in a sheet's `fields` list contains: 95 | 96 | * `key`: a short id for this field that is unique within a sheet, usually of the form 'f1' 97 | * `name`: display name 98 | * `description`: column description 99 | * `type`: this value can be: 100 | - 'general' or 'enum': a data column (possibly with an enum) 101 | - 'formula': a formula column 102 | - 'join': a link to another sheet 103 | * `width`: field width in pixels 104 | * `validation`: data input type ('generic', 'text', 'number', 'date', 'dayofyear', 'currency', 'percent', 'email', 'website', 'image', 'file', 'boolean' (checkbox), or 'picklist') 105 | * `enum`: for a pick list, the list of values 106 | * `expression`: for formula columns, the formula (as an AST) 107 | * `expressionString`: for formula columns, the rendered display version of the formula 108 | * `wrapText`: whether text should be wrapped in this column (true/false) 109 | * `required`: whether this field requires a value (true/false) 110 | * `hidden`: whether this field is hidden (true/false) 111 | 112 | Note that in addition to the regular columns, each sheet contains a special field with the key `__name__`. This field can be ignored. 113 | 114 | Records 115 | ------- 116 | 117 | Each entry in the top-level `recordSets` key corresponds to a sheet, and contains a list of records. 118 | 119 | Each record is an object that has a key for each field in its sheet's fields list (typically f0, f1, f2, etc.) 120 | 121 | The value for each key is an object that contains: 122 | 123 | * `value`: the value in this cell 124 | * `type`: the value type ('string', 'numeric', 'currency', 'percent', 'boolean' (checkbox), 'date', 'dayofyear', 'image', 'file', or 'empty') 125 | * `input`: the original input that was entered or imported, before being parsed and interpreted into the value 126 | 127 | Nav items 128 | --------- 129 | 130 | Each object in a sheet's `navItems` list contains: 131 | 132 | * `key`: a short id for this nav item that is unique within a sheet, usually of the form 'n1' 133 | * `name`: display name 134 | * `description`: view description 135 | * `type`: 'query' or 'form' 136 | * `public`: for a form, whether it is publicly accessible (true/false) 137 | * `uuid`: an id that is used to create a public form link, if any 138 | 139 | If the type is 'query', this is a saved search/view and there will be a `query` key that contains an AST of the query. 140 | 141 | If the type is 'form', this is a form and there will be a `form` key that contains the configuration for the form. 142 | 143 | Joins 144 | ----- 145 | 146 | A join represents the fact that two sheets are linked. Each object in a book's `joins` list contains a `left` and `right` side of the join (these are arbitrary and interchangeable). Each side contains a `sheetId` and `fieldKey` (within that sheet) identifying the link column that is one side of the join. 147 | 148 | As an example, in a book linking Projects and Tasks, there would be a join where: 149 | 150 | * the “right” side (for example) had the sheetId for Projects, and the fieldKey of the Tasks column within the Projects sheet 151 | * the “left” side had the sheetId for Tasks, and the fieldKey of the Project column within the Tasks sheet 152 | 153 | Symrefs 154 | ------- 155 | 156 | Each entry in the top-level `symrefSets` key corresponds to a join, and contains a list of symrefs (“symmetric references”, aka links). 157 | 158 | Each symref is an object with `left` and `right` sides. Each side has an `_id` key that refers to a row. To continue the example above, if the sheets Projects and Tasks are linked, and Project 3 has two tasks with IDs 11 and 93, you would see these two entries in the symref set: 159 | 160 | * `{right: {_id: 3, ordinal: 0}, left: {_id: 11, ordinal: 0}}` 161 | * `{right: {_id: 3, ordinal: 0}, left: {_id: 93, ordinal: 1}}` 162 | 163 | Note that each side also has an `ordinal`. This indicates the order of the links within the link cell. (note: could be clearer) 164 | 165 | Example code 166 | ------------ 167 | 168 | Here is sample code (written and tested in Node v6.9.5) to read a JSON file given on the command line and print the contents of the book. It demonstrates how to walk through sheets, records and fields; how to access values; and how to look up links: 169 | 170 | ``` 171 | const fs = require('fs'); 172 | 173 | let json = fs.readFileSync(process.argv[2], 'utf8'); 174 | let {book, recordSets, symrefSets} = JSON.parse(json); 175 | 176 | // Build a map of records by id 177 | let recordMap = {}; 178 | recordSets.forEach(recordSet => { 179 | recordSet.forEach(record => { 180 | recordMap[record._id] = record; 181 | }) 182 | }) 183 | 184 | let joinKey = (sheetId, fieldKey) => `${sheetId}:${fieldKey}`; 185 | 186 | // Build a convenience map to look up the join info for a given sheet and field 187 | let joinMap = {}; 188 | book.joins.forEach((join, i) => { 189 | joinMap[joinKey(join.left.sheetId, join.left.fieldKey)] = {index: i, side: 'left', oppositeSide: 'right'}; 190 | joinMap[joinKey(join.right.sheetId, join.right.fieldKey)] = {index: i, side: 'right', oppositeSide: 'left'}; 191 | }) 192 | 193 | // Get all the linked records for a given record and field 194 | let getLinkedRecords = (sheet, record, field) => { 195 | let {index, side, oppositeSide} = joinMap[joinKey(sheet._id, field.key)]; 196 | 197 | return symrefSets[index] // look up the symref set 198 | .filter(s => s[side]._id === record._id) // filter for symrefs that match the source record 199 | .map(s => s[oppositeSide]) // pick out the opposite side (i.e., the linked record) 200 | .sort((a, b) => a.ordinal - b.ordinal) // sort by the symref ordinal to get links in correct order 201 | .map(r => recordMap[r._id]); // look up the actual referenced record by id 202 | } 203 | 204 | console.log(`Book: ${book.title}`); 205 | 206 | book.sheets.forEach((sheet, i) => { 207 | let fields = sheet.fields; 208 | let records = recordSets[i]; 209 | 210 | console.log(`\n---\n\nSheet: ${sheet.title}`); 211 | records.forEach(record => { 212 | console.log(`\nRecord ${record.shortId}`); 213 | fields.forEach(field => { 214 | if (field.type === 'join') { 215 | let linkedRecords = getLinkedRecords(sheet, record, field); 216 | let names = linkedRecords.map(r => r.name); 217 | console.log(`- ${field.name}: ${names.join(', ')}`); 218 | } else if (field.type === 'formula') { 219 | // We aren't going to compute formulas here, but we can log the string 220 | console.log(`- ${field.name} = ${field.expressionString}`); 221 | } else { 222 | let value = record[field.key]; 223 | if (value) console.log(`- ${field.name}: ${value.value}`); 224 | } 225 | }) 226 | }) 227 | }) 228 | ``` 229 | --------------------------------------------------------------------------------