Class: Basecamp::ApiClient

Inherits:
Object
  • Object
show all
Defined in:
app/services/basecamp/api_client.rb

Overview

HTTP client for Basecamp 4 JSON API.

Features:

  • Auto-refreshes expired access tokens
  • Respects Basecamp rate limits (429 + Retry-After)
  • Retries transient failures (502, 503)
  • User-Agent required by Basecamp API

Reference: https://github.com/basecamp/api/blob/master/sections/authentication.md

Defined Under Namespace

Classes: ApiError, NotFoundError, RateLimitError

Constant Summary collapse

BASE_URL =

URL for base.

'https://3.basecampapi.com'
USER_AGENT =

User agent.

'WarmlyYours CRM (support@warmlyyours.com)'
MAX_RETRIES =

Maximum retries.

3
RATE_LIMIT_SLEEP =

default wait if no Retry-After header

5

Instance Method Summary collapse

Constructor Details

#initialize(account: nil, oauth_service: nil) ⇒ ApiClient

Returns a new instance of ApiClient.

Parameters:

  • account (Account, nil) (defaults to: nil)

    the CRM account whose Basecamp token to use (nil for system-level)

  • oauth_service (Basecamp::OauthService, nil) (defaults to: nil)

    override for testing



42
43
44
# File 'app/services/basecamp/api_client.rb', line 42

def initialize(account: nil, oauth_service: nil)
  @oauth_service = oauth_service || Basecamp::OauthService.new(account: )
end

Instance Method Details

#campfire_lines(project_id, campfire_id) ⇒ Object



204
205
206
# File 'app/services/basecamp/api_client.rb', line 204

def campfire_lines(project_id, campfire_id)
  get("/buckets/#{project_id}/chats/#{campfire_id}/lines.json")
end

#card_tables(project_id) ⇒ Object

--- Card Table (Kanban) endpoints ---



210
211
212
# File 'app/services/basecamp/api_client.rb', line 210

def card_tables(project_id)
  get("/buckets/#{project_id}/card_tables.json")
end

#cards(project_id, column_id) ⇒ Object



218
219
220
# File 'app/services/basecamp/api_client.rb', line 218

def cards(project_id, column_id)
  get("/buckets/#{project_id}/card_tables/lists/#{column_id}/cards.json")
end

#columns(project_id, card_table_id) ⇒ Object



214
215
216
# File 'app/services/basecamp/api_client.rb', line 214

def columns(project_id, card_table_id)
  get("/buckets/#{project_id}/card_tables/#{card_table_id}/columns.json")
end

#comment(project_id, comment_id) ⇒ Object



261
262
263
# File 'app/services/basecamp/api_client.rb', line 261

def comment(project_id, comment_id)
  get("/buckets/#{project_id}/comments/#{comment_id}.json")
end

#comments(project_id, recording_id) ⇒ Object

--- Comments ---



257
258
259
# File 'app/services/basecamp/api_client.rb', line 257

def comments(project_id, recording_id)
  get_paginated("/buckets/#{project_id}/recordings/#{recording_id}/comments.json")
end

#complete_todo(project_id, todo_id) ⇒ Object



138
139
140
# File 'app/services/basecamp/api_client.rb', line 138

def complete_todo(project_id, todo_id)
  post("/buckets/#{project_id}/todos/#{todo_id}/completion.json")
end

#create_card(project_id, column_id, title:, content: nil, due_on: nil) ⇒ Object



222
223
224
225
226
227
# File 'app/services/basecamp/api_client.rb', line 222

def create_card(project_id, column_id, title:, content: nil, due_on: nil)
  body = { title: title }
  body[:content] = content if content
  body[:due_on] = due_on if due_on
  post("/buckets/#{project_id}/card_tables/lists/#{column_id}/cards.json", body)
end

#create_comment(project_id, recording_id, content:) ⇒ Object



265
266
267
# File 'app/services/basecamp/api_client.rb', line 265

def create_comment(project_id, recording_id, content:)
  post("/buckets/#{project_id}/recordings/#{recording_id}/comments.json", { content: content })
end

#create_document(project_id, vault_id, title:, content:, status: nil) ⇒ Object



