NAV
shell

TODO, take in account:

Introduction

Welcome to the QBilling API. It will provide you access to universal billing system that is usage-agnostic, what makes it suitable for financial companies for storing accounts and transferring money, games with inner currency or anyone else who have balance and any actions with it.

Key Features

Data Retention Policy

We believe that vendor-lock is a bad thing, thats why you are free to download all data from your account in a JSON format.

Also you can remove all you data from our servers. After your confirm data remove process in Dashboard we will keep everything for additional 30 days, so your account will be protected from accidental removals. After 30 days all data will be scrubbed and impossible to restore.

API-centered

We are API-centered, that means that we are trying to make it simple, easy to understand, and yet very powerful.

Crossintegrations

We support hustle-free integration with oAuth providers. See more at Integrating oAuth provider section.

Also we support custom webhook integrations in a SOA-style. This will help you to add any puzzle-pieces that you may need: users storage, antifraud systems, data storages, scoring systems, etc.

Project scopes

We allow you to create separated projects in case you need them for different test or production environments.

Account types

We have 3 plans for different clients:

  1. Starter. This is a free account that you can use for testing purposes. It’s rate limited to 1000 requests per 15 minutes. Support trough GitHub issues.

  2. Basic. Includes all features starter plan and rate limit of 5000 requests per 15 minutes.

  3. Enterpise. Basic package plus access to custom SQL queries. Backups for this plan can be downloaded 4 times per day. Plus email support.

Need custom installation or 99.9% SLA? Contact us!

Interacting with API

Our API is organized around REST. It has predictable, resource-oriented URLs, and uses HTTP response codes to indicate API errors. We use built-in HTTP features, like HTTP authentication and HTTP verbs, which are understood by off-the-shelf HTTP clients. We support cross-origin resource sharing, allowing you to interact securely with our API from a client-side web application (though you should never expose your secret API key in any public website’s client-side code).

HTTP Verbs

As per RESTful design patterns, API implements following HTTP verbs:

HTTP status codes

HTTP Code Description
200 Everything worked as expected.
400 Bad Request. The request was unacceptable, often due to missing a required parameter. Or request contains invalid JSON. Duplicate idempotency key.
401 Unauthorized. No valid API key provided or API key doesn’t match project.
402 The parameters were valid but the request failed.
403 Source or destination account is disabled.
404 Not Found. The requested resource doesn’t exist.
415 Incorrect Content-Type HTTP header.
429 Too Many Requests. Rate limit is exceeded.
500, 502, 503, 504 Server Errors. Something went wrong on our end. (These are rare.)

Authentication

To use our service you need to authenticate your application. Authentication to the API is performed via HTTP Basic Auth. Provide your API key as the basic auth username value. You do not need to provide a password. You can manage your API keys in the Dashboard.

curl https://example.com/resource \
   -u WgLodNU5wCdbSw4f:

Additionally you can create oAuth cross-integration for authenticating your clients and making requests directly to our API. You can find more info at Integrating oAuth provider section.

Response structure

Response can consist of 5 root properties:

{
  "meta": {
    "url": "https://qbill.ly/transactions/",
    "type": "list",
    "code": "200",
    "idempotency_id": "iXXekd88DKqo",
    "request_id": "qudk48fFlaP"
  },
  "urgent": {
    "notifications": ["Read new emails!"],
    "unseen_payments": 10
  },
  "data": {
    <...>
  },
  "paging": {
    "limit": 50,
    "cursors": {
      "after": "MTAxNTExOTQ1MjAwNzI5NDE=",
      "before": "NDMyNzQyODI3OTQw"
    },
    "has_more": true
  },
  "sandbox": {
    "debug_varibale": "39384",
    "live": "false"
  }
}

Errors

All errors is returned in JSON format if another Content-Type is not specified. This means that your will receive JSON for requests with HTTP 415 code when incorrect Content-Type is provided.

Error Object Properties

You can find all necessary information about occurred error in a response meta.error property. It can have following fields:

