or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

assistants.mdaudio.mdbatches.mdchat-completions.mdchatkit.mdclient-initialization.mdcompletions.mdcontainers.mdconversations.mdembeddings.mdevals.mdfiles.mdfine-tuning.mdimages.mdindex.mdmodels.mdmoderations.mdrealtime.mdresponses.mdruns.mdthreads-messages.mduploads.mdvector-stores.mdvideos.mdwebhooks.md
KNOWN_ISSUES.md

webhooks.mddocs/

0

# Webhooks

1

2

Verify and handle webhook events from OpenAI for asynchronous notifications about fine-tuning jobs, batch completions, and other events.

3

4

## Capabilities

5

6

### Verify Webhook Signature

7

8

Verify that a webhook request came from OpenAI.

9

10

```python { .api }

11

def verify_signature(

12

payload: str | bytes,

13

headers: dict[str, str] | list[tuple[str, str]],

14

*,

15

secret: str | None = None,

16

tolerance: int = 300

17

) -> None:

18

"""

19

Verify webhook signature to ensure authenticity.

20

21

Args:

22

payload: Raw request body (bytes or string).

23

headers: Request headers containing webhook-signature, webhook-timestamp,

24

and webhook-id. Can be dict or list of tuples.

25

secret: Webhook secret from OpenAI dashboard. If None, uses client's

26

webhook_secret or OPENAI_WEBHOOK_SECRET environment variable.

27

tolerance: Maximum age of webhook in seconds (default: 300).

28

Used to prevent replay attacks.

29

30

Returns:

31

None: If signature is valid.

32

33

Raises:

34

InvalidWebhookSignatureError: If signature is invalid or timestamp is too old/new.

35

ValueError: If secret is not provided and not set on client.

36

"""

37

```

38

39

Usage example:

40

41

```python

42

from openai import OpenAI, InvalidWebhookSignatureError

43

44

client = OpenAI(webhook_secret="your-webhook-secret")

45

46

# In your webhook endpoint

47

def webhook_handler(request):

48

payload = request.body # Raw bytes

49

headers = request.headers # Headers dict or list

50

51

try:

52

# Verify signature

53

client.webhooks.verify_signature(

54

payload=payload,

55

headers=headers,

56

secret="your-webhook-secret" # Optional if set on client

57

)

58

59

# Signature valid, process webhook

60

event = json.loads(payload)

61

handle_webhook_event(event)

62

63

except InvalidWebhookSignatureError:

64

# Invalid signature, reject request

65

return {"error": "Invalid signature"}, 401

66

```

67

68

### Unwrap Webhook Event

69

70

Verify signature and parse webhook event in one call.

71

72

```python { .api }

73

def unwrap(

74

payload: str | bytes,

75

headers: dict[str, str] | list[tuple[str, str]],

76

*,

77

secret: str | None = None

78

) -> UnwrapWebhookEvent:

79

"""

80

Verify signature and parse webhook event.

81

82

Args:

83

payload: Raw request body (bytes or string).

84

headers: Request headers containing webhook-signature, webhook-timestamp,

85

and webhook-id. Can be dict or list of tuples.

86

secret: Webhook secret from OpenAI dashboard. If None, uses client's

87

webhook_secret or OPENAI_WEBHOOK_SECRET environment variable.

88

89

Returns:

90

UnwrapWebhookEvent: Parsed and verified webhook event.

91

92

Raises:

93

InvalidWebhookSignatureError: If signature is invalid.

94

ValueError: If secret is not provided and not set on client.

95

"""

96

```

97

98

Usage example:

99

100

```python

101

from openai import OpenAI

102

103

client = OpenAI(webhook_secret="your-webhook-secret")

104

105

# In webhook endpoint

106

def webhook_handler(request):

107

try:

108

# Verify and parse in one call

109

event = client.webhooks.unwrap(

110

payload=request.body,

111

headers=request.headers,

112

secret="your-webhook-secret" # Optional if set on client

113

)

114

115

# Handle different event types

116

if event.type == "fine_tuning.job.succeeded":

117

handle_fine_tuning_success(event.data)

118

119

elif event.type == "batch.completed":

120

handle_batch_completion(event.data)

121

122

return {"status": "ok"}, 200

123

124

except Exception as e:

125

return {"error": str(e)}, 400

126

```

127

128

## Webhook Event Types

129

130

### Fine-tuning Events

131

132

```python

133

# Job succeeded

134

{

135

"type": "fine_tuning.job.succeeded",

136

"data": {

137

"id": "ftjob-abc123",

138

"fine_tuned_model": "ft:gpt-3.5-turbo:org:model:abc",

139

"status": "succeeded"

140

}

141

}

142

143

# Job failed

144

{

145

"type": "fine_tuning.job.failed",

146

"data": {

147

"id": "ftjob-abc123",

148

"status": "failed",

149

"error": {...}

150

}

151

}

152

153

# Job cancelled

154

{

155

"type": "fine_tuning.job.cancelled",

156

"data": {

157

"id": "ftjob-abc123",

158

"status": "cancelled"

159

}

160

}

161

```

