この頁は英語の原文の参考訳です。内容が異なる場合は原文が優先します。

ガイド

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 に要る項目、そして時間を取られたエラーを書きます。

資格情報を用意する

  1. Apple Ads の管理者が Account Settings → User Management を開き、API の役割を持つ利用者を招待します(書き込むなら API Account Manager、報告だけなら読み取り専用の役割)。人のログインを使い回さず、API 専用の利用者を作ります。
  2. 下の 2 つのコマンドで、P-256 の鍵の対を自分の機械で作ります。秘密鍵は手元から出しません。Apple に渡すのは公開鍵だけです。
  3. API の利用者で Apple Ads にサインインし、Account Settings → API を開いて、public-key.pem の中身を貼ります。
  4. 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.pem

client secret

client secret は秘密鍵で署名する JWT です。項目ごとに確かめられ、どこか 1 つでも違うと invalid_client が返ります。

項目値
header.algES256
header.kidAPI の設定画面の keyId
subclientId
issteamId
audhttps://appleid.apple.com
iat今の時刻(秒)
expiat から最長 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 を使います。

v5 が止まると何が変わるか →

Loop Marketing ではどうなるか

Loop Marketing は同じ 4 つの値を受け取り、トークンを取るたびに client secret を作り直し、秘密鍵はワークスペースごとに暗号化して保管します。そのうえで Apple Ads の口座から支出とインストールを読み、予算と入札はあなたが決めた範囲の中だけで変えます。

無料で試せます。始めるのにクレジットカードは要りません。