353
354
355
356
357
# File 'app/services/basecamp/api_client.rb', line 353

def create_document(project_id, vault_id, title:, content:, status: nil)
  body = { title: title, content: content }
  body[:status] = status if status
  post("/buckets/#{project_id}/vaults/#{vault_id}/documents.json", body)
end

#create_message(project_id, message_board_id, subject:, status:, content: nil, category_id: nil, subscriptions: nil) ⇒ Object



180
181
182
183
184
185
186
# File 'app/services/basecamp/api_client.rb', line 180

def create_message(project_id, message_board_id, subject:, status:, content: nil, category_id: nil, subscriptions: nil)
  body = { subject: subject, status: status }
  body[:content] = content if content
  body[:category_id] = category_id if category_id
  body[:subscriptions] = subscriptions if subscriptions
  post("/buckets/#{project_id}/message_boards/#{message_board_id}/messages.json", body)
end

#create_project(name:, description: nil) ⇒ Object



58
59
60
61
62
# File 'app/services/basecamp/api_client.rb', line 58

def create_project(name:, description: nil)
  body = { name: name }
  body[:description] = description if description
  post('/projects.json', body)
end

#create_todo(project_id, todolist_id, content:, description: nil, assignee_ids: nil, due_on: nil, starts_on: nil) ⇒ Object



119
120
121
122
123
124
125
126
# File 'app/services/basecamp/api_client.rb', line 119

def create_todo(project_id, todolist_id, content:, description: nil, assignee_ids: nil, due_on: nil, starts_on: nil)
  body = { content: content }
  body[:description] = description if description
  body[:assignee_ids] = assignee_ids if assignee_ids
  body[:due_on] = due_on if due_on
  body[:starts_on] = starts_on if starts_on
  post("/buckets/#{project_id}/todolists/#{todolist_id}/todos.json", body)
end

#create_todo_group(project_id, todolist_id, name:, color: nil) ⇒ Object



152
153
154
155
156
# File 'app/services/basecamp/api_client.rb', line 152

def create_todo_group(project_id, todolist_id, name:, color: nil)
  body = { name: name }
  body[:color] = color if color
  post("/buckets/#{project_id}/todolists/#{todolist_id}/groups.json", body)
end

#create_todolist(project_id, todoset_id, name:, description: nil) ⇒ Object



96
97
98
99
100
# File 'app/services/basecamp/api_client.rb', line 96

def create_todolist(project_id, todoset_id, name:, description: nil)
  body = { name: name }
  body[:description] = description if description
  post("/buckets/#{project_id}/todosets/#{todoset_id}/todolists.json", body)
end

#create_upload(project_id, vault_id, attachable_sgid:, description: nil, base_name: nil) ⇒ Object



373
374
375
376
377
378
# File 'app/services/basecamp/api_client.rb', line 373

def create_upload(project_id, vault_id, attachable_sgid:, description: nil, base_name: nil)
  body = { attachable_sgid: attachable_sgid }
  body[:description] = description if description
  body[:base_name] = base_name if base_name
  post("/buckets/#{project_id}/vaults/#{vault_id}/uploads.json", body)
end

#document(project_id, document_id) ⇒ Object



349
350
351
# File 'app/services/basecamp/api_client.rb', line 349

def document(project_id, document_id)
  get("/buckets/#{project_id}/documents/#{document_id}.json")
end

#documents(project_id, vault_id) ⇒ Object

--- Documents ---



345
346
347
# File 'app/services/basecamp/api_client.rb', line 345

def documents(project_id, vault_id)
  get_paginated("/buckets/#{project_id}/vaults/#{vault_id}/documents.json")
end

#message(project_id, message_id) ⇒ Object



176
177
178
# File 'app/services/basecamp/api_client.rb', line 176

def message(project_id, message_id)
  get("/buckets/#{project_id}/messages/#{message_id}.json")
end

#message_board(project_id, message_board_id = nil) ⇒ Object

--- Message Board / Campfire ---



164
165
166
167
168
169
170
# File 'app/services/basecamp/api_client.rb', line 164

def message_board(project_id, message_board_id = nil)
  if message_board_id
    get("/buckets/#{project_id}/message_boards/#{message_board_id}.json")
  else
    get("/buckets/#{project_id}/message_board.json")
  end
