抖音开放平台调用手册

网站扫码授权后换取用户凭证

在自己的服务端校验授权回调后,用一次性 code、ClientKey 和 ClientSecret 交换用户 access_token。

官方资料核对 2026-10-15示例代码,未进行真实接口实测
← 返回平台功能

接入准备

  • 组织主体完成网站应用创建和审核,应用已取得 user_info 权限,按官方要求设置自己的 HTTPS 授权回调。
  • 准备自己的服务端登录会话与回调处理;同一次授权生成随机 state 并保存在会话中,回调先校验 state、用户会话与实际 scopes,再交换 code。下方不是完整登录网站。
  • Python 3 标准库环境;ClientSecret 与凭证存储只在服务端,DY_TOKEN_FILE 指向已存在的私有目录。

准备环境变量

示例值需要替换为自己的凭证或资源 ID。代码在本机或服务端运行;密钥不应放进公开网页。

DY_CLIENT_KEY
当前网站应用 ClientKey。
DY_CLIENT_SECRET
同一应用的服务端密钥,不属于授权 URL 参数。
DY_AUTH_CODE
自己的回调完成 state 和会话校验后得到的未消费 code。
DY_TOKEN_FILE
私有服务端目录中本用户的凭证文件;请求前预建 0600 临时文件确认可写,成功后原子替换。
终端
export DY_CLIENT_KEY='tt_REPLACE_WITH_CLIENT_KEY'
export DY_CLIENT_SECRET='REPLACE_WITH_SERVER_SECRET'
export DY_AUTH_CODE='REPLACE_WITH_VALIDATED_CODE'
export DY_TOKEN_FILE='/secure/douyin-user-token.json'

以上 export 适用于 macOS / Linux 的终端。按调用步骤保存代码文件,再在同一终端执行。

调用步骤

  1. 01

    在浏览器打开 https://open.douyin.com/platform/oauth/connect?client_key=自己的ClientKey&response_type=code&scope=user_info&redirect_uri=已URL编码的HTTPS回调&state=与会话绑定的随机值 。不要用后台 HTTP 请求代替用户扫码;ClientSecret 不能放进该链接。

  2. 02

    用户同意后回到自己的回调;验证一次性 state 后读取 code 和实际 scopes。回调 URL 不携带自定义 query,缺少 user_info 同意时停止资料调用。

  3. 03

    将已校验的 code、应用凭证和私有文件路径设置为下方环境变量;保存 Python 代码为 token.py,运行 python3 token.py。授权码仅有效 10 分钟且仅能用一次。

  4. 04

    代码在请求前检查全部环境变量并预建 0600 临时文件;成功保存 client_key、open_id、received_at、access_expires_at 和 refresh_expires_at。后续按绝对截止时间检查,切勿把整份 token 文件上传到版本库。

调用 01

以表单交换授权码

POST 表单请求,不是 JSON,也不是小程序 tt.login 的登录 code。下方请求体仅展示字段,完整代码从环境变量读取实际敏感值。

POSThttps://open.douyin.com/oauth/access_token/

请求头

Content-Type
application/x-www-form-urlencoded

参数说明

参数表可左右滑动,查看完整说明。

字段 / 位置类型 / 必填说明
client_key请求体string必填发起授权所用网站应用 ClientKey。
client_secret请求体string必填同一应用 ClientSecret,只在服务端取用。
code请求体string必填校验过 state 的一次性用户授权码,10 分钟内使用。
grant_type请求体string必填固定 authorization_code。

调用代码

替换示例 ID 与请求体内容后运行。

Python 3
import json
import os
import time
import tempfile
from pathlib import Path
from contextlib import contextmanager
from urllib.parse import urlencode
import urllib.error
import urllib.request

def env(name):
    value = os.environ.get(name)
    if not value or not value.strip() or "REPLACE_WITH" in value:
        raise SystemExit("请设置真实环境变量: " + name)
    return value

def positive_int(value, name):
    if isinstance(value, bool):
        raise SystemExit("无效期限字段: " + name)
    try:
        number = int(value)
    except (TypeError, ValueError):
        raise SystemExit("无效期限字段: " + name)
    if number <= 0:
        raise SystemExit("无效期限字段: " + name)
    return number

def load_record(path):
    try:
        record = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, ValueError):
        raise SystemExit("无法读取原凭证文件;请检查路径与安全存储格式")
    if not isinstance(record, dict):
        raise SystemExit("原凭证文件格式错误")
    return record

@contextmanager
def pending_file(value):
    path = Path(value).expanduser().absolute()
    if path.is_symlink() or (path.exists() and not path.is_file()):
        raise SystemExit("凭证目标必须是私有目录中的普通文件,不能是目录或符号链接")
    # 先创建 0600 临时文件并确认可写,再消费授权码或发送刷新请求。
    # 这能前置发现路径配置错误;不能保证请求后永不发生磁盘或系统故障。
    try:
        output = tempfile.NamedTemporaryFile("w", encoding="utf-8",
            dir=path.parent, prefix=".douyin-", delete=False)
    except OSError:
        raise SystemExit("无法在凭证目录创建文件;尚未发送请求")
    try:
        os.chmod(output.name, 0o600)
        output.flush()
        os.fsync(output.fileno())
        yield path, output
    finally:
        output.close()
        try:
            os.unlink(output.name)
        except FileNotFoundError:
            pass

