I have a working functionality for sending push notifications via Firebase (FCM) from apex. But it is legacy now and I need to upgrade to HTTP v1.

Based on the official documentation and the few solutions that I've been able to find on the internet, I've been fruitlessly trying to solve this problem for two days now.

Here is my code:

public static void sendNotification(String firebaseConfigDeveloperName) {
    FCM_Settings__mdt firebaseConfig = queryFCMSettings(firebaseConfigDeveloperName);

    if (firebaseConfig != null) {

        String accessToken = generateJWT(firebaseConfig.private_key__c, firebaseConfig.client_id__c);

        HttpRequest request = prepareRequest(firebaseConfig.project_id__c, accessToken);

        Http http = new Http();
        HTTPResponse response = http.send(request);
        System.debug('response ==> ' + response);
    } else {
        System.debug('FCM_Settings__mdt not found by DeveloperName ==> ' + firebaseConfigDeveloperName);

public static FCM_Settings__mdt queryFCMSettings(String firebaseConfigDeveloperName) {
    FCM_Settings__mdt firebaseConfig = [
            SELECT client_id__c, private_key__c, project_id__c
            FROM FCM_Settings__mdt WHERE DeveloperName =:firebaseConfigDeveloperName
            LIMIT 1

    return firebaseConfig;

public static String generateJWT(String privateKey, String clientID) {
    //privateKey = 'MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCjSe21kQhGJVbP...';
    //clientID = '583536554635647655647';

    String scope = 'https://www.googleapis.com/auth/firebase.messaging';
    String aud = 'https://oauth2.googleapis.com/token';
    Long iat = DateTime.now().getTime();
    Long exp = DateTime.now().addMinutes(60).getTime();

    String jwtHeader = '{"alg":"RS256","typ":"JWT"}';
    String jwtPayload =
            '{"iss":"' + clientID + '","scope":"' + scope + '","aud":"' + aud + '","iat":' +
                    iat +
                    ',"exp":' +
                    exp +

    String encodedHeader = base64URLEncode(Blob.valueOf(jwtHeader));
    String encodedPayload = base64URLEncode(Blob.valueOf(jwtPayload));

    String signatureInput = encodedHeader + '.' + encodedPayload;

    Blob privateKeyBlob = EncodingUtil.base64Decode(privateKey);

    Blob signatureBlob = Crypto.sign('RSA-SHA256', Blob.valueOf(signatureInput), privateKeyBlob);
    String encodedSignature = base64UrlEncode(signatureBlob);

    return encodedHeader + '.' + encodedPayload + '.' + encodedSignature;

public static HttpRequest prepareRequest(String projectId, String accessToken) {
    HttpRequest request = new HttpRequest();
    request.setEndpoint('https://fcm.googleapis.com/v1/projects/' + projectId + '/messages:send');
    request.setHeader('Content-Type', 'application/json');
    request.setHeader('Authorization', 'Bearer ' + accessToken);

    String devicePushToken = '09568cmx983up98vb687yp9e85p49y5-6506b7...';
    String title = 'test title';
    String body = 'test body';

    request.setBody(generateBodyForSingleToken(devicePushToken, title, body));
    return request;

public static String generateBodyForSingleToken(String devicePushToken, String title, String body) {
    String bodyToSend =
            '{' +
                    '"message":' +
                    '{' +
                    '"token": "' + devicePushToken +
                    '", "notification": {' +
                    '"title": "' + title +
                    '", "body": "' + body +
                    '"},' +
                    '"dry_run": ' + true +
                    '}' +
    return bodyToSend;

public static String base64URLEncode(Blob input){
    String output = EncodingUtil.base64Encode(input);
    output = output.replaceAll('\\+', '-').replaceAll('\\/', '_').replaceAll('=', '');
    return output;

The code runs successfully, but I get this:

enter image description here

This is my generated private key from Firebase service account:

enter image description here

I've tried many key manipulations, because according to this instruction (which has links from many answers that I managed to find on various forums), it follows that the key may be with a different encoding. But these manipulations did not bring success: I either received errors at that moment Blob signatureBlob = Crypto.sign('RSA-SHA256', Blob.valueOf(signatureInput), privateKeyBlob); or the code was executed successfully, but still with the same result (unauthorized). Therefore, I tend to believe that everything is fine with the key and it looks like it has the pkcs8 format.

I still don't understand how to prepare jwtPayload correctly. So maybe that's the problem. It's just that I initially had an obvious typo (extra character) here and I successfully received the accessToken from generateJWT().

I'm confused. I would really appreciate your help!

P.S. Here are a few more sources that I have read, but it did not help solve my problem: click1 click2 click3 click4

  • 1
    Don't encode JSON manually (e.g. string concatenation). It's a recipe for disaster. To prove a point, you have a : in your payload that shouldn't be there. See if you can find it. It took me a moment, too. Create a Map<String, Object>, and use JSON.serialize on it. This reduces the likelihood of mistakes. It is possible that this typo is resulting in a token not being returned.
    – sfdcfox
    Commented May 10 at 19:57
  • @sfdcfox oh, yes, I suppose that's what you mean "dry_run": true? But that's not the point, unfortunately, I checked... I think that in this case I would have received a successful response status to the request, but there would have been error details regarding the failure sending the push notification. Commented May 10 at 20:45
  • 1
    I still don't understand how to prepare jwtPayload correctly - this would be a good step 1, yes. Once you do understand this, you can use out of the box Apex library or Named/External Credentials to make the request. You'll need to learn some more about key formats.
    – identigral
    Commented May 11 at 2:45


You must log in to answer this question.

Browse other questions tagged .