Fields Description
type General error type. You should return human-readable error message based on this field as a key. Type descriptions is listed in next section.
invalid Collection of validation errors for your request.
invalid[].entry_type Type of invalid field.
invalid[].entry_id ID of invalid field.
invalid[].rule Failed rule for invalid field. You can find supported validation rules at Request Validators table.
invalid[].params Optional parameters that can be used in a human-readable error message, to make it easier to understand. Usually it contains limit values for failed validator.
message Human readable message for API developer.
{
  "meta": {
    "error": {
      "type": "form_validation_failed",
      "invalid": [
        {
          "entry_type": "field",
          "entry_id": "username",
          "rules": [
            {
              "rule": "min:6",
              "params":{"min": 6}
            },
            {
              "rule": "length:2",
              "params":{"lenght": 2}
            }
          ]
        },
        {
          "entry_type": "field",
          "entry_id": "email",
          "rules": [
            {"rule": "empty"}
          ]
        },
        {
          "entry_type": "header",
          "entry_id":"Timezone",
          "rules": [
            {"rule": "date"}
          ]
        },
        {
          "entry_type": "request",
          "entry_id":null,
          "rules": [
            {"rule": "json"}
          ]
        }
      ],
      "message": "Validation failed. Return human-readable error message. You find all possible validation rules at https://docs.qbill.ly/#request-validators."
    }
  }
}

Error Types

Parameter Description
ID The ID to retrieve
duplicated_idempotency_key

Request Data Validators

All invalid request data is listed in meta.error.invalid object. There are different types of entries:

List of possible validation rules:

Validator Rule Description
active_url The field under validation must be a valid URL according to the checkdnsrr PHP function.
after:<date> The field under validation must be a value after a given date. The dates will be passed into the strtotime PHP function. Sample rule: “`date
before:<date> The field under validation must be a value preceding the given date. The dates will be passed into the PHP strtotime function. Sample rule: ”`date
alpha The field under validation must be entirely alphabetic characters.
alpha_dash The field under validation may have alpha-numeric characters, as well as dashes and underscores.
alpha_num The field under validation must be entirely alpha-numeric characters.
between:<min>,<max> The field under validation must have a size between the given and . Strings, numerics, and files are evaluated in the same fashion as the size rule.
boolean The field under validation must be able to be cast as a boolean. Accepted input are true, false, 1, 0, "1", and "0".
confirmed The field under validation must have a matching field of foo_confirmation. For example, if the field under validation is password, a matching password_confirmation field must be present in the input.
date A valid data in ISO 8601 format.
digits:<value> The field under validation must be numeric and must have an exact length of value.
digits_between:<min>,<max> The field under validation must have a length between the given min and max.
email The field under validation must be formatted as an e-mail address.
in:<foo>,<bar>,<...> The field under validation must be included in the given list of values.
json The field under validation must be a valid JSON string.
max:<value> The field under validation must be less than or equal to a maximum value. Strings, numerics, and files are evaluated in the same fashion as the size rule.
min:<value> The field under validation must have a minimum value. Strings, numerics, and files are evaluated in the same fashion as the size rule.
not_in:<foo>,<bar>,<...> The field under validation must not be included in the given list of values.
numeric The field under validation must be numeric.
regex:<pattern> The field under validation must match the given regular expression.
required The field under validation must be present in the input data and not empty. A field is considered “empty” is one of the following conditions are true: the value is null; the value is an empty string; the value is an empty array or empty Countable object; the value is an uploaded file with no path.
size:<value> The field under validation must have a size matching the given value. For string data, value corresponds to the number of characters. For numeric data, value corresponds to a given integer value. For files, size corresponds to the file size in kilobytes.
string The field under validation must be a string.
timezone The field under validation must be a valid timezone identifier according to the timezone_identifiers_list PHP function.
url The field under validation must be a valid URL according to PHP’s filter_var function.
otp_code A valid OTP code.
password_strong Password field that should include at least one latin letter in lowercase, one letter in uppercase and one number. Min lenght is set by another rule.
—————————–

Pagination

All top-level API resources with root type list have support of pagination over a “list” API methods. These methods share a common structure, taking at least these three parameters: limit, starting_after, and ending_before.

API utilizes cursor-based pagination via the starting_after and ending_before parameters. Both take an existing object ID value (see below). The ending_before parameter returns objects created before the named object, in descending chronological order. The starting_after parameter returns objects created after the named object, in ascending chronological order. If both parameters are provided, only ending_before is used.

Arguments:

Content Type

You should send Content-Type header in all your requests, otherwise API will return HTTP 415 status. Right now we support two content types:

This header doesn’t affect any outgoing requests, they are always sent in JSON format.

Rate Limits (Throttling)

We throttle our APIs by default to ensure maximum performance for all developers.

Rate limiting of the API is primarily considered on a per-consumer basis. All your projects share a same rate limit, to avoid API-consuming fraud. Rate limits depend on your account type.