162

163

### Batch Events

164

165

```python

166

# Batch completed

167

{

168

"type": "batch.completed",

169

"data": {

170

"id": "batch_abc123",

171

"status": "completed",

172

"output_file_id": "file-xyz789"

173

}

174

}

175

176

# Batch failed

177

{

178

"type": "batch.failed",

179

"data": {

180

"id": "batch_abc123",

181

"status": "failed",

182

"errors": {...}

183

}

184

}

185

186

# Batch cancelled

187

{

188

"type": "batch.cancelled",

189

"data": {

190

"id": "batch_abc123",

191

"status": "cancelled"

192

}

193

}

194

195

# Batch expired

196

{

197

"type": "batch.expired",

198

"data": {

199

"id": "batch_abc123",

200

"status": "expired"

201

}

202

}

203

```

204

205

### Eval Events

206

207

```python

208

# Eval run succeeded

209

{

210

"type": "eval.run.succeeded",

211

"data": {

212

"id": "eval_run_abc123",

213

"status": "succeeded",

214

"results": {...}

215

}

216

}

217

218

# Eval run failed

219

{

220

"type": "eval.run.failed",

221

"data": {

222

"id": "eval_run_abc123",

223

"status": "failed",

224

"error": {...}

225

}

226

}

227

```

228

229

## Types

230

231

```python { .api }

232

from typing import Union, Literal

233

from typing_extensions import TypeAlias

234

235

# UnwrapWebhookEvent is a union of all possible webhook event types

236

UnwrapWebhookEvent: TypeAlias = Union[

237

BatchCancelledWebhookEvent,

238

BatchCompletedWebhookEvent,

239

BatchExpiredWebhookEvent,

240

BatchFailedWebhookEvent,

241

EvalRunCanceledWebhookEvent,

242

EvalRunFailedWebhookEvent,

243

EvalRunSucceededWebhookEvent,

244

FineTuningJobCancelledWebhookEvent,

245

FineTuningJobFailedWebhookEvent,

246

FineTuningJobSucceededWebhookEvent,

247

RealtimeCallIncomingWebhookEvent,

248

ResponseCancelledWebhookEvent,

249

ResponseCompletedWebhookEvent,

250

ResponseFailedWebhookEvent,

251

ResponseIncompleteWebhookEvent,

252

]

253

254

# Fine-tuning event types

255

class FineTuningJobSucceededWebhookEvent:

256

type: Literal["fine_tuning.job.succeeded"]

257

data: dict

258

259

class FineTuningJobFailedWebhookEvent:

260

type: Literal["fine_tuning.job.failed"]

261

data: dict

262

263

class FineTuningJobCancelledWebhookEvent:

264

type: Literal["fine_tuning.job.cancelled"]

265

data: dict

266

267

# Batch event types

268

class BatchCompletedWebhookEvent:

269

type: Literal["batch.completed"]

270

data: dict

271

272

class BatchFailedWebhookEvent:

273

type: Literal["batch.failed"]

274

data: dict

275

276

class BatchCancelledWebhookEvent:

277

type: Literal["batch.cancelled"]

278

data: dict

279

280

class BatchExpiredWebhookEvent:

281

type: Literal["batch.expired"]

282

data: dict

283

284

# Eval event types

285

class EvalRunSucceededWebhookEvent:

286

type: Literal["eval.run.succeeded"]

287

data: dict

288

289

class EvalRunFailedWebhookEvent:

290

type: Literal["eval.run.failed"]

291

data: dict

292

293

class EvalRunCanceledWebhookEvent:

294

type: Literal["eval.run.canceled"]

295

data: dict

296

297

# Response event types

298

class ResponseCompletedWebhookEvent:

299

type: Literal["response.completed"]

300

data: dict

301

302

class ResponseFailedWebhookEvent:

303

type: Literal["response.failed"]

304

data: dict

305

306

class ResponseCancelledWebhookEvent:

307

type: Literal["response.cancelled"]

308

data: dict

309

310

class ResponseIncompleteWebhookEvent:

311

type: Literal["response.incomplete"]

312

data: dict

313

314

# Realtime event types

315

class RealtimeCallIncomingWebhookEvent:

316

type: Literal["realtime.call.incoming"]

317

data: dict

318

```

319

320

## Complete Webhook Handler

321

322

