# Document Synchronization

Learn how to integrate your practice management system with Askara to receive and synchronize validated medical documents automatically.

## Overview

When a healthcare professional validates a document in Askara, your system can receive the document and import it into your database. This guide explains the integration flow and available synchronization methods.

## Integration Flow

1. **Receive notification** - Via webhook (push) or polling (pull)
2. **Download document** - Get PDF and metadata via API
3. **Import to your system** - Save document and update your database
4. **Confirm status** - Report success or failure back to Askara


## Synchronization Methods

### Option 1: Webhooks (Recommended)

Webhooks provide real-time notifications when documents are ready. Askara sends an HTTP POST request to your endpoint.

**Advantages:**

- Real-time synchronization
- No polling overhead
- Efficient resource usage


#### Webhook Event

Subscribe to the `new_document` event. Askara sends it:

- when a healthcare professional validates a document;
- each time a new synchronization of that document is requested, for example when the healthcare professional asks for it again from Askara.


The same document can therefore reach you several times: use the document `uuid` to detect documents you have already imported.

The API also accepts a `document_synchronize` subscription, but Askara never sends that event. Do not rely on it: a webhook subscribed only to `document_synchronize` receives nothing.

#### 1. Register Your Webhook

**Endpoint:** `POST /webhooks`

[→ API Reference: Create Webhook](/apis/askara-symfony/askara-api/webhook/api_webhook_create)

```json
{
  "url": "https://your-system.com/api/askara/webhook",
  "subscriptions": "new_document"
}
```

Webhooks are registered per healthcare professional: the user who authorizes your application registers their own webhook, and receives events only for their own documents. Your OAuth2 client needs the `write:webhooks` scope.

**Fields:**

- `url` *(required)* - Public HTTP(S) URL of your endpoint
- `subscriptions` *(required)* - Comma-separated list of events, as a single string. Do not add spaces around the commas: unknown or malformed event names are ignored without an error.


The webhook is enabled on creation. If you register the same `url` again with the same account, Askara adds the new subscriptions to the existing webhook (and re-enables it) instead of creating a second one.

