The signed envelope
Every signed Cheggout API — the Rewards Center token endpoints, and the Bank & SDK server APIs — uses one envelope. Implement it once and it works everywhere.
Structure
{
"CHANNELID": "YOURCHANNEL",
"SIGN": "<Base64 RSA signature>",
"BDATA": "<Base64 of the JSON payload>"
}
| Field | Type | Required | Description |
|---|---|---|---|
CHANNELID | string | Yes | Your predefined channel identifier, issued by Cheggout. Tells Cheggout which public key to verify with. |
SIGN | string | Yes | Base64 signature over the BDATA string, using SHA256withRSA and your private key. |
BDATA | string | Yes | Base64 of the UTF-8 JSON payload for this endpoint. |
The business payload for the endpoint lives inside BDATA. Each API reference page documents its
own payload fields.
What exactly gets signed
This is the detail that most commonly trips up a first integration:
You sign the Base64
BDATAstring itself — not the decoded JSON, and not the whole envelope.
payload JSON ──Base64──► BDATA string ──sign these UTF-8 bytes──► SIGN
CHANNELID is not covered by the signature.
| Parameter | Value |
|---|---|
| Algorithm | SHA256withRSA (RSASSA-PKCS#1 v1.5 with SHA-256) |
| Signed input | UTF-8 bytes of the Base64 BDATA string |
| Signature encoding | Base64 |
| Key | Your RSA private key (2048-bit) |
Some circulated documents describe SIGN as "generated using the public key of the other party."
That is imprecise: you sign with your own private key, and Cheggout verifies with the public key
you gave them. The reverse applies to responses.
Building a request
- Java
- C#
- Node.js
- Python
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.Signature;
import java.util.Base64;
public String buildEnvelope(String payloadJson, PrivateKey privateKey, String channelId)
throws Exception {
// 1. Base64-encode the payload
String bdata = Base64.getEncoder()
.encodeToString(payloadJson.getBytes(StandardCharsets.UTF_8));
// 2. Sign the Base64 STRING, not the original JSON
Signature signer = Signature.getInstance("SHA256withRSA");
signer.initSign(privateKey);
signer.update(bdata.getBytes(StandardCharsets.UTF_8));
String sign = Base64.getEncoder().encodeToString(signer.sign());
// 3. Assemble
Map<String, String> envelope = new LinkedHashMap<>();
envelope.put("CHANNELID", channelId);
envelope.put("SIGN", sign);
envelope.put("BDATA", bdata);
return new ObjectMapper().writeValueAsString(envelope);
}
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
public string BuildEnvelope(string payloadJson, RSA privateKey, string channelId)
{
// 1. Base64-encode the payload
var bdata = Convert.ToBase64String(Encoding.UTF8.GetBytes(payloadJson));
// 2. Sign the Base64 string
var signature = privateKey.SignData(
Encoding.UTF8.GetBytes(bdata),
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
// 3. Assemble
return JsonSerializer.Serialize(new {
CHANNELID = channelId,
SIGN = Convert.ToBase64String(signature),
BDATA = bdata
});
}
const crypto = require('crypto');
function buildEnvelope(payload, privateKeyPem, channelId) {
// 1. Base64-encode the payload
const bdata = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64');
// 2. Sign the Base64 string
const sign = crypto
.createSign('RSA-SHA256')
.update(bdata, 'utf8')
.sign(privateKeyPem, 'base64');
// 3. Assemble
return {CHANNELID: channelId, SIGN: sign, BDATA: bdata};
}
import base64, json
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
def build_envelope(payload: dict, private_key_pem: bytes, channel_id: str) -> dict:
# 1. Base64-encode the payload
bdata = base64.b64encode(json.dumps(payload).encode("utf-8")).decode("ascii")
# 2. Sign the Base64 string
key = serialization.load_pem_private_key(private_key_pem, password=None)
signature = key.sign(bdata.encode("utf-8"), padding.PKCS1v15(), hashes.SHA256())
# 3. Assemble
return {
"CHANNELID": channel_id,
"SIGN": base64.b64encode(signature).decode("ascii"),
"BDATA": bdata,
}
Verifying a response
Signed endpoints reply with the same envelope shape, signed by Cheggout:
{
"CHANNELID": "CHEGGOUT",
"SIGN": "<Cheggout's signature>",
"BDATA": "<Base64 payload>"
}
Verify before you trust it.
- Java
- C#
- Node.js
public String verifyAndDecode(String bdata, String sign, PublicKey cheggoutPublicKey)
throws Exception {
Signature verifier = Signature.getInstance("SHA256withRSA");
verifier.initVerify(cheggoutPublicKey);
verifier.update(bdata.getBytes(StandardCharsets.UTF_8));
if (!verifier.verify(Base64.getDecoder().decode(sign))) {
throw new SecurityException("Cheggout response signature invalid");
}
return new String(Base64.getDecoder().decode(bdata), StandardCharsets.UTF_8);
}
public string VerifyAndDecode(string bdata, string sign, RSA cheggoutPublicKey)
{
var ok = cheggoutPublicKey.VerifyData(
Encoding.UTF8.GetBytes(bdata),
Convert.FromBase64String(sign),
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
if (!ok) throw new SecurityException("Cheggout response signature invalid");
return Encoding.UTF8.GetString(Convert.FromBase64String(bdata));
}
function verifyAndDecode(bdata, sign, cheggoutPublicKeyPem) {
const ok = crypto
.createVerify('RSA-SHA256')
.update(bdata, 'utf8')
.verify(cheggoutPublicKeyPem, sign, 'base64');
if (!ok) throw new Error('Cheggout response signature invalid');
return JSON.parse(Buffer.from(bdata, 'base64').toString('utf8'));
}
A failed request may come back as plain JSON with no SIGN at all — for example
{"STATUS":"FAILURE","ERRORSTRING":"Signature Mismatch!"}. Check for the failure shape before
attempting verification, or your client will throw on a legitimate error response.
Worked example
Payload for getSecuredReferenceToken:
{ "bName": "YOURCHANNEL" }
Base64-encoded:
eyAiYk5hbWUiOiAiWU9VUkNIQU5ORUwiIH0=
Signed and assembled:
{
"CHANNELID": "YOURCHANNEL",
"SIGN": "REPLACE_WITH_YOUR_BASE64_SIGNATURE",
"BDATA": "eyAiYk5hbWUiOiAiWU9VUkNIQU5ORUwiIH0="
}
Response, after verifying and decoding BDATA:
{
"referenceKey": "REPLACE_WITH_TOKEN",
"expireDate": "<expiry timestamp>"
}
All signatures and tokens in this documentation are placeholders. Real values come from your own key pair and Cheggout's UAT environment.
Treat tokens as opaque credentials. No token format is documented — do not parse one, and do not derive an expiry from its contents.
Common failures
| Symptom | Likely cause |
|---|---|
Signature Mismatch! | Signed the decoded JSON instead of the Base64 string |
Signature Mismatch! | Re-serialised the payload after computing BDATA — sign exactly the bytes you send |
Signature Mismatch! | Wrong CHANNELID, so Cheggout is verifying with a different key |
Signature Mismatch! | Used PSS padding instead of PKCS#1 v1.5 |
| Payload field ignored | Wrong key case — several payloads match keys case-sensitively |
| Connection refused / rejected | Calling IP not whitelisted |
The rule for the top three: compute BDATA once, sign that exact string, and transmit that exact
string.
Payload key reference
Payload keys differ per endpoint and are matched case-sensitively. Always confirm on the endpoint page.
| Endpoint | Payload key for your channel |
|---|---|
getSecuredReferenceToken | bName |
GetAuthToken | channelName |
GenerateSessionInfoV2 | bName |
FetchRewardInfo | channelName — note: this endpoint sends CHANNELID and BDATA but no SIGN. It is authenticated by a bearer token, not by a signature. See FetchRewardInfo. |
Replay protection
TBD The envelope carries no nonce or timestamp, and the signature does not
cover CHANNELID. A captured request body is therefore replayable at the transport layer; today
the mitigations are IP whitelisting, TLS, and per-request idempotency via extTransactionId.
Do not design flows that assume the envelope alone prevents replay. A nonce/timestamp extension is tracked in Open Items.