end

#messages(project_id, message_board_id) ⇒ Object



172
173
174
# File 'app/services/basecamp/api_client.rb', line 172

def messages(project_id, message_board_id)
  get_paginated("/buckets/#{project_id}/message_boards/#{message_board_id}/messages.json")
end

#my_assignments(completed: false, group_by: 'bucket') ⇒ Object

Query tasks assigned to the current authenticated user.

For open assignments we use the reports endpoint, which returns pending to-dos
assigned to a person and matches Basecamp's assignment report behavior.
For completed assignments we fall back to /my/assignments.json.



306
307
308
309
310
311
# File 'app/services/basecamp/api_client.rb', line 306

def my_assignments(completed: false, group_by: 'bucket')
  return get_paginated('/my/assignments.json', params: { completed: true }) if completed

  person_id = my_profile['id']
  todo_assignments_for_person(person_id, group_by: group_by)
end

#my_profileObject



339
340
341
# File 'app/services/basecamp/api_client.rb', line 339

def my_profile
  get('/my/profile.json')
end

#overdue_todosObject



325
326
327
# File 'app/services/basecamp/api_client.rb', line 325

def overdue_todos
  get('/reports/todos/overdue.json')
end

#peopleObject

--- People ---



231
232
233
# File 'app/services/basecamp/api_client.rb', line 231

def people
  get_paginated('/people.json')
end

#person(person_id) ⇒ Object



235
236
237
# File 'app/services/basecamp/api_client.rb', line 235

def person(person_id)
  get("/people/#{person_id}.json")
end

#pin_recording(project_id, recording_id) ⇒ Object



196
197
198
# File 'app/services/basecamp/api_client.rb', line 196

def pin_recording(project_id, recording_id)
  post("/buckets/#{project_id}/recordings/#{recording_id}/pin.json")
end

#pingable_peopleObject



251
252
253
# File 'app/services/basecamp/api_client.rb', line 251

def pingable_people
  get('/circles/people.json')
end

#project(project_id) ⇒ Object



54
55
56
# File 'app/services/basecamp/api_client.rb', line 54

def project(project_id)
  get("/projects/#{project_id}.json")
end

#project_people(project_id) ⇒ Object



239
240
241
# File 'app/services/basecamp/api_client.rb', line 239

def project_people(project_id)
  get_paginated("/projects/#{project_id}/people.json")
end

#projects(status: nil) ⇒ Object

--- Project endpoints ---



48
49
50
51
52
# File 'app/services/basecamp/api_client.rb', line 48

def projects(status: nil)
  params = {}
  params[:status] = status if status.present?
  get_paginated('/projects.json', params: params.presence)
end

#reposition_todo(project_id, todo_id, position:, parent_id: nil) ⇒ Object



132
133
134
135
136
# File 'app/services/basecamp/api_client.rb', line 132

def reposition_todo(project_id, todo_id, position:, parent_id: nil)
  body = { position: position }
  body[:parent_id] = parent_id if parent_id
  put("/buckets/#{project_id}/todos/#{todo_id}/position.json", body)
end

#reposition_todo_group(project_id, group_id, position:) ⇒ Object



158
159
160
# File 'app/services/basecamp/api_client.rb', line 158

def reposition_todo_group(project_id, group_id, position:)
  put("/buckets/#{project_id}/todolists/groups/#{group_id}/position.json", { position: position })
end

#search(q:, type: nil, bucket_id: nil, creator_id: nil, file_type: nil, exclude_chat: nil, page: nil, per_page: nil, fetch_all: false) ⇒ Object



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'app/services/basecamp/api_client.rb', line 283

def search(q:, type: nil, bucket_id: nil, creator_id: nil, file_type: nil, exclude_chat: nil, page: nil, per_page: nil, fetch_all: false)
  params = { q: q }
  params[:type] = type if type.present?
  params[:bucket_id] = bucket_id if bucket_id.present?
  params[:creator_id] = creator_id if creator_id.present?
  params[:file_type] = file_type if file_type.present?
  params[:exclude_chat] = exclude_chat unless exclude_chat.nil?
  params[:page] = page if page.present?
  params[:per_page] = per_page if per_page.present?
  if fetch_all
    get_paginated('/search.json', params: params)
  else
    get('/search.json', params: params)
  end
