NAV Navbar
shell javascript

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.

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:

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):

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

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

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

  1. Prepare the signed_payload string

The actual JSON payload (i.e., the request’s body)

  1. 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.

  2. 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:

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.

Common causes and mitigations

Rate limiting can occur under a variety of conditions, but it's most common in these scenarios:

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!