Currently free accounts is rate limited to 1000 API calls every 15 minutes, but this value may be adjusted at our discretion.

For your convenience, all requests is sent with 3 additional headers:

HTTP Header Description
X-RateLimit-Limit Current rate limit for your application.
X-RateLimit-Remaining Remaining rate limit for your application.
X-RateLimit-Reset The time at which the current rate limit window resets in UTC epoch seconds.
X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4966
X-RateLimit-Reset: 1372700873

Cross Origin Resource Sharing

The API supports Cross Origin Resource Sharing (CORS) for AJAX requests from any origin. You can read the CORS W3C Recommendation, or this intro from the HTML 5 Security Guide.

Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-Requested-With
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Expose-Headers: ETag, Link, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-Request-ID, X-Idempotency-Key
Access-Control-Allow-Credentials: true

Timezones

All requests allow to provide a Time-Zone header to receive all date and time fields in a local timezone.

Explicitly provide an ISO 8601 timestamp with timezone information to use this feature. Also it is possible to supply a Time-Zone header which defines a timezone according to the list of names from the Olson database.

curl -H "Time-Zone: Europe/Amsterdam" ..
curl -H "Time-Zone: Europe/Kiev" ..

Request ID’s

Each API request has an associated request identifier. You can find it in X-Request-ID response header or inside meta.request_id property of returned JSON.

You can also find request identifiers in the URLs of individual request logs in your Dashboard. If you need to contact us about a specific request, providing the request identifier will ensure the fastest possible resolution.

All Request ID’s are prefixed with a human-readable name of server that served your request. (Yes, we give names to all our servers.)

X-Request-ID: flash-99spDSoim3i

Limiting Response Fields

By default, all the fields in a node are returned when you make a query. You can choose the fields you want returned with the fields query parameter. This is really useful for making your API calls more efficient and fast.

/resource?fields=id,name,balance

Expanding Response Fields into Objects

Many response fields contain the ID of a related object in their values. For example a Transfer can have an associated User object linked by a sender field. Those objects can be expanded inline with the expand request parameter. Objects that can be expanded are noted in this documentation. This parameter is available on all API requests, and applies to the response of that request only.

You can expand object by querying list, expands will be applied on all matched elements. Expanded lists return up to 25 elements, limit can be specified in brackets.

You can expand multiple objects at once by identifying multiple items divided by comma.

/accounts?expand=transactions(5)

You can nest expand requests with the dot property. For example, requesting sender.payments on a Transfer list will expand the sender property into a full User object, and will then expand the payments property of that User into a full Transactions collection.

/accounts?expand=transactions(5).recipients

Ordering Lists and Collections

By default, all collections are ordered in ascending chronological order. You can specify different order by providing the “order” query parameter.

/accounts?order=account.created_at(reverse_chronological)

Available orders:

Filtering Lists

You can filter all lists by a data. Filter can refer to any fields inside a data response property, except entities that was expanded. Also you can filter by a metadata value.

To apply filter you should provide Base64 encoded encoded JSON string with a filtering rules in a filter query parameter.

Create a JSON object and convert it to string:

$ echo "{predicates:[<predicate_1>,<predicate_1>,...]}" | base64
e3ByZWRpY2F0ZXM6WzxwcmVkaWNhdGUxPiw8cHJlZGljYXRlMT4sLi4uXX0K

Send encoded string as filter query parameter:

GET /v1/accounts?filter=e3ByZWRpY2F0ZXM6WzxwcmVkaWNhdGUxPiw8cHJlZGljYXRlMT4sLi4uXX0K

Predicate is an object with at least 3 fields:

Available comparison methods:

Name Description
eq Matches values that are equal to a specified value.
ne Matches all values that are not equal to a specified value.
gt Matches values that are greater than a specified value.
gte Matches values that are greater than or equal to a specified value.
lt Matches values that are less than a specified value.
lte Matches values that are less than or equal to a specified value.
in Matches any of the values specified in an array. Array is set to a value predicate property.
nin Matches none of the values specified in an array.
ewi Matches all values that ends with a specified value.
swi Matches all values that starts with a specified value.
{
  predicates:[
    {
      "attribute":"balance",
      "comparison":"eq",
      "value":"27"
    },
    {
      "attribute":"metadata.user_id",
      "comparison":"in",
      "value":[1, 2, 3]
    }
  ]
}

To apply filters with logical rules you can add with logical type. Available types:

