Authentication

Signing a Converge Cloud request

Converge Cloud uses TC3-HMAC-SHA256. Every request carries seven headers and a signature computed over the request body.

Get a credential

Every request is signed with a SecretId and SecretKey pair. They are issued from the Converge Cloud console at console.convergecloud.com, under access key management for the account.

Keys cannot be created through the API. The cam actions that manage sub-accounts and access keys are not available on Converge Cloud, so issuing, rotating and revoking a key is a console operation. Plan for that in any automation.

  • —A credential is account-wide. There is no way to ask the API which account it belongs to, so the account must be recorded with the credential.
  • —For anything long-running, prefer temporary credentials from sts:GetFederationToken or sts:AssumeRole rather than embedding a long-lived key.

The seven headers

AuthorizationTC3-HMAC-SHA256 Credential=…, SignedHeaders=…, Signature=…
Content-Typeapplication/json; charset=utf-8
Host<service>.api3.convergecloud.com
X-TC-ActionThe action name, e.g. DescribeInstances
X-TC-Regionluzon. Required; a missing header or any other value fails with InternalError.
X-TC-TimestampUnix seconds. Hard 300-second window.
X-TC-VersionPer service. A wrong value reads as InvalidAction.

Common signing failures

The signing service is a separate input

The credential scope is <date>/<signingService>/tc3_request, and the signing service is not always the first label of the hostname. A mismatch returns AuthFailure, which reads as a signing error rather than a routing one.

A hard 300-second window

Requests with a timestamp more than five minutes from the server's clock are rejected as an authentication failure. The error does not mention time.

Sign a request

Four steps. The worked example below uses the same inputs throughout, so each intermediate value can be compared against an independent implementation.

Step 1 — canonical request

Build a canonical form of the request and hash it. Converge Cloud uses one path and no query string, so only the headers and the body vary. The action name is lowercased here and nowhere else.

CanonicalRequest
POST
/

content-type:application/json; charset=utf-8
host:<service>.api3.convergecloud.com
x-tc-action:<action, lowercased>

content-type;host;x-tc-action
<hex SHA256 of the request body>
Step 2 — string to sign

The date is UTC, derived from the same timestamp sent in X-TC-Timestamp. A local date will fail on either side of midnight.

StringToSign
TC3-HMAC-SHA256
<X-TC-Timestamp, unix seconds>
<YYYY-MM-DD>/<signing service>/tc3_request
<hex SHA256 of the CanonicalRequest>
Step 3 — derive the signing key

Three chained HMAC-SHA256 operations. Each takes the previous result as its key; only the first is prefixed with the literal TC3. Keep the intermediate values as raw bytes — hex-encoding between steps is the most common implementation error.

Signing key
kDate    = HMAC-SHA256("TC3" + SecretKey, "<YYYY-MM-DD>")
kService = HMAC-SHA256(kDate, "<signing service>")
kSigning = HMAC-SHA256(kService, "tc3_request")

Signature = hex( HMAC-SHA256(kSigning, StringToSign) )
Step 4 — the Authorization header
Authorization
TC3-HMAC-SHA256 Credential=<SecretId>/<YYYY-MM-DD>/<signing service>/tc3_request, SignedHeaders=content-type;host;x-tc-action, Signature=<signature>
Four things that make a correct-looking signature fail
  • The body is hashed and sent as the same bytes. Serialise once; re-serialising between signing and sending changes the hash.
  • The action is lowercased in the canonical request but sent in its original case in X-TC-Action.
  • The signing service is its own value, not the first label of the hostname. It appears in both the credential scope and kService.
  • The date in the scope must be the UTC date of the timestamp, in both the StringToSign and the Authorization header.

Test vector

Fixed inputs with a known-good signature, for checking an implementation offline. These credentials are not real.

Test vector
SecretId       = AKIDEXAMPLE1234567890EXAMPLE12345678
SecretKey      = ExampleSecretKeyDoNotUse0000000000
X-TC-Timestamp = 1787702400                    # 2026-08-26T00:00:00Z
Host           = cbs.api3.convergecloud.com
X-TC-Action    = DescribeDisks
X-TC-Version   = 2017-03-12
X-TC-Region    = luzon
Body           = {"Limit": 1}

SHA256(Body)             = 65d0b99ccb96b0e186fbe9801c78411017f5188af83639d6b348da161950d5aa
SHA256(CanonicalRequest) = c7ead16aee4a72d26adc52d902b42ffd04aea2a609d7b072c2a4d5cec1525012
Signature                = 2a11b808d64385de6523b932c29bbef26ce3b1777bafcd9abfa9e2f1bd67114a

# With the minimal content-type;host signed-header set instead:
Signature                = 296af64e4455784e66af43c09ecc009b79f38320786a0828819166c3c9be0f7d

Note the space after the colon in the body. The signature covers the exact bytes sent, so Python's json.dumps ({"Limit": 1}) and JavaScript's JSON.stringify ({"Limit":1}) produce different signatures. Serialise once and send the same string that was signed.

Working examples

curl -sS https://cvm.api3.convergecloud.com/ \
  -H "Authorization: TC3-HMAC-SHA256 Credential=$SECRET_ID/2026-09-16/cvm/tc3_request, SignedHeaders=content-type;host;x-tc-action, Signature=$SIG" \
  -H 'Content-Type: application/json; charset=utf-8' \
  -H 'Host: cvm.api3.convergecloud.com' \
  -H 'X-TC-Action: DescribeInstances' \
  -H 'X-TC-Version: 2017-03-12' \
  -H "X-TC-Timestamp: $(date +%s)" \
  -H 'X-TC-Region: luzon' \
  -d '{"Limit": 10}'

Credential handling

A credential cannot be resolved to an account

sts:GetCallerIdentity, sts:QueryApiKey and cam:GetUserAppId are not available, so a credential cannot be resolved to an account through the API. A key pointing at an unexpected account is indistinguishable from one pointing at an empty account.

  • —sts:GetFederationToken and sts:AssumeRole are both available, so applications can use short-lived credentials instead of long-lived keys.
  • —Key last-used data is not available, so rotation must be time-based rather than usage-based.