def post(url, payload):
    request = urllib.request.Request(url, data=payload,
        headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
    try:
        with urllib.request.urlopen(request, timeout=30) as response:
            result = json.load(response)
    except urllib.error.HTTPError as error:
        raise SystemExit("HTTP " + str(error.code) + ";不输出可能含凭证的原文")
    except (urllib.error.URLError, TimeoutError):
        raise SystemExit("网络或超时错误;不要盲目重复消费一次性授权码")
    except (ValueError, UnicodeError):
        raise SystemExit("响应不是有效 JSON,原凭证文件未覆盖")
    if not isinstance(result, dict) or not isinstance(result.get("data"), dict):
        raise SystemExit("响应缺少 data,原凭证文件未覆盖")
    data = result["data"]
    if str(data.get("error_code")) != "0" or not data.get("access_token"):
        raise SystemExit("业务请求失败,业务码: " + str(data.get("error_code")))
    if not isinstance(data.get("open_id"), str) or not data["open_id"]:
        raise SystemExit("响应缺少用户归属,原凭证文件未覆盖")
    return data

def commit(path, output, record):
    json.dump(record, output, ensure_ascii=False)
    output.flush()
    os.fsync(output.fileno())
    output.close()
    os.replace(output.name, path)
    print("凭证已安全保存;请按绝对截止时间检查有效期,不输出凭证")

# 所有配置先读取和检查,缺失项不能在 code 已消费后才被发现。
client_key = env("DY_CLIENT_KEY")
client_secret = env("DY_CLIENT_SECRET")
auth_code = env("DY_AUTH_CODE")
token_file = env("DY_TOKEN_FILE")
with pending_file(token_file) as (path, output):
    previous = load_record(path) if path.exists() else None
    if previous and (previous.get("client_key") != client_key or not previous.get("open_id")):
        raise SystemExit("原凭证文件缺少归属或属于其他应用;改用该用户的独立文件")
    body = urlencode({"client_key": client_key, "client_secret": client_secret,
                      "code": auth_code, "grant_type": "authorization_code"}).encode("utf-8")
    started_at = int(time.time())
    data = post("https://open.douyin.com/oauth/access_token/", body)
    received_at = int(time.time())
    if previous and previous["open_id"] != data["open_id"]:
        raise SystemExit("返回用户与原文件不一致;没有覆盖原用户凭证")
    if not isinstance(data.get("refresh_token"), str) or not data["refresh_token"]:
        raise SystemExit("响应缺少刷新凭证,原文件未覆盖")
    # 以请求开始时刻加服务端时长,避免把网络耗时计入可用期限。
    record = dict(data, client_key=client_key, received_at=received_at,
        access_expires_at=started_at + positive_int(data.get("expires_in"), "expires_in"),
        refresh_expires_at=started_at + positive_int(data.get("refresh_expires_in"), "refresh_expires_in"))
    if min(record["access_expires_at"], record["refresh_expires_at"]) <= received_at:
        raise SystemExit("响应到达时期限已过,原文件未覆盖")
    commit(path, output, record)

返回示例

成功数据节选;expires_in 等为秒,示意值来自文档结构,实际有效期以当次响应为准。 以下为结构示意,非本站实测结果。

JSON
{
  "data": {
    "error_code": 0,
    "access_token": "ACT_EXAMPLE_NOT_A_REAL_TOKEN",
    "expires_in": 1296000,
    "open_id": "OPEN_ID_EXAMPLE",
    "refresh_token": "RFT_EXAMPLE_NOT_A_REAL_TOKEN",
    "refresh_expires_in": 2592000,
    "scope": "user_info"
  },
  "message": "success"
}

常见错误与排查

10007授权码失效或已消费
重新让用户主动授权;不要重放旧 code。
10003 / 10013 / 10014应用密钥或 ClientKey 不匹配
核对 ClientKey、ClientSecret 与发起授权使用的应用。
10002参数或编码错误
检查四个表单字段并使用 application/x-www-form-urlencoded。

调用注意

  • HTTP 200 仍检查 data.error_code;不把业务错误当作已登录。
  • 示例仅保存凭证,不创建自己的业务会话;登录产品必须把校验后的授权结果绑定回发起授权的会话,不能信任前端任意传来的 open_id。

注意事项

  • 客户端只取得授权码,交换和存储凭证均在服务端完成。
  • received_at 是本机接收时间;绝对截止时间按请求开始时间加响应时长保守计算。现存凭证文件要有 client_key/open_id,且只能覆盖同一应用同一用户;错误和失败临时文件不会覆盖原文件。
  • 用不同文件或数据库主键隔离应用与用户;此示例没有提供集群锁、加密凭证库或定时刷新任务。
  • 授权 URL 构造与扫码需在自己的网页和回调中集成,本站未进行真实扫码授权。

核验来源

本站按官方资料整理本场景的必要调用步骤与代码,覆盖范围以本页列出的接口为准。官方资料用于追溯规范及查看变更。

  1. 01
  2. 02
    抖音登录和授权核验日期 2026-10-15
  3. 03
    上线移动应用和网站应用核验日期 2026-10-15
  4. 04
    获取 access_token核验日期 2026-10-15
  5. 05
    抖音获取授权码核验日期 2026-10-15

抖音开放平台的其他调用手册

← 返回抖音开放平台的全部功能