Name Description
or Joins query clauses with a logical OR returns all objects that match the conditions of either clause.
and Joins query clauses with a logical AND returns all objects that match the conditions of both clauses.
not Inverts the effect of a query expression and returns objects that do not match the query expression.
nor Joins query clauses with a logical NOR returns all objects that fail to match both clauses.
{
  "predicates":[
    {
      "attribute":"metadata.user_id",
      "comparison":"eq",
      "value":"3994kdkd8"
    },
    {
      "type":"or",
      "predicates":[
        {
          "attribute":"balance",
          "comparison":"gt",
          "value":"100"
        },
        {
          "attribute":"transactions.count",
          "comparison":"gt",
          "value":"10"
        }
      ]
    }
  ]
}

Aggregating lists

All lists can be queried to get aggregations on given set of rules. When you are using aggregation default filtered period is one month. For example you can get aggregated count of Accounts to know how many accounts was created at each day for last month.

To use aggregation you need to specify only one query parameter:

Also you can change aggregation dispensation by providing tick query parameter:

Create a JSON object and convert it to string:

$ echo "{aggregate:[<aggregate_1>,<aggregate_1>,...]}" | base64
e2FnZ3JlZ2F0ZXM6WzxhZ2dyZWdhdGVfMT4sPGFnZ3JlZ2F0ZV8xPiwuLi5dfQo=

Send encoded string as filter query parameter:

GET /v1/accounts?aggregate=e3ByZWRpY2F0ZXM6WzxwcmVkaWNhdGUxPiw8cHJlZGljYXRlMT4sLi4uXX0K&tick=day

Aggregate is an object with at least 2 fields:

{
  predicates:[
    {
      "name": "accounts_count",
      "strategy": "count",
      "field": "id"
    },
    {
      "name": "accounts_liquidity",
      "strategy": "sum",
      "field": "balance"
    }
  ]
}
{
  meta: {
    "type": "list"
  }
  data: [
    {
     "tick":"2015-07-11",
     "aggregates": {
       "accounts_count": 123,
       "accounts_equity": 1000000
     }
    },
    {
     "tick":"2015-07-12",
     "aggregates": {
       "accounts_count": 98,
       "accounts_equity": 900000
     },
     ...
    }
  ]
}

Available aggregation strategies:

Name Description
count Returns count of returned aggregated fields.
sum For integers and floats returns total for all field values.
arr Returns array of all possible values. (Very useful for anti-frauds.)
max Returns maximum value of a field.
min Returns minimum value of a field.
avg Returns average value of a field.

Available tick values:

Name Description
hour Return aggregation for each hour in a filtered period.
day Return aggregation for each day in a filtered period. Default value.
month Return aggregation for each month in a filtered period.
year Return aggregation for each year in a filtered period.

Testing

All accounts have test project that is created for you on account creation. Just use test project API secret for your test environment. We don’t have any policy for data retention in test accounts and we can drop all data once a while. You can do it manually from your dashboard.

Responses in sandbox environment will always return a sandbox property.

{
  "meta":{},
  "data":{},
  "sandbox":{
    ...
  }
}

Right now test accounts is not rate limited, but we will manually limit test project that will consume too many resources.

Metadata

We support metadata field for every objects in our API. It allows the API developer to store custom information related to accounts and transactions. This information can be for example:

Metadata field supports key-value pairs with the following limitations:

Key objects

We have list of key objects that is accessible trough our API:

Batching Request

There are number of situations when you need to download few entities at once, for example when your customers have multiple accounts and you need to return balance for all of them in a single request. For this cases we have request multiplexing.

The batch API takes in an array of logical HTTP requests represented as JSON arrays - each request has a method (corresponding to HTTP method GET/PUT/POST/DELETE etc.), a uri (the portion of the URL after domain), optional headers array (corresponding to HTTP headers) and an optional body (for POST and PUT requests). The Batch API returns an array of logical HTTP responses represented as JSON arrays - each response has a status code, an optional headers array and an optional body (which is a JSON encoded string).

To make batched requests, you build a JSON object which describes each individual operation you’d like to perform and POST this to the batch API endpoint at https://batcher.qbill.co.

curl \
    -F ‘batch=[{“method”:”GET",“relative_url”:/accounts/:id1”},{“method”:”GET",“relative_url”:/accounts/:id1”}]’ \
    https://graph.facebook.com

As an alternative you can use simpler alias for a batching requests to the objects. Whenever you can provide an ID inside URL to retrieve resource, you can also provide multiple ID’s separated by comma. For example, you can get two user accounts in one request.

