├── .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 | 
26 |
27 | Then open the Codelets tab:
28 |
29 | 
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 | 
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 |
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 | 
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 |
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 | 
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 | 
207 |
208 | Click on "Webhooks & services", and then click "Add webhook".
209 |
210 | 
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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------