# Receive callbacks

Outgoing APIs are callbacks **you receive from Nabla** when providers export content from the app. Expose a public HTTPS endpoint that accepts `POST` requests, and configure its URL in [Nabla Connect Admin](https://app.nabla.com/admin/nabla-connect).

Payload schemas live in the [callback reference](/connect/reference/callback.md).

warning

Respond quickly. Nabla waits for your response before showing a success message in the app. A slow or failed response surfaces an error to the provider.

## Verify HMAC signatures[​](#verify-hmac-signatures "Direct link to Verify HMAC signatures")

Every callback includes:

* `x-nabla-connect-timestamp` — ISO 8601 timestamp of when the request was generated
* `x-nabla-connect-signature` — one or more HMAC-SHA256 signatures of the request body

Retrieve the **signature secret** from [Nabla Connect Admin](https://app.nabla.com/admin/nabla-connect).

On each `POST`:

1. Extract the timestamp and signature headers.
2. **Reject** if the timestamp is older than 60 seconds (replay protection).
3. Concatenate the timestamp and the **raw** request body.
4. Compute HMAC-SHA256 with the shared signature secret.
5. Compare your digest with each comma-separated signature in the header.
6. **Reject with HTTP 401** if none match.
7. **Reject** if you have already processed this callback's `request_uuid` (idempotency).

```
const bodyParser = require("body-parser");

const crypto = require("crypto");

const express = require("express");



const app = express();



app.use(

  bodyParser.json({

    type: "application/json",

    verify: function (req, res, buf) {

      const webhookSecretKey = "<SIGNATURE_SECRET>";

      const timestamp = req.headers["x-nabla-connect-timestamp"];

      const receivedSignatures = req.headers["x-nabla-connect-signature"];



      const computedSignature = crypto

        .createHmac("sha256", webhookSecretKey)

        .update(timestamp + buf)

        .digest("hex");



      const signatureMatch = receivedSignatures

        .split(",")

        .some((sig) => sig.trim() === computedSignature);



      if (!signatureMatch) {

        const error = new Error("Signature invalid");

        error.status = 401;

        throw error;

      }

    },

  })

);
```

Support **multiple** signatures in the header so key rotation can overlap. See [RFC 2104](https://datatracker.ietf.org/doc/html/rfc2104) and [RFC 4231](https://datatracker.ietf.org/doc/html/rfc4231).

## Optional OAuth on callbacks[​](#optional-oauth-on-callbacks "Direct link to Optional OAuth on callbacks")

Nabla can also send a bearer access token on each callback, using the [OAuth 2.0 client credentials](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4) grant against **your** authorization server. Configure this under Callback URL in [Nabla Connect Admin](https://app.nabla.com/admin/nabla-connect).

Nabla caches and reuses access tokens; it does not request a new token for every callback.

| Field                         | Description                            |
| ----------------------------- | -------------------------------------- |
| Authorization server endpoint | URL where Nabla requests access tokens |
| Authentication method         | How credentials are sent (see below)   |
| Client ID                     | Sent as `client_id`                    |
| Client secret                 | Sent as `client_secret`                |

1. **Form URL-encoded** (`application/x-www-form-urlencoded`) — default, matching [RFC 6749 §4.4.2](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4.2):

   ```
   POST /oauth/token

   Content-Type: application/x-www-form-urlencoded



   grant_type=client_credentials&client_id=xxx&client_secret=yyy
   ```

2. **JSON body** (`application/json`):

   ```
   {

     "grant_type": "client_credentials",

     "client_id": "xxx",

     "client_secret": "yyy"

   }
   ```

## Response protocol[​](#response-protocol "Direct link to Response protocol")

All callbacks share the same envelope. The `type` field selects the payload shape.

```
{

  "request_uuid": "<uuid>",

  "type": "NOTE_EXPORT | PATIENT_INSTRUCTIONS_EXPORT",

  "data": {}

}
```

| Status | When to use                      |
| ------ | -------------------------------- |
| `200`  | Request recognized and processed |
| `400`  | Unrecognized `type`              |
| `401`  | Signature verification failed    |

For every `200`, echo `request_uuid`:

```
{ "request_uuid": "<uuid>" }
```

## NOTE\_EXPORT[​](#note_export "Direct link to NOTE_EXPORT")

Triggered when the provider clicks **Export Note**.

Read each section's `structured_content`, discriminated by `type`:

* `{ "type": "TEXT", "text": "..." }` — most sections
* `{ "type": "SUBSECTIONS", "subsections": [ ... ] }` — Assessment & Plan on A\&P-merged templates, one subsection per problem, in note order

Each subsection has `title` (may be `null`, for example content inserted from a dot phrase), `text`, and `icd10_codes` / `snomed_codes`. Those code lists are empty when codes are not available at export time (for example when code extraction is disabled).

note

Section-level `content` is deprecated: it is the same text, flattened. Use `structured_content` instead.

Optional fields, when enabled or available:

* `visit_diagnoses` — ICD-10 items with `system`, `code`, `display`, `is_hcc`, `is_mcc`
* `transcript` — conversation items (`text`, `speaker_type`, `locale`, `start_offset_ms`, `end_offset_ms`). Enable this in Admin.

Section `category` values include `CHIEF_COMPLAINT`, `HISTORY_OF_PRESENT_ILLNESS`, `PAST_HISTORY`, `CURRENT_MEDICATIONS`, `VITALS`, `IMMUNIZATIONS`, `ASSESSMENT_AND_PLAN`, `EXAMINATION`, `RESULTS`, `PRESCRIPTIONS`, and `APPOINTMENTS`. Several sections can share a category (for example separate Assessment and Plan sections both use `ASSESSMENT_AND_PLAN`). `category: null` means the section does not fit an existing category.

See the [callback reference](/connect/reference/callback.md) for the full schema.

## PATIENT\_INSTRUCTIONS\_EXPORT[​](#patient_instructions_export "Direct link to PATIENT_INSTRUCTIONS_EXPORT")

Triggered when the provider clicks **Export patient instructions**. `data.patient_instructions.instructions` is the patient-facing text.