GET /projects/:project_id/accounts/:id1,id2,...

Response would be a list of requested items.

{
  data: [
    "<user1_id>": {"<user1>"},
    "<user2_id>": {"<user2>"}
  ]
}

Timeouts

Large or complex batches may timeout if it takes too long to complete all the requests within the batch. In such a circumstance, the result is a partially-completed batch. In partially-completed batches, responses from operations that complete successfully will look normal whereas responses for operations that are not completed will have corresponding error code in meta property.

Limits

You can batch up to 10 request. Every request in batch will be counted as separate requests in Rate Limits.

Request Flow

  1. Save request data.
  2. Run any pre-flight webhooks, for example for customer authentication. And if webhooks returned 2XX codes..
  3. Check authentication. And if authorized..
  4. Check rate limits for your application. And if they not exceeded..
  5. Validate request data and complete request. 4.1. Generate event for this request. 4.2. Queue money flow (all operations with money is asynchronous).
  6. Return API response
  7. Save API response to Requests
  8. Schedule all webhooks configured for a Project. If webhook failed, re-try it.
  9. Record all webhooks statuses

Other Security Improvements

Nosniff

We are setting X-Content-Type-Options=nosniff to make sure that older IE browsers won’t MIME-sniff a response away from the declared Content-Type.

strict-transport-security: max-age=2592000;includeSubdomains

iFrames

All Front-End Servers will disallow including any resources within an iFrame with a X-Frame-Options header.

X-Frame-Options DENY;

Content Security Policy

For API server we are setting Content-Security-Policy that disallows to load any resources.

Content-Security-Policy "default-src 'none'; script-src 'none'; img-src 'none'; style-src 'none'; font-src 'none'; child-src 'none'; frame-src 'none'; connect-src 'none'; object-src 'none'";

For Dashboard servers we will configure minimize all possible paths to resources, to make sure anything won’t be loaded from unauthorized services.

SSH Access

We are running a separate server as a single SSH authorization point, that allows to mange access right in one place, and to act fast on security incidents. Connecting to SSH ports on all other instances is only allows from Access Server IP address with a Access Server RSA keys.

Restricted On-Production Hotfixes

Nobody should ever have an SUDO password for API servers. To make any changes you need to kill existing instance and to replace it with a new one.

Best Practices

Currency flows

We encourage you to use right accounting models inside your system. Separate all accounts by a type, lets call them system and client accounts.

This will help you to calculate losses and revenue in a right, predictable way.

Funding account

Every time you need to add a money to your system you should create a Funding. Even trough they have a separate endpoint to load all fundings, we strongly recommend to fund a transit accounts first, rather that directly consumer accounts.

For example, you can create a system account for money inflow for each of your Payment Service Providers, and you would be able to list all transfers trough it, compare money flow of our PSP and QBill, and to charge front-fee by leaving some money on transit account or by batch-transferring it both to customer and “revenue” accounts.

Also we strongly recommend you to use Funding metadata power, by adding to it original ID of your PSP, original amount and other helpful information, to efficiently track money flow trough all the systems.

Additionally it would be easier for you to show your customer all transactions log, because all operations will be available trough Transfers list. Otherwise, your will need to merge two lists: Transfers and Fundings.

Revenue tracking

For revenue we recommend you to create special account for each of revenue-takers (you, your partners, etc.)

This will allow to find all transactions that is gained your revenue, and to easily understand how many you need to pay to your partners.

Finally if you want to clear revenue account once a while (for example, each time you sending money to a partner), you can simply send them to “/dev/null” account, that will accommodate all funds that should be terminated.

Direct reduction of account is not possible to keep you from common accounting mistakes.

Charges

Any time you charge customer for any type your services, you should simply create a transfer to a revenue account with corresponding metadata (to show correct transaction information for your customers).

Account Overdrafts

(IDEA) Create a system overdrafts account and fund users from it each time they need an to get a overdraft. You can calculate overdraft later by using aggregation with account filter.

(IDEA2) Create another account for your customer and fund it, then transfer money from one account to another. Funding should be done each time you are adding overdraft funs to a user.

Transferring money between projects

(IDEA) Simply move money to an project account and create a funding operation in another project.

Integrating oAuth provider

Sometimes is more rational to make request to an API without any additional services that will proxy this requests. But by adding project token to your application you would make it very vulnerable to a third-party’s.