end

#search_metadataObject

--- Search ---



279
280
281
# File 'app/services/basecamp/api_client.rb', line 279

def 
  get('/searches/metadata.json')
end

#todo(project_id, todo_id) ⇒ Object



115
116
117
# File 'app/services/basecamp/api_client.rb', line 115

def todo(project_id, todo_id)
  get("/buckets/#{project_id}/todos/#{todo_id}.json")
end

#todo_assigneesObject

--- Reports ---



315
316
317
# File 'app/services/basecamp/api_client.rb', line 315

def todo_assignees
  get_paginated('/reports/todos/assigned.json')
end

#todo_assignments_for_person(person_id, group_by: 'bucket') ⇒ Object



319
320
321
322
323
# File 'app/services/basecamp/api_client.rb', line 319

def todo_assignments_for_person(person_id, group_by: 'bucket')
  params = {}
  params[:group_by] = group_by if group_by.present?
  get("/reports/todos/assigned/#{person_id}.json", params: params.presence)
end

#todo_groups(project_id, todolist_id, status: nil) ⇒ Object



146
147
148
149
150
# File 'app/services/basecamp/api_client.rb', line 146

def todo_groups(project_id, todolist_id, status: nil)
  params = {}
  params[:status] = status if status.present?
  get_paginated("/buckets/#{project_id}/todolists/#{todolist_id}/groups.json", params: params.presence)
end

#todolist(project_id, todolist_id) ⇒ Object



92
93
94
# File 'app/services/basecamp/api_client.rb', line 92

def todolist(project_id, todolist_id)
  get("/buckets/#{project_id}/todolists/#{todolist_id}.json")
end

#todolists(project_id) ⇒ Object



82
83
84
# File 'app/services/basecamp/api_client.rb', line 82

def todolists(project_id)
  get_paginated("/buckets/#{project_id}/todolists.json")
end

#todolists_in_set(project_id, todoset_id, status: nil) ⇒ Object



86
87
88
89
90
# File 'app/services/basecamp/api_client.rb', line 86

def todolists_in_set(project_id, todoset_id, status: nil)
  params = {}
  params[:status] = status if status.present?
  get_paginated("/buckets/#{project_id}/todosets/#{todoset_id}/todolists.json", params: params.presence)
end

#todos(project_id, todolist_id, status: nil, completed: nil) ⇒ Object



108
109
110
111
112
113
# File 'app/services/basecamp/api_client.rb', line 108

def todos(project_id, todolist_id, status: nil, completed: nil)
  params = {}
  params[:status] = status if status.present?
  params[:completed] = completed unless completed.nil?
  get_paginated("/buckets/#{project_id}/todolists/#{todolist_id}/todos.json", params: params.presence)
end

#todoset(project_id, todoset_id) ⇒ Object

--- Todo endpoints ---



78
79
80
# File 'app/services/basecamp/api_client.rb', line 78

def todoset(project_id, todoset_id)
  get("/buckets/#{project_id}/todosets/#{todoset_id}.json")
end

#trash_project(project_id) ⇒ Object



72
73
74
# File 'app/services/basecamp/api_client.rb', line 72

def trash_project(project_id)
  delete("/projects/#{project_id}.json")
end

#trash_recording(project_id, recording_id) ⇒ Object



273
274
275
# File 'app/services/basecamp/api_client.rb', line 273

def trash_recording(project_id, recording_id)
  post("/buckets/#{project_id}/recordings/#{recording_id}/status/trashed.json")
end

#uncomplete_todo(project_id, todo_id) ⇒ Object



142
143
144
# File 'app/services/basecamp/api_client.rb', line 142

def uncomplete_todo(project_id, todo_id)
  delete("/buckets/#{project_id}/todos/#{todo_id}/completion.json")
end

#unpin_recording(project_id, recording_id) ⇒ Object



200
201
202
# File 'app/services/basecamp/api_client.rb', line 200