```python

323

from openai import OpenAI, InvalidWebhookSignatureError

324

from flask import Flask, request, jsonify

325

326

app = Flask(__name__)

327

client = OpenAI()

328

329

WEBHOOK_SECRET = "your-webhook-secret"

330

331

@app.route("/webhooks/openai", methods=["POST"])

332

def handle_webhook():

333

# Get headers and payload

334

payload = request.data

335

headers = request.headers

336

337

# Verify and parse

338

try:

339

event = client.webhooks.unwrap(

340

payload=payload,

341

headers=headers,

342

secret=WEBHOOK_SECRET

343

)

344

except InvalidWebhookSignatureError:

345

return jsonify({"error": "Invalid signature"}), 401

346

347

# Handle event

348

if event.type == "fine_tuning.job.succeeded":

349

job_id = event.data["id"]

350

model = event.data["fine_tuned_model"]

351

print(f"Fine-tuning succeeded: {job_id} -> {model}")

352

353

# Deploy model or notify user

354

deploy_model(model)

355

356

elif event.type == "batch.completed":

357

batch_id = event.data["id"]

358

output_file = event.data["output_file_id"]

359

print(f"Batch completed: {batch_id}")

360

361

# Process results

362

process_batch_results(batch_id, output_file)

363

364

elif event.type == "fine_tuning.job.failed":

365

job_id = event.data["id"]

366

error = event.data.get("error")

367

print(f"Fine-tuning failed: {job_id}, Error: {error}")

368

369

# Notify user of failure

370

notify_failure(job_id, error)

371

372

return jsonify({"status": "received"}), 200

373

374

if __name__ == "__main__":

375

app.run(port=8080)

376

```

377

378

## Best Practices

379

380

```python

381

from openai import OpenAI, InvalidWebhookSignatureError

382

import hmac

383

import hashlib

384

385

client = OpenAI()

386

387

# 1. Always verify signatures

388

def is_valid_webhook(payload: bytes, headers: dict, secret: str) -> bool:

389

try:

390

client.webhooks.verify_signature(payload, headers, secret=secret)

391

return True

392

except InvalidWebhookSignatureError:

393

return False

394

395

# 2. Handle replay attacks with timestamp

396

def is_recent_webhook(timestamp: str, max_age_seconds: int = 300) -> bool:

397

import time

398

399

event_time = int(timestamp)

400

current_time = int(time.time())

401

402

return (current_time - event_time) < max_age_seconds

403

404

# 3. Process events idempotently

405

processed_events = set()

406

407

def process_webhook_event(event_id: str, event_data: dict):

408

if event_id in processed_events:

409

print(f"Duplicate event: {event_id}")

410

return

411

412

# Process event

413

handle_event(event_data)

414

415

# Mark as processed

416

processed_events.add(event_id)

417

418

# 4. Return 200 quickly, process async

419

from threading import Thread

420

421

def handle_webhook_async(event):

422

# Process in background

423

thread = Thread(target=process_event, args=(event,))

424

thread.start()

425

426

# Return immediately

427

return {"status": "accepted"}, 200

428

429

# 5. Retry on failure

430

import time

431

432

def process_with_retry(event, max_retries=3):

433

for attempt in range(max_retries):

434

try:

435

process_event(event)

436

return

437

except Exception as e:

438

if attempt == max_retries - 1:

439

log_failure(event, e)

440

raise

441

time.sleep(2 ** attempt)

442

```

443

444

## Testing Webhooks

445

446

```python

447

# Generate test signature for development

448

import hmac

449

import hashlib

450

import json

451

import time

452

453

def generate_test_signature(payload: dict, secret: str) -> tuple[str, str]:

454

"""Generate signature for testing."""

455

timestamp = str(int(time.time()))

456

payload_str = json.dumps(payload)

457

458

# Create signature

459

message = f"{timestamp}.{payload_str}"

460

signature = hmac.new(

461

secret.encode(),

462

message.encode(),

463

hashlib.sha256

464

).hexdigest()

465

466

return signature, timestamp

467

468

# Test webhook handler

469

test_payload = {

470

"type": "fine_tuning.job.succeeded",

471

"data": {

472

"id": "ftjob-test123",

473

"fine_tuned_model": "ft:gpt-3.5-turbo:test"

474

}

475

}

476

477

# Create test headers (actual implementation would vary)

478

test_headers = {

479

"webhook-signature": "v1,test_signature",

480

"webhook-timestamp": str(int(time.time())),

481

"webhook-id": "test_webhook_id"

482

}

483

484

# Note: In production, signatures are generated by OpenAI

485

# This is a simplified example for testing

486

event = client.webhooks.unwrap(

487

payload=json.dumps(test_payload),

488

headers=test_headers,

489

secret=WEBHOOK_SECRET

490

)

491

492

print(f"Test event: {event.type}")

493

```

494

495

## Security Considerations

496

497

1. **Always verify signatures** - Never trust webhook data without verification

498

2. **Use HTTPS** - Only accept webhooks over HTTPS

499

3. **Check timestamps** - Reject old events to prevent replay attacks

500

4. **Rate limit** - Limit webhook endpoint to prevent abuse

501

5. **Store secrets securely** - Use environment variables or secret management

502

6. **Log failures** - Track verification failures for security monitoring

503