Skip to main content
This guide provides an expanded explanation of how Rebell’s authentication and signing model works, including implementation examples, common errors, and best practices. Use this section when implementing low-level cryptographic signing or when troubleshooting signature-related issues.
For a higher-level overview of authentication and environments, see Authentication & Environments.

Overview

All Rebell server-to-server requests must be authenticated using an asymmetric RSA signature. The merchant signs the request using a private key; Rebell validates the signature using the merchant’s registered public key. Goals of RSA signing within Rebell:
  • Guarantee request authenticity
  • Prevent tampering of payload and metadata
  • Ensure requests cannot be replayed outside a valid temporal window
  • Enforce a secure merchant identity model
The signing process applies to all payment-related APIs and webhook verification.

Signing Workflow

A signed Rebell API request follows this lifecycle:
1

Construct the Signing String

Build the string that will be signed, including method, path, Client-Id, timestamp, and payload
2

Serialize the Request Body

Convert the request body to a JSON string exactly as it will be sent
3

Sign with RSA

Sign the string using SHA256withRSA algorithm
4

Base64URL Encode

Encode the signature using Base64URL encoding
5

Add Signature Header

Include the signature in the Signature header
6

Send Request

Send the request over HTTPS
7

Rebell Validates

Rebell verifies signature + timestamp + payload integrity
If any of these steps fail or produce inconsistent results, the request is rejected with INVALID_SIGNATURE.

Building the Signing String

The signing string is composed of two lines:
<HTTP_METHOD> <HTTP_PATH>
<Client-Id>.<Request-Time>.<BODY>

Example

POST /v1/payments/retailPay
2022091495540562874792.2024-01-10T12:22:30Z.{"productCode":"51051000101000100040","paymentRequestId":"order-123"}

Important Rules

Critical Requirements:
  • HTTP_PATH must not include hostname or query parameters
  • Body must be serialized exactly as sent (byte-for-byte)
  • Spaces, indentation, or field ordering inconsistencies will break signature validation
  • Empty body should be represented as an empty string ("")
  • Use UTF-8 encoding for all strings

Signing String Construction Example

function buildSigningString(method, path, clientId, requestTime, body) {
  const bodyString = body ? JSON.stringify(body) : '';

  const line1 = `${method} ${path}`;
  const line2 = `${clientId}.${requestTime}.${bodyString}`;

  return `${line1}\n${line2}`;
}

// Example usage
const signingString = buildSigningString(
  'POST',
  '/v1/payments/retailPay',
  '2022091495540562874792',
  '2024-01-10T12:22:30Z',
  {
    productCode: '51051000101000100040',
    paymentRequestId: 'order-123',
    paymentAmount: { currency: 'EUR', value: 1250 }
  }
);

Signature Generation

Once you have the signing string, sign it using RSA-SHA256.
const crypto = require('crypto');

function signRequest(privateKeyPem, signingString) {
  const signer = crypto.createSign('RSA-SHA256');
  signer.update(signingString, 'utf8');
  return signer.sign(privateKeyPem, 'base64url');
}

// Complete signing function
function createSignature(privateKeyPem, method, path, clientId, body) {
  const requestTime = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
  const signingString = buildSigningString(method, path, clientId, requestTime, body);
  const signature = signRequest(privateKeyPem, signingString);

  return {
    requestTime,
    signature,
    header: `algorithm=SHA256withRSA, keyVersion=1, signature=${signature}`
  };
}

Complete Request Example

Here’s a complete example of making a signed API request:
const crypto = require('crypto');
const https = require('https');

class RebellClient {
  constructor(clientId, privateKeyPem, keyVersion = 1) {
    this.clientId = clientId;
    this.privateKeyPem = privateKeyPem;
    this.keyVersion = keyVersion;
    this.baseUrl = 'https://open-eu.rebell.app'; // Production
  }

  buildSigningString(method, path, requestTime, body) {
    const bodyString = body ? JSON.stringify(body) : '';
    return `${method} ${path}\n${this.clientId}.${requestTime}.${bodyString}`;
  }

  sign(signingString) {
    const signer = crypto.createSign('RSA-SHA256');
    signer.update(signingString, 'utf8');
    return signer.sign(this.privateKeyPem, 'base64url');
  }