def unpin_recording(project_id, recording_id)
  delete("/buckets/#{project_id}/recordings/#{recording_id}/pin.json")
end

#upcoming_schedule(window_starts_on:, window_ends_on:) ⇒ Object



329
330
331
332
333
334
335
336
337
# File 'app/services/basecamp/api_client.rb', line 329

def upcoming_schedule(window_starts_on:, window_ends_on:)
  get(
    '/reports/schedules/upcoming.json',
    params: {
      window_starts_on: window_starts_on,
      window_ends_on: window_ends_on
    }
  )
end

#update_comment(project_id, comment_id, content:) ⇒ Object



269
270
271
# File 'app/services/basecamp/api_client.rb', line 269

def update_comment(project_id, comment_id, content:)
  put("/buckets/#{project_id}/comments/#{comment_id}.json", { content: content })
end

#update_document(project_id, document_id, title:, content:) ⇒ Object



359
360
361
# File 'app/services/basecamp/api_client.rb', line 359

def update_document(project_id, document_id, title:, content:)
  put("/buckets/#{project_id}/documents/#{document_id}.json", { title: title, content: content })
end

#update_message(project_id, message_id, subject: nil, content: nil, category_id: nil) ⇒ Object



188
189
190
191
192
193
194
# File 'app/services/basecamp/api_client.rb', line 188

def update_message(project_id, message_id, subject: nil, content: nil, category_id: nil)
  body = {}
  body[:subject] = subject if subject
  body[:content] = content if content
  body[:category_id] = category_id if category_id
  put("/buckets/#{project_id}/messages/#{message_id}.json", body)
end

#update_project(project_id, name:, description: nil, admissions: nil, schedule_attributes: nil) ⇒ Object



64
65
66
67
68
69
70
# File 'app/services/basecamp/api_client.rb', line 64

def update_project(project_id, name:, description: nil, admissions: nil, schedule_attributes: nil)
  body = { name: name }
  body[:description] = description if description
  body[:admissions] = admissions if admissions
  body[:schedule_attributes] = schedule_attributes if schedule_attributes
  put("/projects/#{project_id}.json", body)
end

#update_project_people(project_id, grant: nil, revoke: nil, create: nil) ⇒ Object



243
244
245
246
247
248
249
# File 'app/services/basecamp/api_client.rb', line 243

def update_project_people(project_id, grant: nil, revoke: nil, create: nil)
  body = {}
  body[:grant] = grant if grant.present?
  body[:revoke] = revoke if revoke.present?
  body[:create] = create if create.present?
  put("/projects/#{project_id}/people/users.json", body)
end

#update_todo(project_id, todo_id, **attrs) ⇒ Object



128
129
130
# File 'app/services/basecamp/api_client.rb', line 128

def update_todo(project_id, todo_id, **attrs)
  put("/buckets/#{project_id}/todos/#{todo_id}.json", attrs.compact)
end

#update_todolist(project_id, todolist_id, name:, description: nil) ⇒ Object



102
103
104
105
106
# File 'app/services/basecamp/api_client.rb', line 102

def update_todolist(project_id, todolist_id, name:, description: nil)
  body = { name: name }
  body[:description] = description if description
  put("/buckets/#{project_id}/todolists/#{todolist_id}.json", body)
end

#update_upload(project_id, upload_id, description: nil, base_name: nil) ⇒ Object



380
381
382
383
384
385
# File 'app/services/basecamp/api_client.rb', line 380

def update_upload(project_id, upload_id, description: nil, base_name: nil)
  body = {}
  body[:description] = description if description
  body[:base_name] = base_name if base_name
  put("/buckets/#{project_id}/uploads/#{upload_id}.json", body)
end

#upload(project_id, upload_id) ⇒ Object



369
370
371
# File 'app/services/basecamp/api_client.rb', line 369

def upload(project_id, upload_id)
  get("/buckets/#{project_id}/uploads/#{upload_id}.json")
end

#uploads(project_id, vault_id) ⇒ Object

--- Uploads ---



365
366
367
# File 'app/services/basecamp/api_client.rb', line 365

def uploads(project_id, vault_id)
  get_paginated("/buckets/#{project_id}/vaults/#{vault_id}/uploads.json")
end