この頁は英語の原文の参考訳です。内容が異なる場合は原文が優先します。
ガイド
Apple Ads API の認証:client secret、アクセストークン、その間のエラー
Apple Ads の API は API キーを受け付けません。どの要求にも 1 時間で切れる OAuth のアクセストークンが要り、それを得る道は 1 つだけです。自分で作った秘密鍵で短い JWT(client secret)に署名し、appleid.apple.com で交換します。
この流れは v5 が止まっても変わりません。同じ資格情報と同じトークンが Apple Ads Platform API でも通ります。空の口座から最初の要求が通るまでの道筋、client secret に要る項目、そして時間を取られたエラーを書きます。
資格情報を用意する
- Apple Ads の管理者が Account Settings → User Management を開き、API の役割を持つ利用者を招待します(書き込むなら API Account Manager、報告だけなら読み取り専用の役割)。人のログインを使い回さず、API 専用の利用者を作ります。
- 下の 2 つのコマンドで、P-256 の鍵の対を自分の機械で作ります。秘密鍵は手元から出しません。Apple に渡すのは公開鍵だけです。
- API の利用者で Apple Ads にサインインし、Account Settings → API を開いて、public-key.pem の中身を貼ります。
- Apple が clientId・teamId・keyId の 3 つを表示します。秘密鍵と一緒に保管します。この 4 つで資格情報のすべてです。
鍵の対(1 回だけ実行し、private-key.pem は秘密にする):
openssl ecparam -genkey -name prime256v1 -noout -out private-key.pem
openssl ec -in private-key.pem -pubout -out public-key.pemclient secret
client secret は秘密鍵で署名する JWT です。項目ごとに確かめられ、どこか 1 つでも違うと invalid_client が返ります。
| 項目 | 値 |
|---|---|
header.alg | ES256 |
header.kid | API の設定画面の keyId |
sub | clientId |
iss | teamId |
aud | https://appleid.apple.com |
iat | 今の時刻(秒) |
exp | iat から最長 180 日。期限を管理するより、トークンを取るたびに作り直すほうが簡単 |
これを https://appleid.apple.com/auth/oauth2/token へフォームで POST します(grant_type=client_credentials、scope=searchadsorg)。Node 18 以上、依存なし:
import { createSign, randomUUID } from "node:crypto";
const b64url = (v) => Buffer.from(v).toString("base64url");
function clientSecret({ clientId, teamId, keyId, privateKeyPem }) {
const now = Math.floor(Date.now() / 1000);
const head = b64url(JSON.stringify({ alg: "ES256", kid: keyId }));
const body = b64url(JSON.stringify({
sub: clientId,
iss: teamId,
aud: "https://appleid.apple.com",
iat: now,
exp: now + 3600,
jti: randomUUID(),
}));
const sig = createSign("sha256")
.update(`${head}.${body}`)
.sign({ key: privateKeyPem, dsaEncoding: "ieee-p1363" });
return `${head}.${body}.${b64url(sig)}`;
}
export async function accessToken(creds) {
const res = await fetch("https://appleid.apple.com/auth/oauth2/token", {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: creds.clientId,
client_secret: clientSecret(creds),
scope: "searchadsorg",
}),
});
if (!res.ok) throw new Error(`token: ${res.status} ${await res.text()}`);
return res.json(); // { access_token, token_type, expires_in }
}要求を送る
- Authorization: Bearer <access_token> を付けます。トークンは expires_in 秒(1 時間)で切れます。更新用のトークンは無いので、切れたら取り直します。
- GET /acls は口座の文脈なしで呼べ、API の利用者が触れる物を一覧にします。文脈のヘッダに入れる id はここで探します。
- ほかの呼び出しには、v5 なら X-AP-Context: orgId=…、Platform API なら adAccountId=… が要ります。
別の原因に見えるエラー
どれも invalid_client か読み込みのエラーとしか返らず、原因を指す手がかりがありません。
見えること: 項目は全部正しいのに invalid_client
本当の原因: 署名が DER 形式になっています。JWT の ES256 は 64 バイトの r‖s をそのまま並べた形です。Node の crypto は既定で DER を返すので dsaEncoding: "ieee-p1363" を渡します。Python の cryptography も DER を返します。JWT のライブラリは変換してくれますが、自前のコードはしません。
見えること: 秘密鍵が本番でだけ読み込めない
本当の原因: PEM を環境変数や秘密の保管庫に入れたとき、改行が \n という 2 文字になっています。署名の前に改行へ戻します。
見えること: 鍵を替えた直後から invalid_client
本当の原因: keyId は公開鍵を上げるたびに変わります。新しい鍵の対には新しい keyId をヘッダに入れないと、Apple は古い公開鍵で確かめます。
見えること: 1 台のサーバーからだけ invalid_client
本当の原因: その機械の時計がずれていて、iat が未来、または exp が過去になっています。鍵を疑う前に時計を合わせます。
見えること: トークンは取れるのに、campaign の呼び出しが全部失敗する
本当の原因: 文脈のヘッダが無いか、API の利用者に渡されていない org を指しています。同じトークンで GET /acls を読み、そこにある id を使います。
Loop Marketing ではどうなるか
Loop Marketing は同じ 4 つの値を受け取り、トークンを取るたびに client secret を作り直し、秘密鍵はワークスペースごとに暗号化して保管します。そのうえで Apple Ads の口座から支出とインストールを読み、予算と入札はあなたが決めた範囲の中だけで変えます。
無料で試せます。始めるのにクレジットカードは要りません。