The response contains the webhook `uuid` and its `secret`. Store the `secret`: you need it to verify the signature of every request Askara sends you (see [Verify the Webhook Signature](#verify-the-webhook-signature)).

#### 2. Receive Webhook Events

When an event occurs, Askara sends a POST request to your webhook URL.

**Webhook Payload Structure:**

```json
{
  "events": [{
    "id": "document-uuid",
    "resource_type": "document",
    "action": "new_document",
    "metadata": {
      "document": {
        "uuid": "505fdf3b-e6bf-4e54-8d6f-19c5229d3b17",
        "createdAt": "2026-10-05T12:52:02+00:00",
        "updatedAt": "2026-10-05T12:52:02+00:00",
        "encounteredAt": null,
        "user": "//api.askara.ai/users/6ac32d28-7c31-4542-af58-43dd32d6b051",
        "patient": "//api.askara.ai/patients/cd211374-ed3e-45f5-a864-1e7a69a1f75d",
        "type": "referral_letter",
        "isAnonymized": false,
        "genericContactSpecialty": null
      }
    },
    "links": [],
    "resource_metadata": {
      "user_id": "6ac32d28-7c31-4542-af58-43dd32d6b051",
      "organization_id": "73cf49ad-1f17-442e-be53-8ce0327a6063"
    }
  }],
  "meta": {
    "webhook_id": "your-webhook-uuid"
  }
}
```

The payload is a notification, not the full document. In particular it does **not** contain the PDF or the patient details: `user` and `patient` are references to those resources (the identifier is the last segment). Call `GET /documents/{uuid}` to get the PDF and the patient (see step 3).

**Important:**

- Respond with a `2xx` status within 5 seconds: Askara considers any other answer, or no answer, as a failed delivery
- Askara sends each notification once and does not retry a failed delivery (see [Retry Behavior](#retry-behavior)), so also poll as described below if you cannot afford to miss a document
- Process the document asynchronously (download, import, confirm)


#### Verify the Webhook Signature

Every webhook request carries a `Webhook-Signature` header. Its value is the HMAC-SHA256 of the raw request body, keyed with your webhook `secret`, encoded as a lowercase hexadecimal string (no prefix).

To verify a request:

1. Read the raw request body exactly as received, before any JSON parsing.
2. Compute `HMAC-SHA256(secret, rawBody)` and encode it in hexadecimal.
3. Compare it with the `Webhook-Signature` header using a constant-time comparison. Reject the request if they differ.


```javascript
const crypto = require('crypto');

function isValidAskaraSignature(rawBody, signatureHeader, secret) {
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const received = Buffer.from(signatureHeader || '', 'utf8');
  const computed = Buffer.from(expected, 'utf8');

  return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}
```

The signature covers the body only: it contains no timestamp. Use the document `uuid` to ignore requests you have already processed.

#### 3. Download Document Data

Use the document UUID to fetch the document, including its PDF and its patient.

**Endpoint:** `GET /documents/{uuid}`

[→ API Reference: Get Document](/apis/askara-symfony/askara-api/document/api_document)

The `pdf.url` in the response is pre-signed and valid for a limited time (currently 4 hours). Download the PDF right away, and call the endpoint again to get a fresh URL if it has expired.

### Option 2: Polling

If webhooks aren't feasible, poll periodically for unsynchronized documents.

**Endpoint:** `GET /documents?synchronized=false`

[→ API Reference: List Documents](/apis/askara-symfony/askara-api/document/api_documents)

**Query Parameters:**

- `synchronized=false` - Returns only documents not yet confirmed as synchronized by your client
- `synchronized=true` - Returns only documents confirmed as synchronized by your client


A document modified after you confirmed its synchronization stays in `synchronized=true`. The list is not limited to documents validated by the healthcare professional.

Only the documents of the healthcare professional who authorized your application are returned. The list is paginated (20 documents per page by default).

**Recommended polling interval:** 30-60 seconds

**Response:** List of documents. In the list, `patient`, `pdf` and `user` are references (identifiers), not embedded objects. Call `GET /documents/{uuid}` for each document to get the PDF and the patient.

## Confirming Synchronization Status

**CRITICAL:** Always confirm the synchronization status after processing a document, whether it succeeded or failed.

### Report Success

**Endpoint:** `POST /documents/{uuid}/status`

[→ API Reference: Set Document Status](/apis/askara-symfony/askara-api/document/api_document_status)

```json
{
  "success": true
}
```

**Fields:**

- `success` *(required)* - `true` to mark synchronization as successful


This marks the document as synchronized for your client. If the document is modified afterwards, it needs to be synchronized again.

If Askara receives no confirmation (neither `success: true` nor `success: false`) within an hour after a synchronization, a periodic check records that synchronization as failed with a timeout error. Confirm every document you process, even when you cannot import it.

### Report Failure

Send the same request with `success: false` and provide an error message:

```json
{
  "success": false,
  "error": "Failed to save document: Database connection timeout"
}
```

**Fields:**

- `success` *(required)* - `false` to indicate synchronization failed
- `error` *(optional but recommended)* - Error message describing why synchronization failed (2000 characters maximum)


**Important:** Always provide an `error` message when `success` is `false` to help troubleshoot synchronization issues.

This records the failure with the error message. Healthcare professionals can then retry synchronization manually from Askara's interface.

## Retry Behavior

### Webhook Delivery

Askara tries to deliver each webhook **once**. If your endpoint does not answer within 5 seconds, is unreachable, or answers with a status code outside `2xx`, the delivery is recorded as failed and **Askara does not retry it automatically**.

To make sure you never miss a document:

- keep your endpoint available and answer quickly;
- poll `GET /documents?synchronized=false` periodically (see [Polling](#option-2-polling)) to catch the documents whose notification you missed.


### Document Synchronization Retries

Document synchronization itself is **not retried automatically**. A new synchronization is requested:

- **Manually:** the healthcare professional asks for a new synchronization from Askara. These requests are limited to one every 30 seconds for a given document.
- **After a failure confirmation (`success: false`):** the document stays unsynchronized, and the healthcare professional can request a new synchronization.
- **After a success confirmation (`success: true`):** the document is marked as synchronized and nothing else is needed.


## Required OAuth2 Scopes

To synchronize documents, your OAuth2 client must request these scopes:

- `read:documents` - Retrieve document information
- `write:document_integrations` - Confirm synchronization status
- `write:webhooks` - Register your webhook


[→ OAuth2 Guide](/guides/oauth2)

## Example Implementation

### Node.js Webhook Handler

```javascript
const express = require('express');
const app = express();

// Keep the raw body: the signature is computed on it (see "Verify the Webhook Signature")
app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }));

app.post('/api/askara/webhook', async (req, res) => {
  if (!isValidAskaraSignature(req.rawBody, req.get('Webhook-Signature'), process.env.ASKARA_WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  // Respond quickly to avoid timeout
  res.status(200).send('OK');

  // Process asynchronously
  for (const event of req.body.events) {
    if (event.action !== 'new_document') continue;

    const documentUuid = event.metadata.document.uuid;

    // Access token of the healthcare professional who owns this webhook,
    // from your own storage (see the Refresh Token Flow guide)
    const accessToken = await getAccessToken(event.resource_metadata.user_id);
    const headers = { 'Authorization': `Bearer ${accessToken}` };

    try {
      // The webhook only notifies: fetch the document to get its PDF and patient
      const documentResponse = await fetch(`https://api.askara.ai/documents/${documentUuid}`, { headers });
      if (!documentResponse.ok) throw new Error(`GET /documents failed: ${documentResponse.status}`);
      const document = await documentResponse.json();

      // Download PDF
      const pdfResponse = await fetch(document.pdf.url);
      const pdfBuffer = Buffer.from(await pdfResponse.arrayBuffer());

      // Save to your system
      await saveToSystem(pdfBuffer, document);

      // Confirm success
      await fetch(`https://api.askara.ai/documents/${documentUuid}/status`, {
        method: 'POST',
        headers: { ...headers, 'Content-Type': 'application/json' },
        body: JSON.stringify({ success: true })
      });
    } catch (error) {
      // Report failure
      await fetch(`https://api.askara.ai/documents/${documentUuid}/status`, {
        method: 'POST',
        headers: { ...headers, 'Content-Type': 'application/json' },
        body: JSON.stringify({ success: false, error: error.message })
      });
    }
  }
});
```

## Best Practices

1. **Always confirm status** - Report both successes and failures
2. **Provide error messages** - Include detailed error information when synchronization fails
3. **Process asynchronously** - Don't block webhook responses while processing
4. **Validate webhook signatures** - Check the `Webhook-Signature` header on every request (see [Verify the Webhook Signature](#verify-the-webhook-signature))
5. **Handle duplicates** - Use document UUID to prevent duplicate imports
6. **Implement idempotency** - Same document UUID should produce the same result


## Troubleshooting

### Webhook Not Receiving Events

- Verify your webhook URL is publicly accessible
- Check that your endpoint responds with a `2xx` status within 5 seconds
- Check that the healthcare professional who owns the document registered a webhook with the `new_document` subscription: events are only sent to the webhook of the document's author
- Check that your endpoint is reachable from the internet: Askara does not call URLs that resolve to a private network
- If the problem persists, [contact our team](https://www.askara.ai/en/contact) with the document `uuid` and the approximate time of the event


### Synchronization Failures

- Check OAuth2 token hasn't expired
- Verify you have the required scopes
- Review the error message recorded for your client in the `documentIntegrations` of `GET /documents/{uuid}`
- Test with polling method to isolate webhook issues


## Next Steps

- **[API Reference](/apis/askara-symfony/askara-api)** - Explore all endpoints
- **[OAuth2 Authentication](/guides/oauth2)** - Secure your integration
- **[Rate Limiting](/guides/rate_limiting)** - Understand API limits
- **[Support](https://www.askara.ai/en/contact)** - Get help with your integration