For this cases we recommend you to request our API with a oAuth token and to add a pre-flght webhook to connect API to your oAuth provider. Flow should look something like this:

  1. Your application requests API with Authorization: Bearer <token> header.
  2. Pre-flight webhook is send to your oAuth endpoint.
  3. oAuth endpoint validates Bearer token and if its valid returns HTTP 200 status code with a X-Override-Authorization: Token <project_token> header, where <project_token> is a API project token issued in Dashboard.
  4. Our API validates new authorization token and fulfills the request.

Sample oAuth provider can be found in our GitHub account.

Token, ID lengths and formats

In order to avoid interruptions in processing, it’s best to make minimal assumptions about what our gateway-generated tokens and identifiers will look like in the future. The length and format of these identifiers – including payment method tokens and transaction IDs – can change at any time, with or without advance notice. However, it is safe to assume that they will remain 1 to 64 upper- and lowercase alphanumeric characters with minuses (-) and underscores (_).

We use ISO 8601 formats for all dates in our API.

We use E.123 telephone number in international notation for all phone numbers in our API.

All tokens have a human-readable prefix, so you can always see what scope it carries. Example: project-lksdlkfjf8ds8dfsl`.

user-Skd90i0d
project-dkkfi49dkkf
admin-3kkd9re0fdkspmv

All ID’s have a human-readable prefix that carries first 3 characters from a entity name. Example: acc_ssj8988udj.

Geographic Redundancy and Optimization

To ensure that you will always have the lowest response time we can provide, we are automatically detecting nearest datacenter to you, so all your projects have master servers in it. To migrate data to a different region please contact our support team.

Data Storage and Backup Policy

To ensure that you won’t loose your data we use geographical redundant MongoDB replica sets. It means that at least one of your secondary DB’s is hosted in another region, and will save all data in case main datacenter would be unavailable.

Providing urgent data for your users

Sometimes you want to update account balance and notifications list on each request you made. You can provide additional HTTP header X-Urgent-Account-ID with an Account ID that should be queried. All result data will be in urgent response field.

Requesting urgent data counts as a separate request and affects your rate limits.

X-Urgent-Data-ID: acc_3idjdjkd9
{
  "meta": {
  },
  "urgent": {
    "account": "acc_3idjdjkd9",
    "notifications": [],
    "unseen_payments": 0,
    "holds": 0,
    "balance": 0
  },
  "data": {
  }
}

Idempotent Requests

The API supports idempotency for safely retrying write requests without accidentally performing the same operation twice. For example, if a request to create a charge fails due to a network connection error, you can retry the request with the same idempotency key to guarantee that only a single charge is created.

To perform an idempotent request, attach a unique key to any POST, DELETE or PUT request made to the API via the Idempotency-Key: <key> header.

How you create unique keys is completely up to you. We suggest using random strings or UUIDs. We’ll always send back the same response for requests made with the same key. However, you cannot use the same key with different request parameters (We will return HTTP 400 error in this case). The keys expire after 24 hours.

If you have an “Account” entity, within your system, than one of best ways to generate right Idempotency key is to add a additional property nonce to a Account, and re-generate it every time transaction is created. Thus you will be sure that there no way to create a transaction without knowing latest state of the user account.

Conditional requests

Most responses return an ETag header. Many responses also return a Last-Modified header. You can use the values of these headers to make subsequent requests to those resources using the If-None-Match and If-Modified-Since headers, respectively. If the resource has not changed, the server will return a 304 Not Modified.

Also note: making a conditional request and receiving a 304 response does not count against your Rate Limit, so we encourage you to use it whenever possible.

Versioning

All API calls should be made with a X-API-Version header which guarantees that your call is using the correct API version. Version is passed in as a date (UTC) of the implementation in YYYY-MM-DD format.

If no version is passed, the newest will be used and a warning will be shown. Under no circumstances should you always pass in the current date as that will return the current version which might break your implementation.

SSL certificates

We recommend that all users obtain an SSL certificate and serve any data that is stored in our service over HTTPS. Also we don’t allow to add a webhook that doesn’t support SSL encryption.

We don’t have a specific preferred vendor for SSL certificates, but we recommend that you stick with a well-known provider (e.g. Network Solutions, GoDaddy, Namecheap). Generally speaking, most certificates will be similar, so it’s up to you to determine what fits your needs best.

You can test your server with a SSL Server Test.

HTTP Redirects

API uses HTTP redirection where appropriate. Clients should assume that any request may result in a redirection. Receiving an HTTP redirection is not an error and clients should follow that redirect. Redirect responses will have a Location header field which contains the URI of the resource to which the client should repeat the requests.

Status codes:

Other redirection status codes may be used in accordance with the HTTP 1.1 spec.

Accounts

Account is a one a base entities that represents any object with a balance. This balance can be in any currency, you can add a currency code in metadata object. (You find info at Metadata section.)

(TODO: Add information about in-built currencies.)

(TODO: Add overdraft limit to an Account. overdraft field that hold maximum negative balance for this user.)

List all Accounts

GET /projects/:project_id/accounts

Create an Account

POST /projects/:project_id/accounts
{
  metadata: {
    external_id: 192838,
    currency_code: 'USD'
  }
}

Response

{
  "meta": {
    "code": "201",
    "url": "https://api.qbill.ly/accounts/acc_388djejje88du",
    "type": "account",
    "request_id": "qudk48fFdaP",
    "idempotency_id": "iXXedd88DKqo"
  },
  "urgent": {
    "notifications": [],
    "unseen_payments": 0,
    "holds": 0,
    "balance": 0
  },
  "data": {
    id: "acc_388djejje88du"
    balance: 0,
    metadata: {
      external_id: 192838,
      currency_code: 'USD'
    }
  }
}

Get all Account data

GET /projects/:project_id/accounts/:id

List all Account Funding Operations

This is a shortcut to List all Fundings with an filter based on account id.

GET /projects/:project_id/accounts/:id/fundings

List all Account Holds

This is a shortcut to List all Holds with an filter based on account id.

GET /projects/:project_id/accounts/:id/holds

List all Account Transfers

This is a shortcut to List all Transfers with an filter based on account id.

GET /projects/:project_id/accounts/:id/transfers

Disabling and Enabling an Account

Accounts can’t be deleted, but can be disabled to prevent its future usage. Requesting or linking a disabled account will always result a HTTP 403 error.

Disabling

PUT /projects/:project_id/accounts/:id
{
  is_disabled: true
}

Enabling

You can enable account to continue using it later.

PUT /projects/:project_id/accounts/:id
{
  is_disabled: false
}

Fundings

Funding allows to top-up any account balance in a system. (Basically this is an equivalent for an money emission.)

You can find best practices for Funding an Account.

Create a Funding to Account

POST /projects/:project_id/fundings
{
  account_id: <account_id>,
  total: 1000
}

List all Fundings

GET /projects/:project_id/fundings

Canceling a Funding Operation

All Fundings can’t be canceled, to do so just create a Transaction and move money to a system account. You can create some sort of /dev/null account for this purpose.

Holds

(TODO: Allow subpayments on funding to charge front fee?)

(TODO: Holds should be part of Transfers API response, so consumers can return correct payment history to clients.)

(TODO: Transfer with a currency conversion.)

You should hold some amount from account balance whenever you create a multi-step payments.

List all Holds

GET /projects/:project_id/holds

Create a Hold

To create a hold you should provide at least two fields:

You can attach any metadata to a Hold.

POST /projects/:project_id/holds
{
  transfer: [
    {subtotal: 90, destination: "<service_account_id>", metadata: {service_id: 1, service_name: 'Cellular Topup'}}
    {subtotal: 10, destination: "<fees_account_id>", metadata: {for: "service_payment", service_id 1}}
  ],
  total: 100,
  metadata: {
    desctiption: "Payment for a Cellular topup"
  }
}

Changing a Hold

While money is on-hold you can change any payment details, for eg. to refund some part of money

PUT /projects/:project_id/holds/:hold_id
{
  total: 20.00
}

Cancel a Hold

After hold payment can be completed to commit balance change and turn hold into charge or declined to remove hold and return funds to available balance.

POST /projects/:project_id/holds/:hold_id/decline

Complete Hold into a Transfer

On hold completion we will add same metadata to a Transfer.

POST /projects/:project_id/holds/:hold_id/complete

Transfer

You should use Transfers whenever you accept a payment or transfer money from one account to another. Creating Transfer will decrease Account balance by transfer total.

List all Transfers

GET /projects/:project_id/transfers

Create a Transfer

To create a Transfer you need to specify at least two request fields:

You can attach any metadata to a Transfer.

POST /projects/:project_id/transfers
{
  total: 100,
  transfer: [
    {subtotal: 90, destination: "<service_account_id>", meta: {service_id: 1, service_name: 'Cellular Topup'}}
    {subtotal: 10, destination: "<fees_account_id>", meta: {for: "service_payment", service_id 1}}
  ],
  meta: {}
}

Rollback a Transfer

On a Transfer rollback we will create new Transfer from a destination to a source accounts with same totals. This means that we will also return all the fees you charged from a Account. Created Transfer will have is_rollback field set to true and rollback_refference set to a original Transfer ID.

Also rollbacked transfer will have a rollback_transfer property that will hold rollback Transfer ID and a is_rollbacke field set to true.

Create Refund for a Transfer

Refund is similar to a Rollback, but you need to specify refund total for every account that received funds. This allows you to refund funds by keeping the fees or to refund it with all the fees.

Transferring money between projects

(TODO: Should we handle it or leave it for developers?)

Currencies and Conversion Rates

Our system supports any currency with a custom conversion rates. To simplify conversion we have a base

Creating a Currency

List all Currencies

Updating a Currency

Setting a base Currency

Changing Conversion Rates

Events

List all Events

Webhooks

All webhooks is sent as POST request to a selected endpoint.

Right now we support two types of webhooks: pre-flight and event-based. Pre-flight webhooks is send on all requests and should respond synchronously. All event-based webhooks is sent when request is completed.

All actions will trigger creation of an `Event, that is be send to all your webhooks.

