1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

freee x SalesforceをAPIで連携してみた

Last updated at Posted at 2025-01-12

freee APIでSalesforceと連携

freee APIを利用して、Salesforceで帳票の項目を入力し、ApexでPOST メソッドをCalloutしてfreee内で請求書を作成します。

CleanShot 2025-01-11 at 18.09.24.gif

手順

  1. freee開発者環境を作成 (freee)
  2. freee API Referenceで試す (freee)
  3. Salesforce用のアプリを作成 (freee)
  4. Auth. Providerを設定 (Salesforce)
  5. External Credentialsを設定 (Salesforce)
  6. Named Credetialsを設定 (Salesforce)
  7. ApexでCalloutし、POSTするメソッドを作成 (Salesforce)
  8. LWCでSalesforceからfreeeの取引先(Partner)を作成(Salesforce)

freee開発者環境を作成

freee開発者環境は無料で作成できます。開発者環境で遠慮なく請求書を作成してみます。以下のサイトに丁寧なチュートリアルがあるのでおすすめです。
https://developer.freee.co.jp/startguide

また開発者アプリ一覧から、API Referenceからテストするためのアプリと、Salesforceから接続するアプリを別途管理していると便利です。私は、アプリ自体は Salesforce Connector と名づけ、API Reference ver.とSalesforce ver.に分けました。基本は同じでCallback URLが違います。
https://app.secure.freee.co.jp/developers/applications

CleanShot 2025-01-11 at 18.20.55@2x.png

Salesforce Connector (API Reference) ver.
Callback URLが、urn:ietf:wg:oauth:2.0:oob になります。アプリ名と概要は任意です。Client IDClient Secretは後ほど、Customer Keyを取得する際に利用しますので、メモっておくかこちらから常に取得できるようにしておいてください。

CleanShot 2025-01-11 at 18.23.45@2x.png

freee API Referenceで試す (freee)

それでは、まずAPI Referenceでテストしてみましょう。
https://developer.freee.co.jp/reference/accounting/reference

API Callはプランによって1日の制限数がありますが、大抵の普通の利用では、使い切ることはないでしょう。
CleanShot 2025-01-11 at 18.28.01@2x.png

AuthorizeにてTokenを入力する

スクロールしていくとAuthorizeするボタンが出てくるので、クリックを押します。
CleanShot 2025-01-11 at 18.32.36@2x.png
するとTokenを入れろと出てくるので、Tokenが必要になります。Tokenの取得は、こちらのURLから。

https://accounts.secure.freee.co.jp/public_api/authorize?client_id={Client_ID}&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&response_type=token
{Client_ID}は先ほどのアプリに表示されているClient IDを入力します。入力すると、freeeへログインを促されたのち、以下のようにアプリに接続許可を与えます。
CleanShot 2025-01-11 at 18.36.45@2x.png

与えると、次は無事アクセス用のTokenが発行されます。こちらを、先ほどのAuthorizeの際に求められる箇所へ入力することで、API Referenceページからテストランができるようになります。
CleanShot 2025-01-11 at 18.38.15@2x.png

このように入力します。Authorizeボタンを押して後、CloseでOKです。
CleanShot 2025-01-11 at 18.39.52@2x.png

まず試しに、接続している組織のデータを取得してみましょう。
Companies 事業所のセクションへ移動して、Try it out ボタンを押してみます。
CleanShot 2025-01-11 at 18.41.58@2x.png

company_idを取得する

すると、接続している事業所の情報が返ってきます。便利ですね。ここで、取得するidが後ほど利用するcompany_idになるのでメモしておきます。

CleanShot 2025-01-11 at 18.43.41@2x.png

以上で、まず開発環境でアプリを作成して、API Referenceのページからアクセストークンを入力して、組織に入れることを確認しました。

Salesforce用のアプリを作成 (freee)

では同様にSalesforceからアクセスするためのアプリを作成します。ここで取得するClient IdとClient Secretを次のAuth. Providerを設定で利用します。 アプリは「プライベート」です。

CleanShot 2025-01-12 at 19.59.28@2x.png

Client IdとClient Secretを取得する

すると、また別のClient IdとClient Secretが表示されるのでメモして、次の「Auth. Providerを設定 (Salesforce)」で利用します。

CleanShot 2025-01-12 at 20.03.23@2x.png

