---
title: "Get started"
description: "Get started with CloudHub in C#, PHP, Python, Node.js and Java: get an API key, install a client, create a session, send the user to a provider, read the certificate and sign a hash."
canonical: "https://docs.lacunasoftware.com/en-us/articles/cloudhub/get-started"
locale: "en"
product: "CloudHub"
last_updated: "2026-10-12"
---

# Get started

This page takes you from an API key to a signed hash: install a client, create a session, send the
user to a provider, and use the session that comes back. Each step has code in C#, PHP, Python,
Node.js and Java. [How it works](https://docs.lacunasoftware.com/en-us/articles/cloudhub/how-it-works) explains the flow behind the steps.

The code signs a hash that your signature library computed. [Signing a PDF](https://docs.lacunasoftware.com/en-us/articles/cloudhub/pades-signature) and
[Authentication](https://docs.lacunasoftware.com/en-us/articles/cloudhub/authentication) show complete handlers that compute that hash and finish the
signature with PKI SDK, Rest PKI or PKI Express.

## 1. Get an API key

Ask Lacuna Software support (suporte@lacunasoftware.com) for a CloudHub API key, and list the
redirect URIs your application will use, for production and for development. If you will add your
own parameters to the redirect URI, as the examples below do with `fileId`, ask for a host entry
such as `https://app.example.com` instead of an exact URI. See
[Redirect URIs](https://docs.lacunasoftware.com/en-us/articles/cloudhub/how-it-works#redirect-uris).

The examples read the key from the `CLOUDHUB_API_KEY` environment variable. Keep it on the server.

## 2. Install a client

**C#**

```bash
dotnet add package Lacuna.Cloudhub.Client
```

For ASP.NET Core dependency injection, add `Lacuna.Cloudhub.ClientCore` as well.

**PHP**

```bash
composer require lacuna/cloudhub-client
```

The client needs PHP 7.4 or later and uses Guzzle.

**Python**

```bash
pip install requests
```

The Python examples call the REST API directly; there are only eight operations.

**Node.js**

```bash
npm install cloudhub-client
```

**Java**

```groovy title="build.gradle"
dependencies {
    implementation 'com.lacunasoftware.cloudhub:cloudhub-client:1.0.5'
}
```

With Maven:

```xml
<dependency>
    <groupId>com.lacunasoftware.cloudhub</groupId>
    <artifactId>cloudhub-client</artifactId>
    <version>1.0.5</version>
</dependency>
```

## 3. Create a client

**C#**

```csharp
using Lacuna.Cloudhub.Api;
using Lacuna.Cloudhub.Client;

var cloudhub = new CloudhubClient(
    "https://cloudhub.lacunasoftware.com/",
    Environment.GetEnvironmentVariable("CLOUDHUB_API_KEY"));
```

With `Lacuna.Cloudhub.ClientCore`, register the client once and inject `ICloudhubService`, which has
the same methods:

```csharp title="Program.cs"
using Lacuna.Cloudhub.ClientCore;

builder.Services.AddCloudhub().Configure(builder.Configuration.GetSection("Cloudhub"));
```

```json title="appsettings.json"
{
  "Cloudhub": {
    "Endpoint": "https://cloudhub.lacunasoftware.com/",
    "ApiKey": "set it in user secrets or an environment variable"
  }
}
```

**PHP**

```php
<?php
require __DIR__ . '/vendor/autoload.php';

use Lacuna\Cloudhub\CloudhubClient;

// The client appends "api/..." to the address, so keep the trailing slash
$cloudhub = new CloudhubClient('https://cloudhub.lacunasoftware.com/', getenv('CLOUDHUB_API_KEY'));
```

The PHP methods end in `Async` but are synchronous: each one returns the result.

**Python**

```python title="cloudhub.py"
import os

import requests

CLOUDHUB_URL = 'https://cloudhub.lacunasoftware.com'

_http = requests.Session()
_http.headers['X-Api-Key'] = os.environ['CLOUDHUB_API_KEY']


class CloudHubError(Exception):
    """An error CloudHub reported, such as SessionExpired."""

    def __init__(self, code, message):
        super().__init__(f'{code}: {message}')
        self.code = code


def call_cloudhub(method, path, **kwargs):
    # A signature can wait on the provider, so allow a generous timeout
    response = _http.request(method, CLOUDHUB_URL + path, timeout=120, **kwargs)
    if response.status_code == 422:
        error = response.json()
        raise CloudHubError(error.get('code'), error.get('message'))
    response.raise_for_status()
    return response.json()
```

**Node.js**

```js
const { CloudHubClient, TrustServiceSessionTypes } = require('cloudhub-client');

const cloudhub = new CloudHubClient('https://cloudhub.lacunasoftware.com/', process.env.CLOUDHUB_API_KEY);
```

**Java**

```java
import cloudhub.SessionsApi;
import cloudhub.client.CloudhubClient;

// No trailing slash: the client appends "/api/..." to the address
SessionsApi cloudhub = new SessionsApi(
    new CloudhubClient("https://cloudhub.lacunasoftware.com", System.getenv("CLOUDHUB_API_KEY")));
```

A `CloudhubClient` keeps per-request state, so don't share one instance between threads that call
it at the same time.

## 4. Create a session

Send the signer's CPF, digits only, and the URI CloudHub should send the user back to. The examples
carry the ID of the file to sign in the redirect URI, so that the request that comes back knows
which file it is for.

**C#**

```csharp
var session = await cloudhub.CreateSessionAsync(new SessionCreateRequest {
    Identifier = "12345678909",
    RedirectUri = "https://app.example.com/sign/complete?fileId=42",
    Type = TrustServiceSessionTypes.SingleSignature,
    LifetimeInSeconds = 300,
});

foreach (var service in session.Services) {
    Console.WriteLine($"{service.ServiceInfo.Provider}: {service.AuthUrl}");
}
```

**PHP**

```php
use Lacuna\Cloudhub\SessionCreateRequest;
use Lacuna\Cloudhub\TrustServiceSessionTypes;

$session = $cloudhub->createSessionAsync(new SessionCreateRequest(
    '12345678909',                                      // the signer's CPF
    'https://app.example.com/sign/complete?fileId=42',  // redirect URI
    TrustServiceSessionTypes::SingleSignature,
    300                                                 // lifetime in seconds; 300 if omitted
));

foreach ($session->services as $service) {
    echo $service->serviceInfo->provider . ': ' . $service->authUrl . PHP_EOL;
}
```

**Python**

```python
from cloudhub import call_cloudhub

session = call_cloudhub('POST', '/api/sessions', json={
    'identifier': '12345678909',
    'redirectUri': 'https://app.example.com/sign/complete?fileId=42',
    'type': 'SingleSignature',
    'lifetimeInSeconds': 300,
})

for service in session['services']:
    print(service['serviceInfo']['provider'], service['authUrl'])
```

**Node.js**

```js
const session = await cloudhub.createSessionAsync({
  identifier: '12345678909',
  redirectUri: 'https://app.example.com/sign/complete?fileId=42',
  type: TrustServiceSessionTypes.SingleSignature,
  lifetimeInSeconds: 300,
});

for (const service of session.services || []) {
  console.log(`${service.serviceInfo.provider}: ${service.authUrl}`);
}
```

**Java**

```java
import cloudhub.client.model.SessionCreateRequest;
import cloudhub.client.model.SessionModel;
import cloudhub.client.model.TrustServiceAuthParametersModel;
import cloudhub.client.model.TrustServiceSessionTypes;

SessionCreateRequest request = new SessionCreateRequest();
request.setIdentifier("12345678909");
request.setRedirectUri("https://app.example.com/sign/complete?fileId=42");
request.setType(TrustServiceSessionTypes.SINGLE_SIGNATURE);
request.setLifetimeInSeconds(300);

SessionModel session = cloudhub.apiSessionsPost(request);
for (TrustServiceAuthParametersModel service : session.getServices()) {
    System.out.println(service.getServiceInfo().getProvider() + ": " + service.getAuthUrl());
}
```

An empty list means that no provider holds a certificate for that CPF. Tell the user, and offer to
try another CPF.

## 5. Send the user to a provider

Render one choice per entry and navigate the browser to the `authUrl` the user picks. `badgeUrl`
is the provider's logo and may be null.

```html
<!-- One link per entry of "services" -->
<a href="https://api.birdid.com.br/oauth/authorize?client_id=…">
  <img src="https://cdn.lacunasoftware.com/img/psc/birdid.png" alt="BirdID" height="48">
  Sign with SOLUTI
</a>
```

The user signs in at the provider and approves. The provider sends the browser back to CloudHub,
and CloudHub sends it to your redirect URI.

## 6. Read the certificate when the user comes back

Your redirect URI receives the `session` parameter beside your own. When `session` is missing, the
user did not authorize: send them back to choose again.

**C#**

```csharp
// ASP.NET Core: GET /sign/complete?fileId=42&session=…
public async Task<IActionResult> Complete(string fileId, string session) {
    if (string.IsNullOrEmpty(session)) {
        return RedirectToAction("Index", new { fileId });
    }

    byte[] certificate = await cloudhub.GetCertificateAsync(session);
    // ...
}
```

**PHP**

```php
// complete.php?fileId=42&session=…
$fileId = $_GET['fileId'];
$session = $_GET['session'] ?? null;
if (empty($session)) {
    header('Location: /sign.php?fileId=' . urlencode($fileId));
    exit;
}

$certificate = $cloudhub->getCertificateAsync($session); // base64
```

**Python**

```python
from flask import redirect, request, url_for

@app.route('/sign/complete')
def complete():
    file_id = request.args['fileId']
    session = request.args.get('session')
    if not session:
        return redirect(url_for('sign', fileId=file_id))

    certificate = call_cloudhub('GET', '/api/sessions/certificate',
                                params={'session': session})  # base64
    # ...
```

**Node.js**

```js
router.get('/sign/complete', async (req, res, next) => {
  try {
    const { fileId, session } = req.query;
    if (!session) {
      return res.redirect(`/sign?fileId=${encodeURIComponent(fileId)}`);
    }

    const certificate = await cloudhub.getCertificateAsync(session); // base64
    // ...
  } catch (err) {
    next(err);
  }
});
```

**Java**

```java
import cloudhub.client.ApiException;
import java.nio.charset.StandardCharsets;

@GetMapping("/sign/complete")
public String complete(@RequestParam String fileId,
                       @RequestParam(required = false) String session) throws ApiException {
    if (session == null || session.isEmpty()) {
        return "redirect:/sign?fileId=" + fileId;
    }

    String certificate = unquote(cloudhub.apiSessionsCertificateGet(session)); // base64
    // ...
}

// The certificate and sign-hash operations answer with a JSON string ("MIIG..."),
// and the client hands back its raw bytes, quotes included
private static String unquote(byte[] body) {
    return new String(body, StandardCharsets.UTF_8).replace("\"", "");
}
```

Before you sign, check that the certificate belongs to the person you expected: compare the CPF in
it with the one you sent. The complete examples do this with their signature library.

## 7. Sign a hash

Your signature library gives you the hash to sign and its digest algorithm. Send both with the
session; the result is the signature value, for your library to embed in the document. Identify the
algorithm by OID (`digestAlgorithmOid`), as the signature libraries report it, or by name
(`digestAlgorithm`, such as `SHA-256`).

**C#**

```csharp
byte[] signature = await cloudhub.SignHashAsync(new SignHashRequest {
    Session = session,
    Hash = toSignHash,                  // the hash, as bytes
    DigestAlgorithmOid = digestAlgorithmOid,
});
```

**PHP**

```php
use Lacuna\Cloudhub\SignHashRequest;

$signature = $cloudhub->signHashAsync(new SignHashRequest(
    $session,
    $toSignHashBase64,      // the hash, base64-encoded
    null,                   // the digest algorithm name...
    $digestAlgorithmOid     // ...or its OID
)); // base64
```

**Python**

```python
signature = call_cloudhub('POST', '/api/sessions/sign-hash', json={
    'session': session,
    'hash': to_sign_hash_base64,
    'digestAlgorithmOid': digest_algorithm_oid,
})  # base64
```

**Node.js**

```js
const signature = await cloudhub.signHashAsync({
  session,
  hash: toSignHashBase64,
  digestAlgorithmOid,
}); // base64
```

**Java**

```java
import cloudhub.client.model.SignHashRequest;
import java.util.Base64;

SignHashRequest signRequest = new SignHashRequest();
signRequest.setSession(session);
signRequest.setHash(toSignHash);                  // the hash, as bytes
signRequest.setDigestAlgorithmOid(digestAlgorithmOid);

byte[] signature = Base64.getDecoder().decode(unquote(cloudhub.apiSessionsSignHashPost(signRequest)));
```

## 8. Handle errors

CloudHub reports a problem with your request as HTTP 422 and a JSON body with a `code`, a `message`
and sometimes `details`. The code you will meet most is `SessionExpired`: the session ran out
before you used it, so send the user through the provider again. The
[REST API](https://docs.lacunasoftware.com/en-us/articles/cloudhub/rest-api#errors) page lists every code.

**C#**

```csharp
try {
    var signature = await cloudhub.SignHashAsync(request);
} catch (CloudhubApiException ex) when (ex.CodeStr == "SessionExpired") {
    // Create a new session and send the user to the provider again
}
```

Compare `CodeStr`. The `Code` enum property is not filled in from the response.

**PHP**

```php
use GuzzleHttp\Exception\ClientException;

try {
    $signature = $cloudhub->signHashAsync($request);
} catch (ClientException $e) {
    $error = json_decode((string) $e->getResponse()->getBody(), true);
    if (($error['code'] ?? null) === 'SessionExpired') {
        // Create a new session and send the user to the provider again
    }
}
```

**Python**

```python
from cloudhub import CloudHubError

try:
    signature = call_cloudhub('POST', '/api/sessions/sign-hash', json=sign_request)
except CloudHubError as e:
    if e.code == 'SessionExpired':
        pass  # Create a new session and send the user to the provider again
```

**Node.js**

```js
try {
  const signature = await cloudhub.signHashAsync(request);
} catch (err) {
  // cloudhub-client 1.x throws a string: CloudHub's JSON error followed by the HTTP message
  if (String(err).includes('"SessionExpired"')) {
    // Create a new session and send the user to the provider again
  }
}
```

**Java**

```java
try {
    byte[] signature = cloudhub.apiSessionsSignHashPost(request);
} catch (ApiException e) {
    if (e.getCode() == 422 && e.getResponseBody().contains("\"SessionExpired\"")) {
        // Create a new session and send the user to the provider again
    }
}
```

A request without a valid API key gets HTTP 401. An unexpected failure, such as a provider that is
down, gets HTTP 500 with a short code in the body; quote it when you contact support.

## Next steps

- [Signing a PDF](https://docs.lacunasoftware.com/en-us/articles/cloudhub/pades-signature): the complete PAdES flow, with the signature library computing
  the hash and finishing the PDF.
- [Authentication](https://docs.lacunasoftware.com/en-us/articles/cloudhub/authentication): sign users in with their cloud certificate.
- [How it works](https://docs.lacunasoftware.com/en-us/articles/cloudhub/how-it-works#session-types): `SignatureSession` for signing several documents
  with one authorization.