Limits

Webhooks is also exceeding your rate limits, one for each request that was sent, so be careful and void creating too many of them.

Right now you can create two pre-flight webhooks and 5 event-based webhooks.

Request structure

We are sending same data object as described in an Event section of this documentation.

<Request sample>

(TODO: Move this to an event structure.) We add few additional HTTP headers for a request:

X-Application-ID: <application_id>
X-Project-ID: <project_id>
X-HTTP-Verb: <http_verb>
X-Requested-Object: <api_entity>
X-Requested-URI: <uri>

You can make sure that webhooks is coming from our back-end by validating X-Webhook-Secret header. It’s a sha1 hash of request_id and project_api_token. addHeader('X-Webhook-Secret', sha1($request_id . $project_api_token));

Event-Based Webhooks

If event-based webhook is failed to deliver we will retry it for the next 24 hours. Retry time is increased each time request is made: 5, 15, 30 and 60 minutes. (60 minutes is a maximum timeout.)

Pre-Flight Webhooks

To make API easier to integrate with other systems we can synchronously pass all requested data and all available metadata to endpoint specified in a Dashboard.

Pre-Flight requests doesn’t retry over time, of configured endpoint is not responding for 15 seconds we will return HTTP 412 Precondition Failed header.

Request webhooks is sent in same order they was created. You can re-order them in your Dasbhoard.

