Over the past few days I have been struggling to create the AWS signature version 4 in Apex to send a request via query string, I have followed the step-by-step instructions for signing aws requests, regardless I'm getting the error <Code>IncompleteSignature</Code> <Message>Request must contain a signature that conforms to AWS standards</Message>, I don't know where I could have done a mistake, here are the methods (in a class called AWS) I'm using to sign:


public with sharing abstract class AWS {
    public string secret { get; set; }
    public string key { get; set; }

    public Blob createCanonicalRequest(...){...}
    public String getStringToSign(...){...}
    public String signature(...){...}


public Blob createCanonicalRequest(String method, String queryString, Map<String, String> headers, String payload){
    String canonicalRequest = method + '\n';
    canonicalRequest += '/' + '\n';
    canonicalRequest += queryString + '\n';

    List<String> headersList = new List<String>(headers.keySet());

    for (String header : headersList){
        String headerContent = header.toLowerCase() + ':' + headers.get(header).trim();
        canonicalRequest += headerContent + '\n';

    for (String header : headersList){
        canonicalRequest += header.toLowerCase() + ';';

    canonicalRequest = canonicalRequest.removeEnd(';');
    canonicalRequest += '\n';
    canonicalRequest += EncodingUtil.convertToHex(Crypto.generateDigest('SHA256', Blob.valueOf(payload))).toLowerCase();

    return Crypto.generateDigest('SHA256', Blob.valueOf(canonicalRequest));


public String getStringToSign(Blob canonicalRequest, Datetime requestdate, String region, String service){
    String stringToSign = 'AWS4-HMAC-SHA256' + '\n';

    String formatedDate = requestdate.formatGmt('yyyyMMdd') + 'T' + requestdate.formatGmt('hhmmss') + 'Z';
    stringToSign += formatedDate + '\n';
    stringToSign += requestdate.formatGmt('yyyyMMdd') + '/' + region + '/' + service + '/aws4_request\n'; 
    //stringToSign += String.valueOf(canonicalRequest);
    stringToSign += EncodingUtil.base64Encode(canonicalRequest);

    return stringToSign;


public String signature(String secret, String today, String region, String service, String stringToSign){
    Blob privateKey = Crypto.generateMac('HmacSHA256', Crypto.generateMac('HmacSHA256', Crypto.generateMac('HmacSHA256', Crypto.generateMac('HmacSHA256', 
                        Blob.valueOf(today), Blob.valueOf('AWS4' + secret)), 
    return EncodingUtil.convertToHex(Crypto.generateMac('HmacSHA256', Blob.valueOf(stringToSign), privateKey));

And I'm using them to create a topic in SNS like this:

public with sharing class SNS extends AWS {
    String region = 'eu-west-1';
    String endpoint = 'http://sns.' + region + '.amazonaws.com';
    String method = 'GET';
    String service = 'sns';

    public SNS(String key, String secret) {
        this.key = key;
        this.secret = secret;

    public HttpResponse createTopic(String topicName){
        HttpRequest request = new HttpRequest();


        Http http = new Http();

        try {
            return http.send(request);;
        } catch (System.CalloutException e) {
            throw e;

    private String getCreateTopicParameters(String topicName){
        String body = 'Name=' + topicName;
        body += '&Action=CreateTopic';
        body += '&SignatureMethod=HmacSHA256';
        body += '&AWSAccessKeyId=' + this.key;
        body += '&SignatureVersion=4';

        String signature = getSignature(body);

        body += '&Signature=' + signature;
        return body;

    private String getSignature(String body){
        Datetime now = Datetime.now();
        String today = now.formatGmt('yyyyMMdd');
        Map<String,String> headers = new Map<String,String>();

        Blob canonicalRequest = createCanonicalRequest(method, body, headers,'Action=CreateTopic');
        String stringToSign = getStringToSign(canonicalRequest, now, region, service);
        String signature = signature(secret, today, region, service, stringToSign);
        return signature;

If someone can point out where I went wrong, I would really appreciate it.

  • 1
    It would be awesome if you could create this as a gist. Aug 18, 2015 at 22:16
  • Hold on. I have a working version here somewhere...
    – sfdcfox
    Aug 31, 2015 at 16:05

3 Answers 3


Here's a gist that shows the basic framework that I developed. To actually implement a sub-service using this framework, you simply need a couple of extra steps, outlined here:

public class AWSS3_GetService extends AWS {
    public override void init() {
        endpoint = new Url('https://s3.amazonaws.com/');
        resource = '/';
        region = 'us-east-1';
        service = 's3';
        accessKey = 'my-key-here';
        method = HttpMethod.XGET;
        //  You can specify "payload" here if a body is required
        //  payload = Blob.valueof('some-text');

        //  This method helps prevent leaking secret key, 
        //  as it is never serialized
   public String[] getBuckets() {
       HttpResponse response = sendRequest();
       String[] results = new String[0];
       // Read response XML; if we get this far, no exception happened
       // This code was omitted for brevity
       return results;

Edit: Your code looks "similar" to mine, but there's a ton of potential pitfalls. I see you're not sorting your query string correctly (as far as I can tell), and you're using the query-string version of the authorization code (which you'll see in my code is implemented as the Authorization header). The Signature query parameter itself is probably malformed; it took me quite a bit of effort (several days, in fact), to get AWS to accept a valid request from me.

  • Awesome, thanks sfdcfox. I haven't get around to test it (way too busy last few days) yet, but I'm sure it works, so I'm going to go ahead and accept your answer.
    – Jose
    Sep 2, 2015 at 13:35
  • 1
    Great, it works. Just one thing, I was getting this error Date must be in ISO-8601 'basic format' so I had to changed the format from 'E, dd MMM YYYY HH:mm:ss z' to 'YYYYMMdd\'T\'HHmmss\'Z\'' and then it worked. I'm not sure if this an AWS wide changed, or is just for the SNS service. I'm going with the latter. Thanks again!
    – Jose
    Sep 4, 2015 at 16:37
  • @Jose That's odd. It might be specific to that service. The "date" header is supposed to be the standard HTTP-style header. It might be related to just SNS. Thanks for that heads-up, though, because I might need to use SNS at some point.
    – sfdcfox
    Sep 4, 2015 at 16:59
  • Is this possible to use Named Credentials or other means to store accessKey and secretKey in safe place?
    – Patlatus
    Jan 2, 2018 at 13:01
  • @Patlatus Named Credentials doesn't really work here, because you need access to those values. I would probably consider using a Custom Setting and encrypt the values using the Crypto class.
    – sfdcfox
    Jan 2, 2018 at 16:17

The date formatting is wrong in the method "getStringToSign". The day and month must be in 2 digits, and same for hour, minutes...

You should debug your code to ensure the string to sign is in the right format (especially the date/time):


  • Great catch, thanks! I just updated the question with the correct formatting but I still keep getting the same error.
    – Jose
    Aug 18, 2015 at 19:38
  • @Jose Community etiquette is to upvote well thought out questions and answers if someone has sufficient reputation. While the above user may not have completely solved your issue, they invested time in pointing you in the right direction to solving it by catching an error in your code that you've acknowledged. In my view, that would at least merit an upvote from you which I don't see. +1 from me.
    – crmprogdev
    Sep 1, 2015 at 17:32
  • @sfdcfox I am trying to generate v4 signature for AWS API Gateway using your reference code. But it is giving 403.I have refer steps given in below link but still getting error. docs.aws.amazon.com/general/latest/gr/sigv4_signing.html Jun 17, 2020 at 16:15

Try this getStringToSign method. I've changed the second line from the bottom.

public String getStringToSign(Blob canonicalRequest, Datetime requestdate, String region, String service){
String stringToSign = 'AWS4-HMAC-SHA256' + '\n';

String formatedDate = requestdate.formatGmt('yyyyMMdd') + 'T' + requestdate.formatGmt('hhmmss') + 'Z';
stringToSign += formatedDate + '\n';
stringToSign += requestdate.formatGmt('yyyyMMdd') + '/' + region + '/' + service + '/aws4_request\n'; 
//stringToSign += String.valueOf(canonicalRequest);
// stringToSign += EncodingUtil.base64Encode(canonicalRequest); COMMENTED OUT THIS LINE AND REPLACED WITH THE ONE BELOW
stringToSign += EncodingUtil.convertToHex(Crypto.generateDigest('SHA-256', canonicalRequest));

return stringToSign;
  • Hi Joe, thanks for the input. However, why are you parsing canonicalRequest, already a Blob value, to Blob? That line throw me an error, I tried it like stringToSign += EncodingUtil.convertToHex(Crypto.generateDigest('SHA-256', canonicalRequest)); but I keep getting the same error.
    – Jose
    Sep 2, 2015 at 13:36
  • my bad! I'd modified that line based on the class we are using in our org without noticing that your code already has canonicalRequest as a blob. The main change I was trying to highlight is to replace EncodingUtil.base64Encode with EncodingUtil.convertToHex(Crypto.generateDigest('SHA-256'
    – joe
    Sep 2, 2015 at 13:45
  • I've now edited my answer to take this into account
    – joe
    Sep 2, 2015 at 13:48

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .