Introduction
The HAPI Hub API is organized around REST. Our API 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). JSON is returned by all API responses.
To make the API as explorable as possible, accounts have a test and a live mode, differentiated by API key. Requests made with test mode credentials never hit real organization data and instead provide a response from your very own sandbox facility, called St. Mungo's Clinic.
Get Started
It's free and simple to get started: Simply create an account, and sign in to the HAPI Hub Develper Console to retrieve your test API key and the organization ID of your sandbox facility (called St. Mungo's Clinic).
Simply create a free account. Sign in to the HAPI Hub Developer Console. Make sure you're in Test mode. Pick up your test API key and the organization ID for your very own sandbox clinic (St. Mungo's Clinic), complete with pre-loaded data similar to a real medical facility.
API keys and access
Manage your API keys to authenticate requests with HAPI Hub.
HAPI Hub authenticates your API requests using your account’s API keys. If you do not include your key when making an API request, or use one that is incorrect or outdated, HAPI Hub returns an error.
Every account is provided with separate keys for testing and for running live transactions. All API requests exist in either test or live mode, and objects—customers, plans, coupons, and so forth—in one mode cannot be manipulated by objects in the other.
There are also two types of API keys: publishable and secret.
Publishable API keys are meant solely to identify your account with HAPI Hub, they aren’t secret. In other words, they can safely be published in places like your HAPI Hub.js JavaScript code, or in an Android or iPhone app. Publishable keys only have the power to create tokens.
Secret API keys should be kept confidential and only stored on your own servers. Your account’s secret API key can perform any API request to HAPI Hub without restriction.
Each account has a total of four keys: a publishable and secret key pair for test mode and live mode.
Obtaining your API keys
Your API keys are always available in the Dashboard
Use only your test API keys for testing and development. This ensures that you don't accidentally modify your live customers or charges.
If you don’t have an administrator or developer role, you may not have access to view your API keys in the Dashboard. Contact your HAPI Hub account’s owner and ask to be added to their team as a developer.
Test and live modes
The test and live modes function almost identically, with a few necessary differences:
In test mode, payments are not processed by card networks or payment providers, and only our test payment information can be used.
Some payment methods using Sources have a more nuanced flow in live mode, with more steps required than those in test mode.
Disputes also have a more nuanced flow in live mode, and a simpler testing process.
Webhooks that were not successfully acknowledged are retried three times over a few hours (as opposed to 72 hours for live mode).
You can view test data by toggling the Dashboard's Viewing test data option.
Keeping your keys safe
Your secret API key can be used to make any API call on behalf of your account, such as creating charges or performing refunds. Treat your secret API key as you would any other password. Grant access only to those who need it. Ensure it is kept out of any version control system you may be using. Control access to your key using a password manager or secrets management service.
In live mode, new secret keys are only visible the first time you access them. After that, the Dashboard redacts the API key. When the key is revealed, you can leave a note on the Dashboard describing the location on your own systems where you’ve copied it. If you lose your secret key, you can’t recover it from the Dashboard and must roll the key or create another one.
A view of the Dashboard displaying a note for a redacted secret key. The note reads 'Notes are a great place to link to a password manager or a secret store.'
Keys created prior to the introduction of this feature are not automatically hidden when they are revealed, but can be hidden manually.
Rolling keys
If an API key is compromised, roll the key in the Dashboard to block it and generate a new one.
When rolling an API key, you can choose to block the old key immediately or allow it to work for 12 hours, providing you with time to make the transitions. In either case, the new key can be used immediately.
Limiting access with restricted API keys
Your account’s secret API keys can be used to perform any API request without restriction. For greater security, you can create restricted API keys that limit access to, and permissions for, different areas of your account data. These take the place of your secret API key and should be used if you’re working with microservices that interact with the HAPI Hub API on your behalf.
A restricted key allows only the minimum level of access that the service needs while protecting account data it doesn’t need. For example, you can create a restricted key that grants read-only access to dispute data, then use it with a dispute monitoring service.
If you no longer need a restricted key (or you suspect it has been compromised), you can revoke it at any time. A restricted key can also be rolled, or edited to change its level of access.
Restricted keys are intended to reduce the risk when using or building microservices. They are not to be used as an alternative to your account’s API keys during development of your HAPI Hub integration—restricted keys cannot interact with many parts of HAPI Hub’s API. Use your test API keys during development and live API keys once your integration is live.
Authentication
The HAPI Hub API requires you to use Bearer Token-based authentication to access the API.
Every endpoint in the HAPI Hub API comes with two API keys (tokens):
- a Test mode API key to access your sandbox organization.
- a Live mode API key to access all the organizations you are associated to.
With every API request, you'll need to send the following request header:
# With shell, you can just pass the correct header with each request
curl "https://api.hapihub.com/patients" \
-H "Authorization: Bearer meowmeowmeow"
import sdk from '@hapihub/sdk-js';
sdk.initialize('meowmeowmeow');
Make sure to replace
meowmeowmeow
with your API key.
Your Live mode API key is extremely sensitive information. Make sure to familiarise yourself with security best practices around storing and using API secrets. Make sure to never hard-code the API key into source control systems like Git, but extract this information into environment variables instead.
If your API key has been compromised at any point, please Regenerate your secret key immediately
Caveats
Dates
The dates returned by the API are all represented in Unix time in millis.
Most dates will be returned in UTC timestamp.
Where dates are passed in as query parameters they should be in the form Unix timestamp.
Prices and Currencies
All prices returned by the API are represented as integer amounts in a currency’s smallest unit. For example, $5 USD would be returned as 500 (i.e, 500 cents).
For zero-decimal currencies, amounts will still be provided as an integer but without the need to divide by 100. For example, an amount of ¥5 (JPY) would be returned as 5.
All currency codes conform to ISO 4217.
Resource IDs
All resources provide an ID through the id property. These are typically the unique identifier as they're handled in the system.
Country Codes
All country codes will be returned as ISO 3166-1 alpha-3. The same format should be used when providing values for writes.
Errors
The HAPI Hub API uses standard HTTP error and status codes to communicate errors. To provide additional context, you'll se an additional JSON response body which includes message and type properties with further information.
{
"name": "NOT_FOUND_ERROR",
"event": "678c54b5-eb23-4bc7-9410-4b720f22e3c0",
"message": "Not found."
}
The message property contains an explanation specific to that error. This is the property that is most useful to a human.
The type property can be thought of as an error code. They provide HAPI Hub specific error types in addition to the HTTP status code.
The eventId property is a reference to this specific error instance. Please include this information when contacting our support team, as it helps to identify and diagnose the error.
Types of Errors
Here are listed the possible error types:
Value | Description |
---|---|
AUTH_ERROR | Usually accompanied by a 401 status code. Incorrect or invalid authentication details were provided. |
UNKNOWN_ERROR | Usually accompanied by a 500 status code. Something went wrong internally. This is usually a bug - contact us if the problem persists. |
INVALID_PARAMETERS | Usually accompanied by a 400 status code. Some given input was not in the correct format or was incorrect. Where possible the message will provide further information. |
INVALID_CONTENT_TYPE | Usually accompanied by a 415 status code. The request was rejected because it has the wrong Content-Type header. |
NOT_FOUND | Usually accompanied by a 404 status code. Not found, or somehow a necessary piece of data is missing. |
PERMISSIONS_ERROR | Usually accompanied by a 403 status code. The auth details are valid, but don't have the credentials to do the attempted action. |
CREDENTIALS_ERROR | Usually accompanied by a 403 status code. The request was rejected by the target clinic. |
METHOD_NOT_IMPLEMENTED | Usually accompanied by a 404 status code. See Unsupported API Endpoints. |
METHOD_NOT_SUPPORTED | Usually accompanied by a 404 status code. See Unsupported API Endpoints. |
SECURITY_ERROR | Usually accompanied by a 426 status code. The request was rejected based on security concerns like lack of HTTPS. |
RATE_LIMIT_EXCEEDED | Usually accompanied by a 429 status code. The request was rejected as you are making too many requests too quickly. |
Unsupported API Endpoints
Value | Description |
---|---|
METHOD_NOT_IMPLEMENTED | Indicates that the attempted functionality is currently not supported by HAPI Hub. The underlying PMS supports this functionality but we haven't implemented it in the HAPI Hub API yet. If you need a feature that we currently don't support, please let us know by contacting hello@hapihub.com. |
METHOD_NOT_SUPPORTED | Indicates that the attempted functionality is not available for the PMS. The PMS simply doesn't provide the functionality necessary for this feature to be supported. |
Querying
Equality
All fields that do not contain special query parameters are compared directly for equality.
// Find all unread messages in room #2
app.service('messages').find({
query: {
read: false,
room: 2
}
});
GET /messages?read=false&roomId=2
$sort
$sort
will sort based on the object you provide. It can contain a list of properties by which to sort mapped to the order (1 ascending, -1 descending).
// Find the 10 newest messages
app.service('messages').find({
query: {
$limit: 10,
$sort: {
createdAt: -1
}
}
});
/messages?$limit=10&$sort[createdAt]=-1
$in, $nin
Find all records where the property does ($in
) or does not ($nin
) match any of the given values.
// Find all messages in room 2 or 5
app.service('messages').find({
query: {
roomId: {
$in: [ 2, 5 ]
}
}
});
/messages?roomId[$in]=2&roomId[$in]=5
$lt, $lte
Find all records where the value is less ($lt
) or less and equal ($lte
) to a given value.
// Find all messages older than a day
const DAY_MS = 24 * 60 * 60 * 1000;
app.service('messages').find({
query: {
createdAt: {
$lt: new Date().getTime() - DAY_MS
}
}
});
/messages?createdAt[$lt]=1479664146607
$gt, $gte
Find all records where the value is more ($gt
) or more and equal ($gte
) to a given value.
// Find all messages within the last day
const DAY_MS = 24 * 60 * 60 * 1000;
app.service('messages').find({
query: {
createdAt: {
$gt: new Date().getTime() - DAY_MS
}
}
});
/messages?createdAt[$gt]=1479664146607
$ne
Find all records that do not equal the given property value.
// Find all messages that are not marked as archived
app.service('messages').find({
query: {
archived: {
$ne: true
}
}
});
/messages?archived[$ne]=true
$exists
Find all records where the field's existense exists (or dont).
// Find all messages where the archived field is set (has value even if falsy)
app.service('messages').find({
query: {
archived: {
$exists: true
}
}
});
/messages?archived[$exists]=true
$or
Find all records that match any of the given criteria.
// Find all messages that are not marked as archived
// or any message from room 2
app.service('messages').find({
query: {
$or: [
{ archived: { $ne: true } },
{ roomId: 2 }
]
}
});
/messages?$or[0][archived][$ne]=true&$or[1][roomId]=2
$and
Find all records that match all of the given criteria.
// Find all messages that are not marked as archived
// and any message from room 2
app.service('messages').find({
query: {
$and: [
{ archived: { $ne: true } },
{ roomId: 2 }
]
}
});
/messages?$and[0][archived][$ne]=true&$or[1][roomId]=2
Searching
$search
In selected services, fuzzy search can be performed in selected fields
// find documents with title and messages containing 'cat'
// will find titles including 'cat', 'cats', etc.
app.service('messages').find({
query: {
$search: 'cat'
}
})
// find documents with title and messages containing 'cat'
// will find titles including 'cat', 'cats', etc.
app.service('messages').find({
query: {
$search: {
text: 'cat',
// default values
caseSensitive: false,
language: null, // two-letter ISO 639-1 language code for determining list of stop words. eg en, da, nl, ru, es
diacriticSensitive: false, // flag to enable or disable diacritic sensitive search (respect accents: à â ç é è ê ë î ï ô û ù ü ÿ)
sortByRelevance: false, // sort result by relevance
},
}
})
/messages?$search=cat
/messages?$search[text]=cat&$search[caseSensistive]=false&$search[language]=null&$search[diacriticSensitive]=false&$search[sortByRelevance]=false
Exact phrase search
You can also search for exact phrases by wrapping them in double-quotes. If the $search string includes a phrase and individual terms, text search will only match documents that include the phrase.
// find documents containing “coffee shop”
app.service('messages').find({
query: {
$search: '"coffee shop"'
}
})
Term Exclusion
To exclude a word, you can prepend a “-” character
// find documents containing "java" or "shop" but not "cofee"
app.service('messages').find({
query: {
$search: 'java shop -coffee'
}
})
Pagination
$limit
$limit
will return only the number of results you specify:
// Retrieves the first two unread messages
app.service('messages').find({
query: {
$limit: 2,
read: false
}
});
/messages?$limit=2&read=false
Pro tip:
With pagination enabled, to just get the number of available records set $limit to 0. This will only run a (fast) counting query against the database and return a page object with the total and an empty data array.
$skip
$skip
will skip the specified number of results:
// Retrieves the next two unread messages
app.service('messages').find({
query: {
$limit: 2,
$skip: 2,
read: false
}
});
/messages?$limit=2&$skip=2&read=false
$total
Responses that are list returns a object with pagination metadata (skip/offset, limit), to also get the total (expensive query), a $total
flag must be enabled. Total is automatically set to true if $limit
is set to 0
// Retrieves the first two unread messages
app.service('messages').find({
query: {
$limit: 2,
read: false
}
});
// returns
{
data: [...documents here],
total: undefined,
skip: undefined,
limit: 2,
}
// Retrieves the first two unread messages (with total)
app.service('messages').find({
query: {
$limit: 2,
$total: true,
read: false
}
});
// returns
{
data: [...documents here],
total: 5,
skip: undefined,
limit: 2,
}
Projection
$select
$select
allows to pick which fields to include in the result. This will work for any service method.
// Only return the `text` and `userId` field in a message
app.service('messages').find({
query: {
$select: [ 'text', 'userId' ]
}
});
app.service('messages').get(1, {
query: {
$select: [ 'text' ]
}
});
/messages?$select[]=text&$select[]=userId
/messages/1?$select[]=text
Webhooks
You can register webhook URLs for HAPI Hub to notify you any time an event happens in your account. When the event occurs—a successful charge is made on a customer’s subscription, a transfer is paid, your account is updated, etc.—HAPI Hub creates an Event object.
This Event object contains all the relevant information about what just happened, including the type of event and the data associated with that event. HAPI Hub then makes an HTTP POST request to send the Event object to any endpoint URLs defined. (You define them in your account’s Webhooks settings.) You can have HAPI Hub send a single event to multiple webhook endpoints.
Create a webhook endpoint
Webhook data is sent as JSON in the POST request body. The full event details are included and can be used directly, after parsing the JSON into an Event object.
Creating a webhook endpoint on your server is no different from creating any page on your website. With PHP, you might create a new .php file on your server; with a Ruby framework like Sinatra, you would add a new route with the desired URL.
// This example uses Express to receive webhooks
const app = require('express')();
// Use body-parser to retrieve the raw body as a buffer
const bodyParser = require('body-parser');
// Match the raw body to content type application/json
app.post('/webhook', bodyParser.raw({type: 'application/json'}), (request, response) => {
let event;
try {
event = JSON.parse(request.body);
} catch (err) {
response.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle the event
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object;
// Then define and call a method to handle the successful payment intent.
// handlePaymentIntentSucceeded(paymentIntent);
break;
case 'payment_method.attached':
const paymentMethod = event.data.object;
// Then define and call a method to handle the successful attachment of a PaymentMethod.
// handlePaymentMethodAttached(paymentMethod);
break;
// ... handle other event types
default:
// Unexpected event type
return response.status(400).end();
}
// Return a response to acknowledge receipt of the event
response.json({received: true});
});
app.listen(8000, () => console.log('Running on port 8000'));
Checking webhook signatures
HAPI Hub can optionally sign the webhook events it sends to your endpoints. We do so by including a signature in each event’s HAPI Hub-Signature header. This allows you to verify that the events were sent by HAPI Hub, not by a third party. You can verify signatures either using our official libraries, or manually using your own solution.
Before you can verify signatures, you need to retrieve your endpoint’s secret from your Dashboard’s Webhooks settings. Select an endpoint that you want to obtain the secret for, then select the Click to reveal button.
Each secret is unique to the endpoint to which it corresponds. If you use the same endpoint for both test and live API keys, note that the secret is different for each one. Additionally, if you use multiple endpoints, you must obtain a secret for each one. After this setup, HAPI Hub starts to sign each webhook it sends to the endpoint.
Verifying signatures manually
HAPI Hub generates signatures using a hash-based message authentication code (HMAC) with SHA-256
- Prepare the signed_payload string
The actual JSON payload (i.e., the request’s body)
Determine the expected signature Compute an HMAC with the SHA256 hash function. Use the endpoint’s signing secret as the key, and use the signed_payload string as the message.
Compare signatures Compare the signature(s) in the header to the expected signature. If a signature matches, compute the difference between the current timestamp and the received timestamp, then decide if the difference is within your tolerance.
To protect against timing attacks, use a constant-time string comparison to compare the expected signature to each of the received signatures.
Preventing replay attacks
A replay attack is when an attacker intercepts a valid payload and its signature, then re-transmits them. To mitigate such attacks, HAPI Hub includes a timestamp in the HAPI Hub-Signature header. Because this timestamp is part of the signed payload, it is also verified by the signature, so an attacker cannot change the timestamp without invalidating the signature. If the signature is valid but the timestamp is too old, you can have your application reject the payload.
Our libraries have a default tolerance of five minutes between the timestamp and the current time. You can change this tolerance by providing an additional parameter when verifying signatures. We recommend that you use Network Time Protocol (NTP) to ensure that your server’s clock is accurate and synchronizes with the time on HAPI Hub’s servers.
HAPI Hub generates the timestamp and signature each time we send an event to your endpoint. If HAPI Hub retries an event (e.g., your endpoint previously replied with a non-2xx status code), then we generate a new signature and timestamp for the new delivery attempt.
Best Practices
Event handling
Handling webhook events correctly is crucial to making sure your integration’s business logic works as expected.
Acknowledge events immediately
If your webhook script performs complex logic, or makes network calls, it’s possible that the script would time out before HAPI Hub sees its complete execution. Ideally, your webhook handler code (acknowledging receipt of an event by returning a 2xx status code) is separate of any other logic you do for that event.
Handle duplicate events
Webhook endpoints might occasionally receive the same event more than once. We advise you to guard against duplicated event receipts by making your event processing idempotent. One way of doing this is logging the events you’ve processed, and then not processing already-logged events.
Order of events
HAPI Hub does not guarantee delivery of events in the order in which they are generated. For example, creating a subscription might generate the following events:
customer.subscription.created invoice.created invoice.payment_succeeded charge.created (if there’s a charge) Your endpoint should not expect delivery of these events in this order and should handle this accordingly. You can also use the API to fetch any missing objects (e.g., you can fetch the invoice, charge, and subscription objects using the information from invoice.payment_succeeded if you happen to receive this event first).
Security
Keeping your endpoints secure is critical to protecting your customers’ information. HAPI Hub provides several ways for you to verify events are coming from HAPI Hub in a secure manner.
Receive events with an HTTPS server
If you use an HTTPS URL for your webhook endpoint, HAPI Hub will validate that the connection to your server is secure before sending your webhook data. For this to work, your server must be correctly configured to support HTTPS with a valid server certificate.
Roll endpoint secrets
If you need to change the secret used for verifying events come from HAPI Hub, you can do so in the Webhooks section in the Dashboard. For each endpoint, click Roll secret and you can choose to immediately expire the current secret, or delay for up to 24 hours to provide time to update the verification code on your server. During this time, multiple secrets are active for the endpoint and HAPI Hub generates one signature for each secret until expiration.
Verify events are sent from HAPI Hub
Verify webhook signatures to confirm that received events are sent from HAPI Hub. Additionally, HAPI Hub sends webhook events from a set list of IP addresses. Only trust events coming from these IP addresses.
Rate Limiting
Learn about API rate limits and how to work with them.
The HAPI Hub API employs a number of safeguards against bursts of incoming traffic to help maximize its stability. Users who send many requests in quick succession may see error responses that show up as status code 429. We have several limiters in the API, including:
- A rate limiter that limits the number of requests received by the API within any given second. We generally allow up to 100 read operations per second and 100 write operations per second in live mode, and 25 operations per second for each in test mode, though this may change at any time.
Please treat these limits as maximums and don't generate unnecessary load. We may adjust limits to prevent abuse or enable well-behaved high-traffic applications. See Handling limiting gracefully for advice on handling 429s.
- A concurrency limiter that limits the number of requests that are active at any given time. Problems with this limiter are less common compared to the request rate limiter, but it's more likely to result in resource-intensive, long-lived requests.
Common causes and mitigations
Rate limiting can occur under a variety of conditions, but it's most common in these scenarios:
Running a large volume of closely-spaced requests can lead to rate limiting. Often this is part of an analytical or migration operation. When engaging in these activities, you should try to control request rate on the client side (see Handling limiting gracefully).
Issuing many long-lived requests can trigger limiting. Requests vary in the amount of HAPI Hub's server resources they use, and more resource-intensive requests tend to take longer and run the risk of causing new requests to be shed by the concurrency limiter. Resource requirements vary widely, but list requests and requests that include expansions generally use more resources and take longer to run. We suggest profiling the duration of HAPI Hub API requests and watching for timeouts to try and spot those which are are unexpectedly slow.
A sudden increase in charge volume like a flash sale might result in rate limiting. We try to set our rates high enough that the vast majority of users will never be rate limited for legitimate payment traffic, but if you suspect that an upcoming event might push you over the limits listed above, please reach out to support.
Handling limiting gracefully
A basic technique for integrations to gracefully handle limiting is to watch for 429 status codes and build in a retry mechanism. The retry mechanism should follow an exponential backoff schedule to reduce request volume when necessary. We'd also recommend building some randomness into the backoff schedule to avoid a thundering herd effect.
Individual requests can only be optimized to a limited degree, so an even more sophisticated approach would be to control traffic to HAPI Hub at a global level, and throttle it back if substantial rate limiting is detected. A common technique for controlling rate is to implement something like a token bucket rate limiting algorithm on the client-side. Ready-made and mature implementations for token bucket are available in almost any programming language.
Object lock timeouts
Integrations may encounter errors with HTTP status 429, code lock_timeout, and this message:
This object cannot be accessed right now because another API request or HAPI Hub process currently accessing it. If you see this error intermittently, retry the request. If you see this error frequently and are making multiple concurrent requests to a single object, make your requests serially or at a lower rate.
The HAPI Hub API locks objects on some operations so that concurrent workloads don't interfere and produce an inconsistent result. The error above is caused by a request trying to acquire a lock that's already held elsewhere, and timing out after it couldn't be acquired in time.
Lock timeouts have a different cause than rate limiting, but their mitigations are similar. Like with rate limiting errors, we recommend retrying on an exponential backoff schedule (see Handling limiting gracefully).
Lock contention is caused by concurrent access on related objects. Integrations can vastly reduce it by making sure that mutations on the same object are queued up and run sequentially instead. Concurrent operations against the API are still okay, but try to make sure simultaneous operations operate only on unique objects. It's also possible to see lock contention caused by a conflict with an internal HAPI Hub background process' this should be rare, but because it's beyond user control, we recommend that all integrations are able to retry requests.
Load testing
It's common for users to prepare for a major sales event by load testing their systems, with the HAPI Hub API running in test mode as part of it. We generally discourage this practice because API limits are lower in test mode, so the load test is likely to hit limits that it wouldn't hit in production. Test mode is also not a perfect stand-in for live API calls, and that can be subtly misleading. For example, creating a charge in live mode sends a request to a payment gateway while that request will be mocked in test mode, resulting in significantly different latency profiles.
As an alternative, we recommend building integrations so that they have a configurable system for mocking out requests to HAPI Hub API, which can be enabled for load tests. For realistic results, they should simulate latency by sleeping for a time that is determined by sampling the durations of real live mode HAPI Hub API calls as seen from the perspective of the integration.
Libraries
Official
HAPI Hub offers official API libraries for different programming languages, which are regularly updated for breaking and non-breaking API changes.
Examples
We've built a set of Quickstart and Example applications to get you building your HAPI Hub integration fast and with ease.
Community
Explore some of the community-supported libraries available for HAPI Hub listed below. If you built your own library, please reach out to our Support team to add it to this list!