Service will act differently based on HTTP response of this endpoint:

oAuth HTTP Code Action
2XX Continue processing your request.
301, 307, 308 Follow the redirect. And continue checking response code.
401, 402, 4XX Return same HTTP code
5XX, timeout and all other codes Return HTTP 412 Precondition Failed

This webhooks is extremely useful for antifraud purposes, when another system needs to validate transaction before they are completed.

Modifying original request headers

Pre-Flight Webhooks can modify original request, for example to exchange oAuth user token to an Project token. This allow you to use our API directly from client devices and don’t expose secret API tokens.

To modify request simply return header in following format: X-Override-<Header>.

X-Override-Authorization: Token 3kdjd9d0uds080ujdsj38

Also you can add and save your internal request ID to a webhooks log by using same technique.

X-Override-External-Request-ID: 8383729938

You can modify following HTTP headers:

List all Webhooks

Add a Webhook

Requests

All incoming requests is logged for your convenience. You can use them for debug or to review as a security log.

Request include, but not limited to: HTTP headers, last 4 digits of access token, all request fields, all response fields.

Outgoing requests is listed in Webhooks section.

All requests are stored and returned in popular HTTP Archive (HAR) format with one difference, we use snake_case instead of CamelCase as field keys.

List all Requests

SQL Queries

Internally we use MongoDB, but we known that almost every developer knows SQL, and it can be more convenient way for you to get analytical data trough SQL query.

For this purpose we replicate all data into PostgreSQL DB, and give you access to perform any kind of SQL read queries.

Limits

(TODO: How to share perfomance for each user?) (TODO: How to limit query complicity?)

(Idea) Each second our DB is handling your request will exceed your rate limit by 50 requests. (But we need to guarantee performance in this case.)