TODO, take in account:
- http://developer.kontomatik.com/api-doc/#best-practices
- https://getmondo.co.uk/docs/#authorization-code-grant
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:
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.
Basic. Includes all features starter plan and rate limit of 5000 requests per 15 minutes.
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:
HEAD
- Can be issued against any resource to get just the HTTP header info.GET
- Read resources.POST
- Create new resources.PUT
- Replace resources (basically field values) or collections.DELETE
- Remove resources.
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 of the requested resource (url
); requested resource type (type
); current status (code
); optional error description (error
); idempotency key (idempotency_id
); request id (request_id
).urgent
- Notifications and counters.data
- Requested resource.paging
- Pagination data.sandbox
- Optional data provided bysandbox
environment, for development purposes.
{
"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:
header
- Response HTTP header.request
- Response JSON object.field
- Response field.
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 |
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:
limit
(optional) - A limit on the number of objects to be returned, between 1 and 100. Default: 50;starting_after
(optional) - A cursor for use in pagination.starting_after
is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending withobj_foo
, your subsequent call can includestarting_after=obj_foo
in order to fetch the next page of the list;ending_before
(optional) - A cursor for use in pagination.ending_before
is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, starting withobj_bar
, your subsequent call can includeending_before=obj_bar
in order to fetch the previous page of the list.
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:
application/json
- Response is sent in a JSON format.text/csv
- Response is trimmed to a requested resource and sent in a CSV format.
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:
ascending_chronological
- Ascending chronological order.reverse_chronological
- Descending chronological order.
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:
field
- Resource filed that should be used for current filter predicate.comparison
- Comparison type that is applied to a field value andvalue
predicate property.value
- Value that should be compared with resource field value.
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:
aggregate
- Base64 encoded JSON string with a aggregation rules.
Also you can change aggregation dispensation by providing tick
query parameter:
tick
- Aggregation period particle. Default value:day
. Optional.
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:
name
- Property name in a response aggregate object.strategy
- Aggregation strategy.field
- Field that will be used for aggregation.
{
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:
- Internal order ID.
- Customers name and email address.
Metadata field supports key-value pairs with the following limitations:
- Up to 24 keys.
- Up to 100 characters for the key (alphanumeric characters, hyphens and underscores).
- Up to 500 characters for the value.
- String, integer, decimals and boolean values only. All other types will be converted into string.
Key objects
We have list of key objects that is accessible trough our API:
Accounts
- Represents a customer object with a balance.Fundings
- Represents fundings into system. All top-ups is grouped in this list, to have all money income in a single place.Transfers
- Represents all charges and transfer operations in system. (Move of money from one account into another.)Holds
- Represents all holds on account balances. Hold is a similar ofTransfers
, with key difference: hold decreases available balance for anAccount
, but doesn’t really decrease balance itself.Hold
can be declined or turned intoTransfer
.Webhooks
- List of all webhooks and their historical data.Events
- List of all events that was created in our API.Requests
- List of all incoming requests to the API.Currencies
- List of custom currencies.Settings
- List of settings for APIProject
.
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
- Save request data.
- Run any pre-flight webhooks, for example for customer authentication. And if webhooks returned 2XX codes..
- Check authentication. And if authorized..
- Check rate limits for your application. And if they not exceeded..
- Validate request data and complete request. 4.1. Generate event for this request. 4.2. Queue money flow (all operations with money is asynchronous).
- Return API response
- Save API response to
Requests
- Schedule all webhooks configured for a
Project
. If webhook failed, re-try it. - 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:
- Your application requests API with
Authorization: Bearer <token>
header. - Pre-flight webhook is send to your oAuth endpoint.
- oAuth endpoint validates Bearer token and if its valid returns
HTTP 200
status code with aX-Override-Authorization: Token <project_token>
header, where<project_token>
is a API project token issued in Dashboard. - 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:
301
- Permanent redirection. The URI you used to make the request has been superseded by the one specified in the Location header field. This and all future requests to this resource should be directed to the new URI.302, 307
- Temporary redirection. The request should be repeated verbatim to the URI specified in the Location header field but clients should continue to use the original URI for future requests.
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:
transfer
- List of Transfers that should be created when Hold is completed into a Transfer. This allows you to take fees from your customers.total
- Total amount should be holded from an Account. Total should be exactly same as sum of all created transfers. (Otherwise we will return an appropriate error.)
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:
transfer
- List of Transfers that should be created in a single Transfer. This allows you to take fees from your customers.total
- Total amount of created transfers. Total should be exactly same as sum of all created transfers. (Otherwise we will return an appropriate error.)
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:
- Authorization
- Timezone
- X-Urgent-Account-ID
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.)