  async request(method, path, body = null) {
    const requestTime = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
    const signingString = this.buildSigningString(method, path, requestTime, body);
    const signature = this.sign(signingString);

    const headers = {
      'Content-Type': 'application/json',
      'Client-Id': this.clientId,
      'Request-Time': requestTime,
      'Signature': `algorithm=SHA256withRSA, keyVersion=${this.keyVersion}, signature=${signature}`
    };

    const response = await fetch(`${this.baseUrl}${path}`, {
      method,
      headers,
      body: body ? JSON.stringify(body) : undefined
    });

    return response.json();
  }

  // Payment methods
  async retailPay(paymentData) {
    return this.request('POST', '/v1/payments/retailPay', paymentData);
  }

  async createQrOrder(orderData) {
    return this.request('POST', '/v1/payments/createQrOrder', orderData);
  }

  async linkPayCreate(paymentData) {
    return this.request('POST', '/v1/payments/linkPayCreate', paymentData);
  }

  async inquiryPayment(paymentId, paymentRequestId) {
    return this.request('POST', '/v1/payments/inquiryPayment', {
      paymentId,
      paymentRequestId
    });
  }
}

// Usage
const client = new RebellClient(
  'your-client-id',
  fs.readFileSync('private-key.pem', 'utf8')
);

const result = await client.retailPay({
  productCode: '51051000101000100040',
  paymentRequestId: 'order-123',
  paymentAuthCode: '281012020262467128',
  paymentAmount: { currency: 'EUR', value: 1250 }
});

Timestamp Requirements

The Request-Time header ensures replay protection.
RequirementDetails
FormatISO 8601 UTC timestamp (e.g., 2024-03-21T10:15:00Z)
TimezoneMust be UTC (indicated by Z suffix)
ToleranceMust not differ from Rebell server time by more than ±5 minutes
ErrorIf drift exceeds this bound, Rebell returns TIMESTAMP_INVALID
Best Practice: Ensure your backend servers use NTP synchronization to keep accurate time.
Timestamp Generation
// Correct - UTC with Z suffix
const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
// Result: "2024-03-21T10:15:00Z"

// Also correct - full ISO format
const timestamp2 = new Date().toISOString();
// Result: "2024-03-21T10:15:00.000Z"

Key Rotation Workflow

Rebell supports key rotation through versioning (keyVersion).

Rotation Sequence

1

Generate New Keypair

Merchant generates a new RSA keypair (minimum 2048-bit)
2

Upload Public Key

Merchant uploads the new public key to Rebell dashboard
3

Receive New Version

Rebell assigns a new keyVersion number
4

Update Signing Logic

Merchant updates signing logic to use the new private key and keyVersion
5

Transition Period

Both old and new signatures remain valid during transition
6

Deactivate Old Key

Old key is eventually deactivated in Rebell dashboard
Recommendation: Rotate keys at least annually or according to your internal security policies.

Key Rotation Example

Key Rotation
class RebellClient {
  constructor(clientId, keys) {
    this.clientId = clientId;
    // Support multiple key versions during rotation
    this.keys = keys; // { 1: privateKey1, 2: privateKey2 }
    this.activeKeyVersion = Math.max(...Object.keys(keys).map(Number));
  }

  sign(signingString) {
    const privateKey = this.keys[this.activeKeyVersion];
    const signer = crypto.createSign('RSA-SHA256');
    signer.update(signingString, 'utf8');
    return {
      signature: signer.sign(privateKey, 'base64url'),
      keyVersion: this.activeKeyVersion
    };
  }
}

Webhook Signature Verification

Rebell signs webhook payloads using the same RSA mechanism. Merchants must verify these signatures.

Verification Steps

1

Extract Signature

Parse the Signature header to extract algorithm, keyVersion, and signature
2

Reconstruct Signing String

Build the signing string from the request data
3

Validate Timestamp

Ensure Request-Time is within acceptable window (e.g., 10 minutes)
4

Verify RSA Signature

Verify using Rebell’s public key for the specified keyVersion
5

Process Event

Only process the webhook if verification succeeds

Verification Example

const crypto = require('crypto');