Auth. Providerを設定 (Salesforce)

次は、Salesforceから同様にアクセするための設定を行います。まず、Auth. Providerの登録です。

Provider Type Open ID Connect
Name freeeDev
Consumer Key {Client_ID}
Consumer Secret {Client_Secret}
Authorize Endpoint URL https://accounts.secure.freee.co.jp/public_api/authorize
Token Endpoint URL https://accounts.secure.freee.co.jp/public_api/token

名前は任意でつけてください。私はfreeeDevつけました。Client IDClient SecretSalesforce用のアプリを作成 (freee)の中で表示されるアプリの設定に自動作成されているものを使います。

CleanShot 2025-01-11 at 22.13.07@2x.png

Callback URLを取得する

Callback URLをコピペして、Salesforce Connector アプリのCallback URLへ入力します。こうすることによって、Salesforceからアクセスできようになります。

CleanShot 2025-01-12 at 19.51.53@2x.png

Callback URLを変更する

先ほど作成したSalesforce ConnectorアプリのCallback URLを、Auth. Providerを作成した際に表示されてCallback URLに置き換えます。この作業が大事です。
CleanShot 2025-01-12 at 19.56.05@2x.png

External Credentialsを設定 (Salesforce)

次は、freeeは外部のサービスなので、External Credentialsを作成します。Named Credentailsのセクションで、External Credentialsのタブにいき、Newを押します。
CleanShot 2025-01-11 at 22.32.12@2x.png

Auth. Providerを選択

次に作成する際は、以下の設定でExternal Credentialsを作成します。Identity Providerは先ほど作成したAuth. Providerを選択します。
CleanShot 2025-01-11 at 22.34.26@2x.png

Named Credentialsを作成

次に、Named Credentialsを作成します。Named CrdentialsのセクションでNewを選択し、URLにapiのendpointを入力します。会計freeeの場合はhttps://api.freee.co.jpですが、請求書freeeの場合は、https://api.freee.co.jp/ivになります。
CleanShot 2025-01-11 at 22.36.35@2x.png

PrincipalをNamed CredentialsにしてConfiguredする

結果、一つのExternal Credentialが、Auth. Providerを利用して、2つのNamed Credentialsを管理するこのような形になります。これで、会計freeeにも請求書freeeにも一つのExternal Credentialsでアクセスできることになります。管理が楽ですね。
CleanShot 2025-01-11 at 22.41.34@2x.png

ApexでCalloutし、POSTするメソッドを作成 (Salesforce)

ApexでCalloutして取引先を作成しますが、いつものAnonyous Windowでテストできます。

@TestVisible
private static String endpoint = 'callout:freeeDev' + '/api/1/partners';

Map<String,Object> jsonMap = new Map<String,Object>{
    'company_id'   => {company_id},
    'name'         => '新しい取引先 1',
    'contact_name' => '営業担当',
    'email'        => 'contact1@example1.com'
};

// Convert the map to a JSON string
String jsonString = JSON.serialize(jsonMap);
String requestBody = jsonString;

HttpRequest req = new HttpRequest();
req.setEndpoint(endpoint);
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setTimeout(120000);
req.setBody(requestBody);
Http http = new Http();
HttpResponse res = http.send(req);
Map<String, Object> response = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
system.debug('response:' + response);

companyIdは、組織のIdになりますが、今回はCustom Metadataに保存していて、Queryで取得して充てています。Sandbox ver.と分けて管理してもいいです。

POST メソッドでrequestBodyをjsonに変換して送信することで、無事取引先が作成されました。responseは逆にdeserializeすることで、いい感じにsystem.debug()で閲覧できるようになります。

CleanShot 2025-01-12 at 14.26.56.gif

LWCでSalesforceからfreeeの取引先(Partner)を作成(Salesforce)

というわけで、Open Execute Anonymous Windowで成功すれば、あとはApexとLWCでそれを実装するのみです。

Files

apex
public with sharing class FreeeApiController {    
    @AuraEnabled
    public static Map<String, Object> createPartner(String requestBody) {
        HttpRequest req = new HttpRequest();
        String endpoint = 'callout:freeeDev' + '/api/1/partners';
        req.setEndpoint(endpoint);
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setTimeout(120000); 
        req.setBody(requestBody);
        Http http = new Http();
        HttpResponse res = http.send(req);
        Map<String, Object> response = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
        system.debug('response: ' + response);
        return response;
    }

    @AuraEnabled
    public static String getCompanyId() {
        List<freee__mdt> metaData = [SELECT Id__c FROM freee__mdt WHERE DeveloperName = 'companyId' LIMIT 1];
        return metaData[0].Id__c;
    }
}


