The new API is a JSON-based REST API. Our new webapp uses the exact same API. This is your guarantee that the API will always support full functionality and generally be nice to work with. We take our own medicine!
During the beta period, the API may have backwards incompatible changes with very short notices. Follow @billydeveloper on Twitter to subscribe to updates.
If you have any questions or feedback (we love feedback!), please ask on Twitter or mail us at beta@billysbilling.com.
- Endpoint
- Authentication
- Conventions
- Relationships
- Paging
- Filtering
- Sorting
- Code examples
- Use case examples
- Resource documentation
The API is located at https://api.billysbilling.com/v2
.
All requests must use SSL.
We currently support two ways of authenticating with the API.
###OAuth access tokens
You can obtain an access token for your organization this way:
- Log into your account on app.billysbilling.com.
- Go to Settings -> Access tokens.
- Click Create access token.
- Enter a descriptive name for it, so you can identify it later, and hit the Save button.
- Hover over the newly generated access token and click the magnifier icon to show the token.
- The token will now be selected in a textfield inside a lightbox.
All you have to do is put this token in an X-Access-Token
header. See Code examples for full example.
The tokens you create under Settings -> Access tokens are tied to that organization only. If you have multiple organizations, you need a token for each one. They do not expire.
We plan on supporting a 3-legged OAuth solution eventually.
###HTTP basic auth
You can use your normal email and password to authenticate with HTTP Basic Auth. You can try it right now by navigating to e.g. https://api.billysbilling.com/v2/user
. Enter your email and password in the browser's credentials form, and you will see your user as a JSON document.
The API is very consistent as we use the same conventions for all resources. Generally speaking there are 5 ways to interact with a resource: Get one item, list, create, update and delete.
A resource is always accessed through its pluralized name. E.g. you can access invoices through /v2/invoices
.
The following examples will use the invoices
resource as an example. But the exact same pattern applies to all of our resources.
When getting a single record you should use e.g. GET /v2/invoices/:id
(where :id
denotes a dynamic slug, and should be replaced by a real invoice ID). The response will be a JSON document with a root key named after the singular name that contains the requested record. Invoice example:
{
"invoice": {
"id": "3mIN9EbDEeOg8QgAJ4gM",
"invoiceNo": 41,
...
}
}
Use GET /v2/invoices
. The response will be a JSON document with a root key named after the pluralized name that contains an array of found records. Invoice example:
{
"invoices": [
{au
"id": "3mIN9EbDEeOg8QgAJ4gM",
"invoiceNo": 41,
...
},
{
"id": "4LmaTkbDEeOg8QgAJ4to",
"invoiceNo": 42,
...
}
]
}
Use POST /v2/invoices
. The request body should be a JSON document containing a single root key named after the singular name and be a hash of record properties. Invoice example:
{
"invoice": {
"invoiceNo": 41,
"entryDate": "2013-11-14",
...
}
}
See more about the response body in the section Responses to create/update/delete requests. You can get the ID of the newly created record by getting invoices.0.id
in the returned JSON document.
Use PUT /v2/invoices/:id
. The request body should be a JSON document containing a single root key named after the singular name and be a hash of record properties. The hash does not need to include the record's ID, but if it does, it needs be the exact same as you used in the URL. You can't change a record's ID. Invoice example:
{
"invoice": {
"contactMessage": 41,
...
}
}
Only properties that you set in the invoice
hash will be updated. Properties that are left out will be considered as if they are the same. So if all you need to do is to update the contactMessage
property, then you don't need to include any other properties.
The response works the same as with a POST
request.
Use PATCH /v2/invoices
. The request body should be a JSON document containing a single root key named after the plural name and be an array of record hashes. Each record hash is either a record to create (if it does not contain an id
) or update (if it does contain an id
). Invoice example:
{
"invoices": [
{
"invoiceNo": 41,
"entryDate": "2013-11-14",
...
},
{
"id": "3mIN9EbDEeOg8QgAJ4gM",
"contactMessage": "Whale",
...
}
]
}
This example will create a new invoice with invoice no. 41, and update the contactMessage
of an existing invoice with ID 3mIN9EbDEeOg8QgAJ4gM
.
Use DELETE /v2/invoices/:id
. The request should not have a body.
DELETE
requests are idempotent, meaning that if you try to DELETE
the same record more than once (or delete any other non-existing ID), you will still receive a 200 OK
response.
See more about the response body in the section Responses to create/update/delete requests.
Use DELETE /v2/invoices?ids[]=:id1&ids[]=:id2
. The request should not have a body.
When you make POST
, PUT
, PATCH
or DELETE
requests to the API the response will always contain all the records that were changed because of the request. The record that you created/updated will of course always be there. But some. Example: When you create an invoiceLine
for an existing invoice
, the amount
field on the invoice
will also be changed. So a POST /v2/invoiceLines
would return something like:
{
"invoices": [
{
"id": "3mIN9EbDEeOg8QgAJ4gM",
"amount": 200,
...
}
],
invoiceLines: [
{
"id": "cgYxHZWCSfajDIvj9Q8yRQ",
"invoiceId": "3mIN9EbDEeOg8QgAJ4gM",
...
}
]
}
The IDs of deleted records will be accessible through the meta.deletedRecords
property. Example: When requesting DELETE /v2/invoices/1234
, the response will be:
{
"meta": {
"deletedRecords": {
"invoices": [
"1234"
]
}
}
}
Note that some PUT
requests may delete records, too. An example is when you overwrite a record's has-many relationship using an embedded array of child records.
The API is smart about relationships. Many resources have relationships, and we distinguis between two kinds of relationships:
- Belongs-to: A record of resource A has an ID that points to a record of resource B. Example:
invoice
records have acontactId
property which points to acontact
record. - Has-many: A record of resource A has zero or more records of resource B that points to it. Example:
invoice
records have zero or moreinvoiceLine
records that have aninvoiceId
property that points back. A has-many relationship always implies a belongs-to relationship on the inverse resource.
Normally when you GET
a resource that has a relationships, the belongs-to relationship will be presented as the name of the property suffixed with Id
and contain the ID. If you GET /v2/invoices/123
and it responds with a "contactId": "987"
, you can get the contact
by requesting GET /v2/contacts/987
. Has-many relationships won't be in the request per default.
You can include relationships by adding an include
parameter to your GET
request. The value of the include
parameter should be on the form resource.property:mode[,resource.property:mode,...]
, where resource
is the name of a resource, property
is the name of a property of that resource, and mode
is either sideload
(default if the :
part is omitted) or embed
. You can include multiple relationships by separating them with ,
.
Example: You can load invoices
with its invoiceLines
embedded, its contacts
sideloaded, and the contact's country embedded by doing GET /invoices?include=invoice.lines:embed,invoice.contact:sideload,contact.country:embed
. The response will be something like:
{
"invoices: [
{
"id": "invoice1",
"contactId": "contact1",
...
"lines": [
{
"id": "line1",
...
},
{
"id": "line2",
...
}
]
}
],
"contacts: [
{
"id": "contact1",
"country": {
"id": "DK",
"name": "Denmark",
...
},
...
}
]
}
When sideloading belongs-to relationships the name of the key is the singular name of the resource suffixed with Id
(like the default behavior), and each sideloaded record can be found in a root key named after the plural name of the belongs-to inverse resource. It's recommended to sideload records instead of embedding, to avoid duplication (each belongs-to record is only included in the response once).
When embedding belongs-to relationships the name of the key is the singular name of the resource, e.g. contact
and it contains all of the record's properties.
When sideloading has-many relationships all the child IDs are included as an array of strings in the parent record in a key named after the relationship's name (not the inverse resource) suffixed with Ids
.
When embedding has-many relationships all the full child records are included as an array of hashes in the parent record in a key named after the relationship's name (not the inverse resource).
Some resources supports saving child records embedded. An example is invoices. A POST /v2/invoices
's request body could look like this:
{
"invoice": {
"invoiceNo": 41,
...
"lines:" [
{
"productId": "...",
"unitPrice": 100,
...
}
]
}
}
It is advantageous to embed records when possible, as you get fewer round trips to the API, and everything happens as an atomic transaction (either all or no records are saved).
You can limit long lists of resources using the paging URL params.
To use pages, you can use page
and pageSize
. E.g. to get the second page having 20 records per page you would request GET /v2/invoices?page=2&pageSize=20
.
If you like using indexes/offsets better you use offset
and pageSize
. E.g. to get 51 records starting at record at zero-index-based 78, you would request GET /v2/invoices?offset=51&pageSize=78
.
pageSize
cannot be greater than 1000
(which is also the default if pageSize
is omitted).
Whether your results are truncated/paged can be found in the meta.paging
key in the response. It will look something like:
{
"meta": {
"paging": {
"page": 4,
"pageCount": 9,
"pageSize": 20,
"total": 192,
"firstUrl": "https://api.billysbilling.com/v2/invoices?pageSize=20",
"previousUrl": "https://api.billysbilling.com/v2/invoices?page=3&pageSize=20",
"nextUrl": "https://api.billysbilling.com/v2/invoices?page=5&pageSize=20",
"lastUrl": "https://api.billysbilling.com/v2/invoices?page=9&pageSize=20"
}
},
"invoices": [...]
}
When listing most resources you can filter the results by various properties.
When listing most resources you can sort the results using the sortProperty
and sortDirection
URL params. Each resource only allows specific properties to be sorted by. Which ones are noted in each resource's documentation. The sortDirection
must be either ASC
(default) or DESC
.
<?php
//Define a reusable class for sending requests to the Billy's Billing API
class BillyClient {
private $accessToken;
public function __construct($accessToken) {
$this->accessToken = $accessToken;
}
public function request($method, $url, $body = null) {
$headers = array("X-Access-Token: " . $this->accessToken);
$c = curl_init("https://api.billysbilling.com/v2" . $url);
curl_setopt($c, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
if ($body) {
curl_setopt($c, CURLOPT_POSTFIELDS, json_encode($body));
$headers[] = "Content-Type: application/json");
}
curl_setopt($c, CURLOPT_HTTPHEADER, $headers);
$res = curl_exec($c);
$body = json_decode($res);
$info = curl_getinfo($c);
return (object)array(
'status' => $info['http_code'],
'body' => $body
);
}
}
$client = new BillyClient("INSERT ACCESS TOKEN HERE");
//Get organization
$res = $client->request("GET", "/organization");
if ($res->status !== 200) {
echo "Something went wrong:\n\n";
print_r($res->body);
exit;
}
$organizationId = $res->body->organization->id;
//Create a contact
$res = $client->request("POST", "/contacts", array(
'contact' => array(
'organizationId' => $organizationId,
'name' => "Arnold",
'countryId' => "DK"
)
));
if ($res->status !== 200) {
echo "Something went wrong:\n\n";
print_r($res->body);
exit;
}
$contactId = $res->body->contacts[0]->id;
//Get the contact again
$res = $client->request("GET", "/contacts/" . $contactId);
print_r($res->body);
First run npm install async request
.
var async = require('async'),
request = require('request');
var accessToken = 'INSERT ACCESS TOKEN HERE';
//Define a reusable function to send requests to the Billy's Billing API
var billyRequest = function(method, url, body, callback) {
body = body || {}
request({
url: 'https://api.billysbilling.com/v2' + url,
method: method,
headers: {
'X-Access-Token': accessToken,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
}, function(err, res, body) {
if (err) {
callback(err);
} else {
callback(null, {
status: res.statusCode,
body: JSON.parse(body)
});
}
});
};
var organizationId, contactId;
async.series([
function(callback) {
//Get organization
billyRequest('GET', '/user/organizations', {}, function(err, res) {
if (err) {
callback(err);
} else if (res.status !== 200) {
console.log('Something went wrong:');
console.log(res.body);
process.exit();
} else {
organizationId = res.body.organizations[0].id;
callback();
}
});
},
function(callback) {
//Create a contact
billyRequest('POST', '/contacts', {
contact: {
organizationId: organizationId,
name: 'Arnold',
countryId: 'DK'
}
}, function(err, res) {
if (err) {
callback(err);
} else if (res.status !== 200) {
console.log('Something went wrong:');
console.log(res.body);
process.exit();
} else {
contactId = res.body.contacts[0].id;
callback();
}
});
},
function(callback) {
//Get the contact
billyRequest('GET', '/contacts/' + contactId, {}, function(err, res) {
if (err) {
callback(err);
} else if (res.status !== 200) {
console.log('Something went wrong:');
console.log(res.body);
process.exit();
} else {
console.log(res.body);
callback();
}
});
}
], function(err) {
if (err) {
console.log(err);
}
process.exit();
});
When you want to mark an invoice as paid, you have to create a bank payment that matches the invoice's amount. Let's say you have just created an invoice with ID inv-1234
with a total amount due of 1,200 USD. This is how you would create the payment
POST https://api.billysbilling.com/v2/bankPayments
{
"bankPayment": {
"organizationId": "YOUR ORGANIZATION ID",
"entryDate": "2014-01-16",
"cashAmount": 1200,
"cashSide": "debit",
"cashAccountId": "BANK ACCOUNT ID",
"associations": [
{
"subjectReference": "invoice:inv-1234"
}
]
}
}
cashAccountId
is the account that an amount of cashAmount
was deposited to/withdrawn on. cashSide: "debit"
means a deposit (what you use for invoices). cashSide: "credit"
means a withdrawal (what you use for bills).
Each item in the associations
array is a balance modifier. Since payments can pay different types of models (invoices, bills, etc.), you add the invoice to the payment by using a "reference", which is a type concatenated with a colon concateneted with an ID, e.g. invoice:inv-1234
.
The currency of the payment is determined by the associations. This means that all the associations have to point to subjects in the same currency, i.e. you can't pay a USD invoice together with en EUR invoice. This currency is called subjectCurrency
, if you GET
the bank payment later from the API. If the subjectCurrency
is different from the currency of cashAccount
, you also need to set a cashExchangeRate
. In this example we assume that the cashAccount
is in USD.
After this call our example invoice (of 1,200 USD) will be paid, which visible through the invoice's isPaid
property, which ill be true
, and it's balance
property, which will be 0.0
.
The organizationId
is only necessary if you are using an access token that's tied to a user (i.e. not an organization), or if you are using Basic Auth. You can list your user's organizations through GET /v2/user/organizations
.
When POST
ing to /v2/products
, you can embed product prices this way:
POST https://api.billysbilling.com/v2/products
{
"product": {
"organizationId": "YOUR ORGANIZATION ID",
"name": "Bat capes",
"accountId": "REVENUE ACCOUNT ID",
"salesTaxRulesetId": "SALES TAX RULESET ID"
"prices": [
{
"unitPrice": 14000,
"currencyId": "USD"
},
{
"unitPrice": 10000,
"currencyId": "EUR"
}
]
}
}
The organizationId
is only necessary if you are using an access token that's tied to a user (i.e. not an organization), or if you are using Basic Auth. You can list your user's organizations through GET /v2/user/organizations
.