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:GetFederationTokenorsts:AssumeRolerather than embedding a long-lived key.
The seven headers
| Authorization | TC3-HMAC-SHA256 Credential=…, SignedHeaders=…, Signature=… |
| Content-Type | application/json; charset=utf-8 |
| Host | <service>.api3.convergecloud.com |
| X-TC-Action | The action name, e.g. DescribeInstances |
| X-TC-Region | luzon. Required; a missing header or any other value fails with InternalError. |
| X-TC-Timestamp | Unix seconds. Hard 300-second window. |
| X-TC-Version | Per service. A wrong value reads as InvalidAction. |
Common signing failures
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.
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.
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.
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>The date is UTC, derived from the same timestamp sent in X-TC-Timestamp. A local date will fail on either side of midnight.
TC3-HMAC-SHA256
<X-TC-Timestamp, unix seconds>
<YYYY-MM-DD>/<signing service>/tc3_request
<hex SHA256 of the CanonicalRequest>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.
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) )TC3-HMAC-SHA256 Credential=<SecretId>/<YYYY-MM-DD>/<signing service>/tc3_request, SignedHeaders=content-type;host;x-tc-action, Signature=<signature>- 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
StringToSignand theAuthorizationheader.
Test vector
Fixed inputs with a known-good signature, for checking an implementation offline. These credentials are not real.
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 = 296af64e4455784e66af43c09ecc009b79f38320786a0828819166c3c9be0f7dNote 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
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:GetFederationTokenandsts:AssumeRoleare 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.