html
<template>
    <lightning-card hide-header="true">
        <template lwc:if={isFreeePartner}>
            <div class="slds-grid slds-grid_vertical-align-center slds-p-around_x-small">
                <lightning-formatted-text class="slds-card__header-title slds-m-right_x-small"
                    value={account_partner_name}></lightning-formatted-text>
                <a href={externalUrl} target="_blank"><lightning-icon icon-name="utility:new_window" size="x-small"
                        alternative-text="open freee window"></lightning-icon></a>
            </div>
        </template>
        <template lwc:else>
            <div class="slds-grid slds-grid_vertical slds-p-around_small" style="position:relative;">
                <lightning-input label="Name" name="name" value={name} onchange={handleChange}></lightning-input>
                <lightning-input label="Email" name="email" value={email} onchange={handleChange}></lightning-input>
                <div class="slds-grid slds-gutters">
                    <lightning-combobox placeholder="都道府県" class="slds-col slds-size_1-of-2" label="Prefecture"
                        name="prefecture" value={prefecture} options={prefectureOptions}
                        onchange={handleChange}></lightning-combobox>
                    <lightning-input placeholder="郵便番号" class="slds-col slds-size_1-of-2" label="Postal Code"
                        name="postalCode" value={postalCode} onchange={handleChange}></lightning-input>
                </div>
                <lightning-input label="Address" placeholder="市区町村・番地" name="address1" value={address1}
                    onchange={handleChange}></lightning-input>
                <lightning-input label="Building" placeholder="建物名・部屋番号など" name="address2" value={address2}
                    onchange={handleChange}></lightning-input>
                <lightning-button class="slds-m-vertical_x-small" label="Create Partner"
                    onclick={handleCreatePartner}></lightning-button>
                <template if:true={isLoading}>
                    <lightning-spinner alternative-text="Loading" size="x-small"></lightning-spinner>
                </template>
            </div>
        </template>
    </lightning-card>
</template>
js
import { LightningElement, wire, api } from 'lwc';
import createPartner from '@salesforce/apex/FreeeApiController.createPartner';
import getCompanyId from '@salesforce/apex/FreeeApiController.getCompanyId';
import { getRecord, updateRecord } from 'lightning/uiRecordApi';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';

export default class FreeeConnectorDemo extends LightningElement {
    @api recordId;
    companyId;
    accountData;
    phone;
    website;
    email;
    name;
    postalCode;
    prefecture;
    address1;
    address2;
    isFreeePartner = false;
    isLoading = false;
    externalUrl;
    account_partner_name;

    async connectedCallback() {
        this.companyId = await getCompanyId();
    }

    get address_attributes() {
        return {
            zipcode: this.postalCode,
            prefecture_code: this.prefecture,
            street_name1: this.address1,
            street_name2: this.address2
        };
    };

    get requestBody() {
        return JSON.stringify({
            name: this.name,
            email: this.email,
            company_id: Number(this.companyId),
            contact_name: this.name,
            address_attributes: this.address_attributes
        });
    }

    fields = [
        'Account.Name',
        'Account.Phone',
        'Account.Website',
        'Account.BillingStreet',
        'Account.BillingCity',
        'Account.BillingState',
        'Account.BillingPostalCode',
        'Account.Partner_Name__c',
        'Account.Partner_Id__c'
    ];

    @wire(getRecord, { recordId: '$recordId', fields: '$fields' })
    account({ error, data }) {
        if (data) {
            this.accountData = Object.keys(data.fields).reduce((acc, fieldName) => {
                acc[fieldName] = data.fields[fieldName].value;
                return acc;
            }, {});
            if (this.accountData['Partner_Id__c']) {
                this.isFreeePartner = true;
                this.externalUrl = 'https://settings.secure.freee.co.jp/partners/' + this.accountData['Partner_Id__c'];
                this.account_partner_name = this.accountData['Partner_Name__c'];
                return;
            }
            this.phone = this.accountData['Phone'];
            this.website = this.accountData['Website'];
            this.name = this.accountData['Name'];
            this.postalCode = this.accountData['BillingPostalCode'];
            this.prefecture = this.prefectureOptions.find(option => option.label === this.accountData['BillingState']).value;
            this.address1 = this.accountData['BillingCity'] + this.accountData['BillingStreet'];
        } else if (error) {
            console.error('Error fetching account:', error);
        }
    }

