抖音开放平台调用手册

在授权期限内刷新用户凭证

使用尚有效的 refresh_token 刷新 access_token;此接口不会延长 refresh_token 本身期限,也不会新增授权。

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

接入准备

  • 持有本站新版授权换取示例保存的同一用户凭证文件,含 client_key、open_id 及 refresh_expires_at 绝对截止时间;不从缺少元数据的旧文件猜测原期限。
  • 用户未撤回授权,refresh_token 仍在有效期;私有 DY_TOKEN_FILE 目录已存在。

准备环境变量

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

DY_CLIENT_KEY
与该用户 refresh_token 相同的网站应用。
DY_REFRESH_TOKEN
该用户尚有效的 refresh_token。
DY_TOKEN_FILE
本用户由新版初始交换生成的私有凭证文件,包含应用/用户归属与绝对截止时间;不能与其他用户共用。
终端
export DY_CLIENT_KEY='tt_REPLACE_WITH_CLIENT_KEY'
export DY_REFRESH_TOKEN='RFT_EXAMPLE_NOT_A_REAL_TOKEN'
export DY_TOKEN_FILE='/secure/douyin-user-token.json'

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

调用步骤

  1. 01

    从该用户私有文件读取 refresh_token,设置 DY_CLIENT_KEY、DY_REFRESH_TOKEN、DY_TOKEN_FILE;代码在请求前检查归属、与原文件一致性、原截止时间和输出目录。

  2. 02

    保存为 refresh.py,运行 python3 refresh.py;成功才原子替换当次结果并保存 received_at、access_expires_at 和 refresh_expires_at;多个服务应集中更新并使用锁。

  3. 03

    access_expires_at 按本次响应计算;refresh_expires_at 保留原截止,若响应给出更早到期值则只取更早值,绝不能从刷新时刻重新计满原期限。过期或撤回后重新授权。

调用 01

刷新用户 access_token

POST 表单,固定 grant_type=refresh_token;不传 ClientSecret,不能把此接口与续期 refresh_token 的另一个能力混用。

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

请求头

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

参数说明

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

字段 / 位置类型 / 必填说明
client_key请求体string必填当前授权应用 ClientKey。
grant_type请求体string必填固定 refresh_token。
refresh_token请求体string必填尚有效的用户刷新凭证,不是 access_token。

调用代码

替换示例 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("凭证已安全保存;请按绝对截止时间检查有效期,不输出凭证")

# 凭证文件必须来自本页新版初始交换;缺少绝对截止时间时不能猜测补值。
client_key = env("DY_CLIENT_KEY")
refresh_token = env("DY_REFRESH_TOKEN")
token_file = env("DY_TOKEN_FILE")
with pending_file(token_file) as (path, output):
    previous = load_record(path)
    if previous.get("client_key") != client_key or not previous.get("open_id"):
        raise SystemExit("原凭证文件缺少归属或属于其他应用;尚未发送请求")
    if previous.get("refresh_token") != refresh_token:
        raise SystemExit("环境刷新凭证与原文件不一致;尚未发送请求")
    original_deadline = positive_int(previous.get("refresh_expires_at"), "refresh_expires_at")
    started_at = int(time.time())
    if original_deadline <= started_at:
        raise SystemExit("原 refresh_token 已到期,请用户重新授权;尚未发送请求")
    body = urlencode({"client_key": client_key, "refresh_token": refresh_token,
                      "grant_type": "refresh_token"}).encode("utf-8")
    data = post("https://open.douyin.com/oauth/refresh_token/", body)
    received_at = int(time.time())
    if data["open_id"] != previous["open_id"]:
        raise SystemExit("刷新结果用户不一致,原文件未覆盖")
    # 普通 refresh_token 接口不延长 refresh_token 本身的有效期。
    # 即使响应给了较长相对时长,也绝不从现在重新计满原期限。
    refresh_deadline = original_deadline
    if data.get("refresh_expires_in") is not None:
        refresh_deadline = min(original_deadline, started_at +
            positive_int(data["refresh_expires_in"], "refresh_expires_in"))
    record = dict(previous)
    record.update(data)
    record.update(client_key=client_key, open_id=previous["open_id"], received_at=received_at,
        access_expires_at=started_at + positive_int(data.get("expires_in"), "expires_in"),
        refresh_expires_at=refresh_deadline)
    if not isinstance(record.get("refresh_token"), str) or not record["refresh_token"]:
        raise SystemExit("刷新结果缺少凭证,原文件未覆盖")
    if min(record["access_expires_at"], refresh_deadline) <= received_at:
        raise SystemExit("响应到达时期限已过,原文件未覆盖")
    commit(path, output, record)

返回示例

结构节选;文档中的有效期字段可显示为字符串,保存时按当次值解析,不能固定照抄秒数。 以下为结构示意,非本站实测结果。

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

常见错误与排查

10010refresh_token 已过期
请用户重新完成授权;不能不断重试旧刷新凭证。
10005 / 10002参数缺失或错误
核对同一应用 ClientKey、grant_type 和刷新凭证,使用表单编码。
10001系统错误
有限次退避后仍失败时记录必要日志标识,向官方反馈,勿把凭证原文写入日志。

调用注意

  • 官方说明通常 access_token 15 天、refresh_token 30 天,最终按响应和用户撤回状态判断;本接口不重置 refresh_token 到期时间。
  • 续期 refresh_token 另需 renew_refresh_token 能力,本手册不申请或调用该能力。

注意事项

  • 在用户撤回授权后,原 access_token 会立即失效,不能靠 refresh_token 绕过用户选择。
  • 请求代码只示意单次刷新;线上需按应用和用户隔离存储,避免多个实例并发刷新互相覆盖。
  • 失败、跨应用或跨用户响应不覆盖原文件;请求前创建的临时文件最终清理。预检不能保证请求后完全不会发生磁盘或系统故障,线上应使用可靠凭证库、原子提交和单用户刷新锁。

核验来源

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

  1. 01
  2. 02
    刷新 access_token核验日期 2026-10-15

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

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