function verifyWebhookSignature(req, rebellPublicKeys) {
  // 1. Parse signature header
  const signatureHeader = req.headers['signature'];
  const parts = {};
  signatureHeader.split(', ').forEach(part => {
    const [key, value] = part.split('=');
    parts[key] = value;
  });

  const { algorithm, keyVersion, signature } = parts;

  // 2. Get the correct public key
  const publicKey = rebellPublicKeys[keyVersion];
  if (!publicKey) {
    throw new Error(`Unknown keyVersion: ${keyVersion}`);
  }

  // 3. Reconstruct signing string
  const requestTime = req.headers['request-time'];
  const webhookPath = '/webhooks/rebell'; // Your webhook endpoint path
  const signingString = `POST ${webhookPath}\n${requestTime}.${req.rawBody}`;

  // 4. Validate timestamp (10-minute window)
  const requestTimestamp = new Date(requestTime).getTime();
  const now = Date.now();
  if (Math.abs(now - requestTimestamp) > 10 * 60 * 1000) {
    throw new Error('Timestamp outside acceptable window');
  }

  // 5. Verify signature
  const verifier = crypto.createVerify('RSA-SHA256');
  verifier.update(signingString, 'utf8');

  const signatureBuffer = Buffer.from(signature, 'base64url');
  return verifier.verify(publicKey, signatureBuffer);
}

Common Errors and Resolution

Caused by:
  • Incorrect signing string construction
  • Wrong body serialization (extra spaces, different field order)
  • Incorrect header values
  • Using a retired keyVersion
  • Base64 vs Base64URL encoding mismatch
How to fix:
  • Log the exact signing string before signing
  • Compare with what you expect byte-by-byte
  • Ensure JSON serialization is consistent (no pretty-printing)
  • Verify you’re using SHA256withRSA algorithm
  • Check that signature uses Base64URL encoding (not standard Base64)
Debug Signing String
// Log for debugging
console.log('Signing string:');
console.log(JSON.stringify(signingString)); // Shows escape characters
console.log('Bytes:', Buffer.from(signingString).toString('hex'));
Caused by:
  • Clock skew greater than ±5 minutes
  • Bad timezone conversions
  • Non-UTC timestamps
  • Missing Z suffix
How to fix:
  • Use NTP time synchronization on your servers
  • Always use UTC with Z suffix
  • Verify timestamp format: 2024-03-21T10:15:00Z
Correct Timestamp
// Correct
const timestamp = new Date().toISOString(); // "2024-03-21T10:15:00.000Z"

// Also correct (no milliseconds)
const timestamp2 = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
Caused by:
  • Wrong Client-Id
  • Missing or wrong keyVersion
  • Using production credentials in sandbox (or vice versa)
  • Public key not registered with Rebell
How to fix:
  • Verify your merchant credential set
  • Ensure environment separation (sandbox vs production)
  • Confirm public key is uploaded to Rebell dashboard
  • Check keyVersion matches the registered key
Caused by:
  • Pretty-printed JSON (with newlines/indentation)
  • Different field ordering
  • Unicode encoding differences
  • Trailing whitespace
How to fix:
  • Use compact JSON serialization
  • Ensure consistent field ordering
  • Use UTF-8 encoding
Consistent Serialization
// Correct - compact, no spaces
const body = JSON.stringify(data);
// Result: {"productCode":"123","amount":100}

// Wrong - pretty printed
const body = JSON.stringify(data, null, 2);
// Result: {\n  "productCode": "123",\n  "amount": 100\n}

Security Recommendations

Critical Security Requirements:
  • ✅ Store private keys in a secure HSM/KMS (e.g., AWS KMS, HashiCorp Vault)
  • ✅ Never include private keys in frontend or mobile apps
  • ✅ Use strict file permissions on key material (e.g., chmod 600)
  • ✅ Regenerate keys periodically (at least annually)
  • ✅ Monitor failed signature attempts for intrusion detection
  • ✅ Use HTTPS for all communication
  • ✅ Keep separate keys for sandbox and production environments
  • ✅ Implement key rotation without downtime

Key Storage Best Practices

# Store as environment variable (base64 encoded)
export REBELL_PRIVATE_KEY=$(cat private-key.pem | base64)
const privateKey = Buffer.from(process.env.REBELL_PRIVATE_KEY, 'base64').toString();

Testing Checklist

Test these scenarios before going live:

Next Steps