    handleChange(event) {
        this[event.target.name] = event.target.value;
    }

    handleCreatePartner() {
        this.isLoading = true;
        createPartner({ requestBody: this.requestBody })
            .then(response => {
                console.log('created:', JSON.stringify(response));
                this.isFreeePartner = true;
                this.isLoading = false;
                this.externalUrl = 'https://settings.secure.freee.co.jp/partners/' + response.partner.id;
                this.account_partner_name = response.partner.name;

                const fields = {};
                fields['Id'] = this.recordId;
                fields['Partner_Id__c'] = response.partner.id;
                fields['Partner_Name__c'] = response.partner.name;
                const recordInput = { fields };
                console.log('recordInput:', JSON.stringify(recordInput));
                updateRecord(recordInput)
                    .then(() => {
                        console.log('Account updated');
                    })
                    .catch(error => {
                        console.error('Error updating account:', error);
                    });

                this.dispatchEvent(
                    new ShowToastEvent({
                        title: 'Success',
                        message: 'Partner created successfully',
                        variant: 'success'
                    })
                );
            })
            .catch(error => {
                console.error('Error creating partner:', error);
                this.isLoading = false;
                this.dispatchEvent(
                    new ShowToastEvent({
                        title: 'Error',
                        message: 'Error creating partner',
                        variant: 'error'
                    })
                );
            });
    }

    prefectureOptions = [
        { label: '北海道', value: 0 },
        { label: '青森県', value: 1 },
        { label: '岩手県', value: 2 },
        { label: '宮城県', value: 3 },
        { label: '秋田県', value: 4 },
        { label: '山形県', value: 5 },
        { label: '福島県', value: 6 },
        { label: '茨城県', value: 7 },
        { label: '栃木県', value: 8 },
        { label: '群馬県', value: 9 },
        { label: '埼玉県', value: 10 },
        { label: '千葉県', value: 11 },
        { label: '東京都', value: 12 },
        { label: '神奈川県', value: 13 },
        { label: '新潟県', value: 14 },
        { label: '富山県', value: 15 },
        { label: '石川県', value: 16 },
        { label: '福井県', value: 17 },
        { label: '山梨県', value: 18 },
        { label: '長野県', value: 19 },
        { label: '岐阜県', value: 20 },
        { label: '静岡県', value: 21 },
        { label: '愛知県', value: 22 },
        { label: '三重県', value: 23 },
        { label: '滋賀県', value: 24 },
        { label: '京都府', value: 25 },
        { label: '大阪府', value: 26 },
        { label: '兵庫県', value: 27 },
        { label: '奈良県', value: 28 },
        { label: '和歌山県', value: 29 },
        { label: '鳥取県', value: 30 },
        { label: '島根県', value: 31 },
        { label: '岡山県', value: 32 },
        { label: '広島県', value: 33 },
        { label: '山口県', value: 34 },
        { label: '徳島県', value: 35 },
        { label: '香川県', value: 36 },
        { label: '愛媛県', value: 37 },
        { label: '高知県', value: 38 },
        { label: '福岡県', value: 39 },
        { label: '佐賀県', value: 40 },
        { label: '長崎県', value: 41 },
        { label: '熊本県', value: 42 },
        { label: '大分県', value: 43 },
        { label: '宮崎県', value: 44 },
        { label: '鹿児島県', value: 45 },
        { label: '沖縄県', value: 46 }
    ];
}

CleanShot 2025-01-12 at 19.18.48.gif

解説

ファイル内での要点をいくつかまとめて解説します。

Apex

getCompanyIdメソッド
@AuraEnabled
public static String getCompanyId() {
    List<freee__mdt> metaData = [SELECT Id__c FROM freee__mdt WHERE DeveloperName = 'companyId' LIMIT 1];
    return metaData[0].Id__c;
}

getCompanyId()というMethodを作成しています。こちらはcompany_idをベタ打ちしないためのもので、簡易に実装する場合は下手打ちで試されても構いません。

createPartnerメソッド
@AuraEnabled
public static Map<String, Object> createPartner(String requestBody) {
    HttpRequest req = new HttpRequest();
    String endpoint = 'callout:freeeDev' + '/api/1/partners';
    req.setEndpoint(endpoint);
    req.setMethod('POST');
    req.setHeader('Content-Type', 'application/json');
    req.setTimeout(120000); 
    req.setBody(requestBody);
    Http http = new Http();
    HttpResponse res = http.send(req);
    Map<String, Object> response = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
    system.debug('response: ' + response);
    return response;
}

こちらはrequestBodyを直接パラメーターとして渡して、http requestに設定しています。戻り値はMapで返してデバッグや戻り値を利用する際に使いやすい形で返しています。

js

getterでrequestBodyを動的に設定

get requesetBody
get address_attributes() {
    return {
        zipcode: this.postalCode,
        prefecture_code: this.prefecture,
        street_name1: this.address1,
        street_name2: this.address2
    };
};

get requestBody() {
    return JSON.stringify({
        name: this.name,
        email: this.email,
        company_id: Number(this.companyId),
        contact_name: this.name,
        address_attributes: this.address_attributes
    });
}
getterとして、requestBody, それが内包するaddress_attributesについて自動で設定します。freeeが取引先を作成する際のパラメータや項目に関しては、こちらで確認できます。

CleanShot 2025-01-12 at 19.30.52@2x.png

prefectureOptionsの設置

こちらは、都道府県を番号へ置き換えるためのデータセットです。別途jsファイルでexportしてもいいですが、簡易的に同コンポーネント内に記述しています。再利用する場合は別途、jsで各所importしてもいいでしょう。

prefectureOptions
   prefectureOptions = [
        { label: '北海道', value: 0 },
        { label: '青森県', value: 1 },
        { label: '岩手県', value: 2 },
        { label: '宮城県', value: 3 },
        { label: '秋田県', value: 4 },
        { label: '山形県', value: 5 },
        { label: '福島県', value: 6 },
        { label: '茨城県', value: 7 },
        { label: '栃木県', value: 8 },
        { label: '群馬県', value: 9 },
        { label: '埼玉県', value: 10 },
        { label: '千葉県', value: 11 },
        { label: '東京都', value: 12 },
        { label: '神奈川県', value: 13 },
        { label: '新潟県', value: 14 },
        { label: '富山県', value: 15 },
        { label: '石川県', value: 16 },
        { label: '福井県', value: 17 },
        { label: '山梨県', value: 18 },
        { label: '長野県', value: 19 },
        { label: '岐阜県', value: 20 },
        { label: '静岡県', value: 21 },
        { label: '愛知県', value: 22 },
        { label: '三重県', value: 23 },
        { label: '滋賀県', value: 24 },
        { label: '京都府', value: 25 },
        { label: '大阪府', value: 26 },
        { label: '兵庫県', value: 27 },
        { label: '奈良県', value: 28 },
        { label: '和歌山県', value: 29 },
        { label: '鳥取県', value: 30 },
        { label: '島根県', value: 31 },
        { label: '岡山県', value: 32 },
        { label: '広島県', value: 33 },
        { label: '山口県', value: 34 },
        { label: '徳島県', value: 35 },
        { label: '香川県', value: 36 },
        { label: '愛媛県', value: 37 },
        { label: '高知県', value: 38 },
        { label: '福岡県', value: 39 },
        { label: '佐賀県', value: 40 },
        { label: '長崎県', value: 41 },
        { label: '熊本県', value: 42 },
        { label: '大分県', value: 43 },
        { label: '宮崎県', value: 44 },
        { label: '鹿児島県', value: 45 },
        { label: '沖縄県', value: 46 }
    ];

まとめ

以上で、簡易的にfreee とSalesforceをAPIで連携してみました。会計freeeと請求書freeeはurlが違うので、そこを注意していれば意外とあっさり繋がります。Client_Id, Client_SecretとCallbackですが、API Referenceから呼ぶときとSalesforceから呼ぶときでは違うのでそこの設定だけ注意が必要です。

請求書APIについては詳細に書きませんが、要領は会計freeeと同じです。使用はこちらのAPI referenceを参照にしてください。
https://developer.freee.co.jp/reference/iv/reference

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?