Skip to content
FolioSign inStart your trial

API reference

Booth devices

Pairing, commands, updates and uploads for Folio Live on a booth machine.

GET/v1/device/commands

Long-poll for commands#

Holds the request open for up to wait seconds (capped by the server, 25 by default) and returns commands queued for this booth after since. Kinds: sync_now (pull the library), update_available and revoke (acknowledging it revokes the key). Store next_since and send it on the next poll. Acknowledge each command.

Auth. A booth device key. Needs the device:read scope. Send X-Folio-Device-Contract; an unsupported version gets 426.

Limits. Responses carry RateLimit-Limit and RateLimit-Remaining; a 429 carries Retry-After.

Auth
Booth device key
Scope
device:read
Operation id
deviceCommands

Parameters

Parameters
NameInDescription
sincequeryReturn commands after this sequence number. (integer, e.g. 0)
waitquerySeconds to hold the request open when nothing is queued. Capped by the server. (integer, e.g. 25)

Responses

Response data fields
FieldTypeDescription
commandsobject[]Commands queued for the device after since.
next_sinceinteger (int64)Pass this as since on the next poll.
objectenum: device_commandsThe object type.
200 Success. Long-poll for commands.
200 example
{
  "status": true,
  "message": "device_commands",
  "data": {
    "object": "device_commands",
    "commands": [
      {
        "id": "cmd_0192f7a8-3b1c-7d2e-9a40-5c6d7e8f9012",
        "object": "device_command",
        "seq": 7,
        "kind": "sync_now",
        "payload": {},
        "created_at": "2026-11-01T08:00:00Z"
      }
    ],
    "next_since": 7
  }
}
400 The request could not be read.
400 example
{
  "status": false,
  "message": "the page cursor is not valid",
  "error": {
    "code": "invalid_argument",
    "status": 400,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
401 The key or session is missing, invalid or expired.
401 example
{
  "status": false,
  "message": "That API key is not valid or has expired.",
  "error": {
    "code": "unauthenticated",
    "status": 401,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
403 The credential lacks the scope, the church is suspended, the feature is not in the plan, or the address is not allowed.
403 example
{
  "status": false,
  "message": "That API key does not have the required scope.",
  "error": {
    "code": "permission_denied",
    "status": 403,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
404 The resource is not in this church.
404 example
{
  "status": false,
  "message": "That item is not in this church.",
  "error": {
    "code": "not_found",
    "status": 404,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
409 Conflict, including a duplicate Idempotency-Key still being processed.
409 example
{
  "status": false,
  "message": "A request with this Idempotency-Key is still running.",
  "error": {
    "code": "conflict",
    "status": 409,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
422 A field failed validation, or an Idempotency-Key was reused with a different body.
422 example
{
  "status": false,
  "message": "A field is not valid.",
  "error": {
    "code": "validation",
    "status": 422,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
426 The booth's contract version is no longer supported. Call GET /v1/device/update.
426 example
{
  "status": false,
  "message": "This booth must update Folio Live.",
  "error": {
    "code": "failed_precondition",
    "status": 426,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
429 Rate limited. Wait Retry-After seconds.
429 example
{
  "status": false,
  "message": "Too many requests. Wait before trying again.",
  "error": {
    "code": "rate_limited",
    "status": 429,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
500 Something went wrong. Quote error.request_id.
500 example
{
  "status": false,
  "message": "Something went wrong. Quote the request id to support.",
  "error": {
    "code": "internal",
    "status": 500,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}

Code samples

curl -X GET "$FOLIO_API_URL/v1/device/commands?since=0&wait=25" \
  -H "Authorization: Bearer $FOLIO_DEVICE_KEY" \
  -H "X-Folio-Device-Contract: 1"

Try it is available for church operations and public reads. Booth devices pair with a code, so this operation is documented but not runnable here.

POST/v1/device/commands/{command}/ack

Acknowledge a command#

Marks a command as handled. Acknowledging a revoke command revokes the device key, so the booth's next call answers 401.

Auth. A booth device key. Needs the device:write scope. Send X-Folio-Device-Contract; an unsupported version gets 426.

Limits. Responses carry RateLimit-Limit and RateLimit-Remaining; a 429 carries Retry-After.

Auth
Booth device key
Scope
device:write
Operation id
deviceAckCommand

Parameters

Parameters
NameInDescription
command requiredpathThe command id from GET /v1/device/commands. (string, e.g. cmd_0192f7a8-3b1c-7d2e-9a40-5c6d7e8f9012)
Idempotency-KeyheaderMakes a retry safe. The same key with the same body replays the first response for 24 hours; the same key with a different body returns 422; a duplicate that is still running returns 409 with Retry-After. Responses that carry a one-time secret are not replayed. (string, e.g. sunday-2026-11-01-upload)

Responses

Response data fields
FieldTypeDescription
created_atstringWhen it was created, in RFC 3339 UTC.
idstringThe id, with a type prefix such as tch_ or note_.
kindenum: sync_now | update_available | revokeThe kind.
objectenum: device_commandThe object type.
payloadobjectDetails for the command.
seqinteger (int64)The command's sequence number. Send the highest one as since.
200 Success. Acknowledge a command.
200 example
{
  "status": true,
  "message": "device_command",
  "data": {
    "id": "cmd_0192f7a8-3b1c-7d2e-9a40-5c6d7e8f9012",
    "object": "device_command",
    "seq": 7,
    "kind": "sync_now",
    "payload": {},
    "created_at": "2026-11-01T08:00:00Z"
  }
}
400 The request could not be read.
400 example
{
  "status": false,
  "message": "the page cursor is not valid",
  "error": {
    "code": "invalid_argument",
    "status": 400,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
401 The key or session is missing, invalid or expired.
401 example
{
  "status": false,
  "message": "That API key is not valid or has expired.",
  "error": {
    "code": "unauthenticated",
    "status": 401,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
403 The credential lacks the scope, the church is suspended, the feature is not in the plan, or the address is not allowed.
403 example
{
  "status": false,
  "message": "That API key does not have the required scope.",
  "error": {
    "code": "permission_denied",
    "status": 403,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
404 The resource is not in this church.
404 example
{
  "status": false,
  "message": "That item is not in this church.",
  "error": {
    "code": "not_found",
    "status": 404,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
409 Conflict, including a duplicate Idempotency-Key still being processed.
409 example
{
  "status": false,
  "message": "A request with this Idempotency-Key is still running.",
  "error": {
    "code": "conflict",
    "status": 409,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
422 A field failed validation, or an Idempotency-Key was reused with a different body.
422 example
{
  "status": false,
  "message": "A field is not valid.",
  "error": {
    "code": "validation",
    "status": 422,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
426 The booth's contract version is no longer supported. Call GET /v1/device/update.
426 example
{
  "status": false,
  "message": "This booth must update Folio Live.",
  "error": {
    "code": "failed_precondition",
    "status": 426,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
429 Rate limited. Wait Retry-After seconds.
429 example
{
  "status": false,
  "message": "Too many requests. Wait before trying again.",
  "error": {
    "code": "rate_limited",
    "status": 429,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
500 Something went wrong. Quote error.request_id.
500 example
{
  "status": false,
  "message": "Something went wrong. Quote the request id to support.",
  "error": {
    "code": "internal",
    "status": 500,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}

Code samples

curl -X POST "$FOLIO_API_URL/v1/device/commands/cmd_0192f7a8-3b1c-7d2e-9a40-5c6d7e8f9012/ack" \
  -H "Authorization: Bearer $FOLIO_DEVICE_KEY" \
  -H "X-Folio-Device-Contract: 1"

Try it is available for church operations and public reads. Booth devices pair with a code, so this operation is documented but not runnable here.

POST/v1/device/events

Push service events#

Sends events recorded during a service, such as songs shown. Events are deduplicated by their id, so resending a batch after a dropped connection is safe. Send up to 500 at a time.

Auth. A booth device key. Needs the device:write scope. Send X-Folio-Device-Contract; an unsupported version gets 426.

Limits. Responses carry RateLimit-Limit and RateLimit-Remaining; a 429 carries Retry-After.

Auth
Booth device key
Scope
device:write
Operation id
deviceRecordEvents

Parameters

Parameters
NameInDescription
Idempotency-KeyheaderMakes a retry safe. The same key with the same body replays the first response for 24 hours; the same key with a different body returns 422; a duplicate that is still running returns 409 with Retry-After. Responses that carry a one-time secret are not replayed. (string, e.g. sunday-2026-11-01-upload)

Request body

Request body fields
FieldTypeDescription
eventsobject[]Up to 500 events to record.
Example request body
{
  "events": [
    {
      "id": "evt-1730451600-1",
      "kind": "song.shown",
      "at_ms": 1730451600000,
      "payload": "{\"song\":\"amazing-grace\"}"
    }
  ]
}

Responses

Response data fields
FieldTypeDescription
objectenum: device_eventsThe object type.
storedinteger (int64)How many events were stored.
200 Success. Push service events.
200 example
{
  "status": true,
  "message": "device_events",
  "data": {
    "object": "device_events",
    "stored": 1
  }
}
400 The request could not be read.
400 example
{
  "status": false,
  "message": "the page cursor is not valid",
  "error": {
    "code": "invalid_argument",
    "status": 400,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
401 The key or session is missing, invalid or expired.
401 example
{
  "status": false,
  "message": "That API key is not valid or has expired.",
  "error": {
    "code": "unauthenticated",
    "status": 401,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
403 The credential lacks the scope, the church is suspended, the feature is not in the plan, or the address is not allowed.
403 example
{
  "status": false,
  "message": "That API key does not have the required scope.",
  "error": {
    "code": "permission_denied",
    "status": 403,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
404 The resource is not in this church.
404 example
{
  "status": false,
  "message": "That item is not in this church.",
  "error": {
    "code": "not_found",
    "status": 404,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
409 Conflict, including a duplicate Idempotency-Key still being processed.
409 example
{
  "status": false,
  "message": "A request with this Idempotency-Key is still running.",
  "error": {
    "code": "conflict",
    "status": 409,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
422 A field failed validation, or an Idempotency-Key was reused with a different body.
422 example
{
  "status": false,
  "message": "A field is not valid.",
  "error": {
    "code": "validation",
    "status": 422,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
426 The booth's contract version is no longer supported. Call GET /v1/device/update.
426 example
{
  "status": false,
  "message": "This booth must update Folio Live.",
  "error": {
    "code": "failed_precondition",
    "status": 426,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
429 Rate limited. Wait Retry-After seconds.
429 example
{
  "status": false,
  "message": "Too many requests. Wait before trying again.",
  "error": {
    "code": "rate_limited",
    "status": 429,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
500 Something went wrong. Quote error.request_id.
500 example
{
  "status": false,
  "message": "Something went wrong. Quote the request id to support.",
  "error": {
    "code": "internal",
    "status": 500,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}

Code samples

curl -X POST "$FOLIO_API_URL/v1/device/events" \
  -H "Authorization: Bearer $FOLIO_DEVICE_KEY" \
  -H "Content-Type: application/json" \
  -H "X-Folio-Device-Contract: 1" \
  -d '{
  "events": [
    {
      "id": "evt-1730451600-1",
      "kind": "song.shown",
      "at_ms": 1730451600000,
      "payload": "{\"song\":\"amazing-grace\"}"
    }
  ]
}'

Try it is available for church operations and public reads. Booth devices pair with a code, so this operation is documented but not runnable here.

GET/v1/device/me

Who this key is#

Returns the booth the key belongs to, its church and its scopes. A revoked key answers 401 here; treat that as revoked and forget the key.

Auth. A booth device key. Needs the device:read scope. Send X-Folio-Device-Contract; an unsupported version gets 426.

Limits. Responses carry RateLimit-Limit and RateLimit-Remaining; a 429 carries Retry-After.

Auth
Booth device key
Scope
device:read
Operation id
deviceMe

Responses

Response data fields
FieldTypeDescription
idstringThe id, with a type prefix such as tch_ or note_.
labelstringThe label shown to people.
objectenum: deviceThe object type.
orgstringThe church the device belongs to.
scopesstring[]The scopes the key carries.
200 Success. Who this key is.
200 example
{
  "status": true,
  "message": "device",
  "data": {
    "object": "device",
    "id": "dev_0192f7a8-3b1c-7d2e-9a40-5c6d7e8f9012",
    "label": "Sanctuary booth",
    "org": "org_0192f7a8-3b1c-7d2e-9a40-5c6d7e8f9012",
    "scopes": [
      "device:read",
      "device:write"
    ]
  }
}
400 The request could not be read.
400 example
{
  "status": false,
  "message": "the page cursor is not valid",
  "error": {
    "code": "invalid_argument",
    "status": 400,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
401 The key or session is missing, invalid or expired.
401 example
{
  "status": false,
  "message": "That API key is not valid or has expired.",
  "error": {
    "code": "unauthenticated",
    "status": 401,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
403 The credential lacks the scope, the church is suspended, the feature is not in the plan, or the address is not allowed.
403 example
{
  "status": false,
  "message": "That API key does not have the required scope.",
  "error": {
    "code": "permission_denied",
    "status": 403,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
404 The resource is not in this church.
404 example
{
  "status": false,
  "message": "That item is not in this church.",
  "error": {
    "code": "not_found",
    "status": 404,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
409 Conflict, including a duplicate Idempotency-Key still being processed.
409 example
{
  "status": false,
  "message": "A request with this Idempotency-Key is still running.",
  "error": {
    "code": "conflict",
    "status": 409,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
422 A field failed validation, or an Idempotency-Key was reused with a different body.
422 example
{
  "status": false,
  "message": "A field is not valid.",
  "error": {
    "code": "validation",
    "status": 422,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
426 The booth's contract version is no longer supported. Call GET /v1/device/update.
426 example
{
  "status": false,
  "message": "This booth must update Folio Live.",
  "error": {
    "code": "failed_precondition",
    "status": 426,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
429 Rate limited. Wait Retry-After seconds.
429 example
{
  "status": false,
  "message": "Too many requests. Wait before trying again.",
  "error": {
    "code": "rate_limited",
    "status": 429,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
500 Something went wrong. Quote error.request_id.
500 example
{
  "status": false,
  "message": "Something went wrong. Quote the request id to support.",
  "error": {
    "code": "internal",
    "status": 500,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}

Code samples

curl -X GET "$FOLIO_API_URL/v1/device/me" \
  -H "Authorization: Bearer $FOLIO_DEVICE_KEY" \
  -H "X-Folio-Device-Contract: 1"

Try it is available for church operations and public reads. Booth devices pair with a code, so this operation is documented but not runnable here.

GET/v1/device/packs

List downloadable packs#

Lists song packs and the King James Version text pack the booth can download for offline use. Scripture on the booth is King James Version only.

Auth. A booth device key. Needs the device:read scope. Send X-Folio-Device-Contract; an unsupported version gets 426.

Limits. Responses carry RateLimit-Limit and RateLimit-Remaining; a 429 carries Retry-After.

Auth
Booth device key
Scope
device:read
Operation id
devicePacks

Responses

Response data fields
FieldTypeDescription
objectenum: device_packsThe object type.
songsvalue[]Downloadable song packs.
translationsvalue[]Downloadable scripture packs. King James Version only.
200 Success. List downloadable packs.
200 example
{
  "status": true,
  "message": "device_packs",
  "data": {
    "object": "device_packs",
    "songs": [
      {
        "id": "worship-2026",
        "name": "Worship 2026",
        "version": 3
      }
    ],
    "translations": [
      {
        "code": "KJV",
        "name": "King James Version",
        "version": 1
      }
    ]
  }
}
400 The request could not be read.
400 example
{
  "status": false,
  "message": "the page cursor is not valid",
  "error": {
    "code": "invalid_argument",
    "status": 400,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
401 The key or session is missing, invalid or expired.
401 example
{
  "status": false,
  "message": "That API key is not valid or has expired.",
  "error": {
    "code": "unauthenticated",
    "status": 401,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
403 The credential lacks the scope, the church is suspended, the feature is not in the plan, or the address is not allowed.
403 example
{
  "status": false,
  "message": "That API key does not have the required scope.",
  "error": {
    "code": "permission_denied",
    "status": 403,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
404 The resource is not in this church.
404 example
{
  "status": false,
  "message": "That item is not in this church.",
  "error": {
    "code": "not_found",
    "status": 404,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
409 Conflict, including a duplicate Idempotency-Key still being processed.
409 example
{
  "status": false,
  "message": "A request with this Idempotency-Key is still running.",
  "error": {
    "code": "conflict",
    "status": 409,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
422 A field failed validation, or an Idempotency-Key was reused with a different body.
422 example
{
  "status": false,
  "message": "A field is not valid.",
  "error": {
    "code": "validation",
    "status": 422,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
426 The booth's contract version is no longer supported. Call GET /v1/device/update.
426 example
{
  "status": false,
  "message": "This booth must update Folio Live.",
  "error": {
    "code": "failed_precondition",
    "status": 426,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
429 Rate limited. Wait Retry-After seconds.
429 example
{
  "status": false,
  "message": "Too many requests. Wait before trying again.",
  "error": {
    "code": "rate_limited",
    "status": 429,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
500 Something went wrong. Quote error.request_id.
500 example
{
  "status": false,
  "message": "Something went wrong. Quote the request id to support.",
  "error": {
    "code": "internal",
    "status": 500,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}

Code samples

curl -X GET "$FOLIO_API_URL/v1/device/packs" \
  -H "Authorization: Bearer $FOLIO_DEVICE_KEY" \
  -H "X-Folio-Device-Contract: 1"

Try it is available for church operations and public reads. Booth devices pair with a code, so this operation is documented but not runnable here.

GET/v1/device/plans/{plan}

Read a service plan#

Returns the service plan with the given id, or the latest with current. The body is the plan document the booth runs the service from.

Auth. A booth device key. Needs the device:read scope. Send X-Folio-Device-Contract; an unsupported version gets 426.

Limits. Responses carry RateLimit-Limit and RateLimit-Remaining; a 429 carries Retry-After.

Auth
Booth device key
Scope
device:read
Operation id
devicePlan

Parameters

Parameters
NameInDescription
plan requiredpathThe service plan id, or current for the latest. (string, e.g. current)

Responses

200 Success. Read a service plan.
200 example
{
  "status": true,
  "message": "device_plan",
  "data": {
    "id": "plan_2026_11_01",
    "title": "Sunday 1 November",
    "items": [
      {
        "kind": "song",
        "ref": "song-amazing-grace"
      }
    ]
  }
}
400 The request could not be read.
400 example
{
  "status": false,
  "message": "the page cursor is not valid",
  "error": {
    "code": "invalid_argument",
    "status": 400,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
401 The key or session is missing, invalid or expired.
401 example
{
  "status": false,
  "message": "That API key is not valid or has expired.",
  "error": {
    "code": "unauthenticated",
    "status": 401,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
403 The credential lacks the scope, the church is suspended, the feature is not in the plan, or the address is not allowed.
403 example
{
  "status": false,
  "message": "That API key does not have the required scope.",
  "error": {
    "code": "permission_denied",
    "status": 403,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
404 The resource is not in this church.
404 example
{
  "status": false,
  "message": "That item is not in this church.",
  "error": {
    "code": "not_found",
    "status": 404,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
409 Conflict, including a duplicate Idempotency-Key still being processed.
409 example
{
  "status": false,
  "message": "A request with this Idempotency-Key is still running.",
  "error": {
    "code": "conflict",
    "status": 409,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
422 A field failed validation, or an Idempotency-Key was reused with a different body.
422 example
{
  "status": false,
  "message": "A field is not valid.",
  "error": {
    "code": "validation",
    "status": 422,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
426 The booth's contract version is no longer supported. Call GET /v1/device/update.
426 example
{
  "status": false,
  "message": "This booth must update Folio Live.",
  "error": {
    "code": "failed_precondition",
    "status": 426,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
429 Rate limited. Wait Retry-After seconds.
429 example
{
  "status": false,
  "message": "Too many requests. Wait before trying again.",
  "error": {
    "code": "rate_limited",
    "status": 429,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
500 Something went wrong. Quote error.request_id.
500 example
{
  "status": false,
  "message": "Something went wrong. Quote the request id to support.",
  "error": {
    "code": "internal",
    "status": 500,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}

Code samples

curl -X GET "$FOLIO_API_URL/v1/device/plans/current" \
  -H "Authorization: Bearer $FOLIO_DEVICE_KEY" \
  -H "X-Folio-Device-Contract: 1"

Try it is available for church operations and public reads. Booth devices pair with a code, so this operation is documented but not runnable here.

POST/v1/device/teachings

Create a teaching from a booth recording#

The booth's version of creating a teaching: it returns a signed upload URL and a key. PUT the recording to url, then call POST /v1/device/teachings/{teaching}/upload with key. The same plan checks apply as for an API key, so the booth needs no second credential.

Auth. A booth device key. Needs the device:write scope. Send X-Folio-Device-Contract; an unsupported version gets 426.

Plan. Needs the booth feature and transcription hours left in the plan (402 quota_exceeded otherwise).

Limits. Responses carry RateLimit-Limit and RateLimit-Remaining; a 429 carries Retry-After.

Auth
Booth device key
Scope
device:write
Plan
Needs the booth feature and transcription hours left in the plan (402 quota_exceeded otherwise).
Operation id
deviceCreateTeaching

Parameters

Parameters
NameInDescription
Idempotency-KeyheaderMakes a retry safe. The same key with the same body replays the first response for 24 hours; the same key with a different body returns 422; a duplicate that is still running returns 409 with Retry-After. Responses that carry a one-time secret are not replayed. (string, e.g. sunday-2026-11-01-upload)

Request body

Request body fields
FieldTypeDescription
content_typestringThe media type of the recording, such as audio/mpeg.
filenamestringThe file name of the recording, used to keep the extension.
titlestringThe title.
Example request body
{
  "title": "Sunday service",
  "filename": "service.mp4",
  "content_type": "video/mp4"
}

Responses

Response data fields
FieldTypeDescription
expires_atstringWhen it stops working, in RFC 3339 UTC.
keystringSend this back when completing the upload.
objectenum: uploadThe object type.
teaching_idstringThe teaching id, prefixed tch_.
urlstringSigned URL. PUT the recording bytes here with the same Content-Type.
200 Success. Create a teaching from a booth recording.
200 example
{
  "status": true,
  "message": "upload ready",
  "data": {
    "object": "upload",
    "teaching_id": "tch_0192f7a8-3b1c-7d2e-9a40-5c6d7e8f9012",
    "key": "originals/0192f7a8-3b1c-7d2e-9a40-5c6d7e8f9099/0192f7a8-3b1c-7d2e-9a40-5c6d7e8f9012/service.mp4",
    "url": "https://files.example.org/originals/0192f7a8-3b1c-7d2e-9a40-5c6d7e8f9099/0192f7a8-3b1c-7d2e-9a40-5c6d7e8f9012/service.mp4?X-Signature=abc123",
    "expires_at": "2026-11-01T10:15:00Z"
  }
}
400 The request could not be read.
400 example
{
  "status": false,
  "message": "the page cursor is not valid",
  "error": {
    "code": "invalid_argument",
    "status": 400,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
401 The key or session is missing, invalid or expired.
401 example
{
  "status": false,
  "message": "That API key is not valid or has expired.",
  "error": {
    "code": "unauthenticated",
    "status": 401,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
403 The credential lacks the scope, the church is suspended, the feature is not in the plan, or the address is not allowed.
403 example
{
  "status": false,
  "message": "That API key does not have the required scope.",
  "error": {
    "code": "permission_denied",
    "status": 403,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
404 The resource is not in this church.
404 example
{
  "status": false,
  "message": "That item is not in this church.",
  "error": {
    "code": "not_found",
    "status": 404,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
409 Conflict, including a duplicate Idempotency-Key still being processed.
409 example
{
  "status": false,
  "message": "A request with this Idempotency-Key is still running.",
  "error": {
    "code": "conflict",
    "status": 409,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
422 A field failed validation, or an Idempotency-Key was reused with a different body.
422 example
{
  "status": false,
  "message": "A field is not valid.",
  "error": {
    "code": "validation",
    "status": 422,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
426 The booth's contract version is no longer supported. Call GET /v1/device/update.
426 example
{
  "status": false,
  "message": "This booth must update Folio Live.",
  "error": {
    "code": "failed_precondition",
    "status": 426,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
429 Rate limited. Wait Retry-After seconds.
429 example
{
  "status": false,
  "message": "Too many requests. Wait before trying again.",
  "error": {
    "code": "rate_limited",
    "status": 429,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
500 Something went wrong. Quote error.request_id.
500 example
{
  "status": false,
  "message": "Something went wrong. Quote the request id to support.",
  "error": {
    "code": "internal",
    "status": 500,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}

Code samples

curl -X POST "$FOLIO_API_URL/v1/device/teachings" \
  -H "Authorization: Bearer $FOLIO_DEVICE_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: booth-service-2026-11-01" \
  -H "X-Folio-Device-Contract: 1" \
  -d '{
  "title": "Sunday service",
  "filename": "service.mp4",
  "content_type": "video/mp4"
}'

Try it is available for church operations and public reads. Booth devices pair with a code, so this operation is documented but not runnable here.

POST/v1/device/teachings/{teaching}/upload

Finish a booth upload#

Tells Folio the recording is in storage and starts processing.

Auth. A booth device key. Needs the device:write scope. Send X-Folio-Device-Contract; an unsupported version gets 426.

Limits. Responses carry RateLimit-Limit and RateLimit-Remaining; a 429 carries Retry-After.

Auth
Booth device key
Scope
device:write
Operation id
deviceCompleteUpload

Parameters

Parameters
NameInDescription
teaching requiredpathThe teaching id, such as tch_0192f7a8-3b1c-7d2e-9a40-5c6d7e8f9012. (string, e.g. tch_0192f7a8-3b1c-7d2e-9a40-5c6d7e8f9012)
Idempotency-KeyheaderMakes a retry safe. The same key with the same body replays the first response for 24 hours; the same key with a different body returns 422; a duplicate that is still running returns 409 with Retry-After. Responses that carry a one-time secret are not replayed. (string, e.g. sunday-2026-11-01-upload)

Request body

Request body fields
FieldTypeDescription
keystringThe key.
Example request body
{
  "key": "originals/0192f7a8-3b1c-7d2e-9a40-5c6d7e8f9099/0192f7a8-3b1c-7d2e-9a40-5c6d7e8f9012/service.mp4"
}

Responses

Response data fields
FieldTypeDescription
objectenum: teachingThe object type.
teaching_idstringThe teaching id, prefixed tch_.
200 Success. Finish a booth upload.
200 example
{
  "status": true,
  "message": "upload received",
  "data": {
    "object": "teaching",
    "teaching_id": "tch_0192f7a8-3b1c-7d2e-9a40-5c6d7e8f9012"
  }
}
400 The request could not be read.
400 example
{
  "status": false,
  "message": "the page cursor is not valid",
  "error": {
    "code": "invalid_argument",
    "status": 400,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
401 The key or session is missing, invalid or expired.
401 example
{
  "status": false,
  "message": "That API key is not valid or has expired.",
  "error": {
    "code": "unauthenticated",
    "status": 401,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
403 The credential lacks the scope, the church is suspended, the feature is not in the plan, or the address is not allowed.
403 example
{
  "status": false,
  "message": "That API key does not have the required scope.",
  "error": {
    "code": "permission_denied",
    "status": 403,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
404 The resource is not in this church.
404 example
{
  "status": false,
  "message": "That item is not in this church.",
  "error": {
    "code": "not_found",
    "status": 404,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
409 Conflict, including a duplicate Idempotency-Key still being processed.
409 example
{
  "status": false,
  "message": "A request with this Idempotency-Key is still running.",
  "error": {
    "code": "conflict",
    "status": 409,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
422 A field failed validation, or an Idempotency-Key was reused with a different body.
422 example
{
  "status": false,
  "message": "A field is not valid.",
  "error": {
    "code": "validation",
    "status": 422,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
426 The booth's contract version is no longer supported. Call GET /v1/device/update.
426 example
{
  "status": false,
  "message": "This booth must update Folio Live.",
  "error": {
    "code": "failed_precondition",
    "status": 426,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
429 Rate limited. Wait Retry-After seconds.
429 example
{
  "status": false,
  "message": "Too many requests. Wait before trying again.",
  "error": {
    "code": "rate_limited",
    "status": 429,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
500 Something went wrong. Quote error.request_id.
500 example
{
  "status": false,
  "message": "Something went wrong. Quote the request id to support.",
  "error": {
    "code": "internal",
    "status": 500,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}

Code samples

curl -X POST "$FOLIO_API_URL/v1/device/teachings/tch_0192f7a8-3b1c-7d2e-9a40-5c6d7e8f9012/upload" \
  -H "Authorization: Bearer $FOLIO_DEVICE_KEY" \
  -H "Content-Type: application/json" \
  -H "X-Folio-Device-Contract: 1" \
  -d '{
  "key": "originals/0192f7a8-3b1c-7d2e-9a40-5c6d7e8f9099/0192f7a8-3b1c-7d2e-9a40-5c6d7e8f9012/service.mp4"
}'

Try it is available for church operations and public reads. Booth devices pair with a code, so this operation is documented but not runnable here.

GET/v1/device/update

Check for a Folio Live update#

Returns the latest and the minimum supported version for a platform, and where to download. It works even when X-Folio-Device-Contract is too old, so a stale booth can learn where to update. update_required means the booth must update before other calls succeed.

Auth. A booth device key. Needs the device:read scope. Send X-Folio-Device-Contract; an unsupported version gets 426.

Limits. Responses carry RateLimit-Limit and RateLimit-Remaining; a 429 carries Retry-After.

Auth
Booth device key
Scope
device:read
Operation id
deviceUpdate

Parameters

Parameters
NameInDescription
platformqueryFor example windows, macos or linux. (string, e.g. windows)
currentqueryThe version the booth runs now, such as 1.4.2. (string, e.g. 1.4.2)

Responses

Response data fields
FieldTypeDescription
contractobjectThe X-Folio-Device-Contract versions this API accepts. Other values get 426.
currentinteger (int64)The newest contract version the API speaks.
minimuminteger (int64)The oldest contract version accepted.
currentstringThe version the booth reported.
lateststringThe newest Folio Live version.
min_supportedstringThe oldest version that still works.
notesstringRelease notes for the update.
objectenum: device_updateThe object type.
platformstringwindows, mac or linux.
update_availablebooleanWhether a newer version exists.
update_requiredbooleanTrue when current is below min_supported. The booth must update before it can sync.
urlstringWhere to download the build for this platform, when one is published.
200 Success. Check for a Folio Live update.
200 example
{
  "status": true,
  "message": "device_update",
  "data": {
    "object": "device_update",
    "platform": "windows",
    "current": "1.4.2",
    "latest": "1.5.0",
    "min_supported": "1.4.0",
    "update_available": true,
    "update_required": false,
    "url": "https://downloads.example.org/folio-live/windows/1.5.0",
    "notes": "Faster library sync.",
    "contract": {
      "current": 1,
      "minimum": 1
    }
  }
}
400 The request could not be read.
400 example
{
  "status": false,
  "message": "the page cursor is not valid",
  "error": {
    "code": "invalid_argument",
    "status": 400,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
401 The key or session is missing, invalid or expired.
401 example
{
  "status": false,
  "message": "That API key is not valid or has expired.",
  "error": {
    "code": "unauthenticated",
    "status": 401,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
403 The credential lacks the scope, the church is suspended, the feature is not in the plan, or the address is not allowed.
403 example
{
  "status": false,
  "message": "That API key does not have the required scope.",
  "error": {
    "code": "permission_denied",
    "status": 403,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
404 The resource is not in this church.
404 example
{
  "status": false,
  "message": "That item is not in this church.",
  "error": {
    "code": "not_found",
    "status": 404,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
409 Conflict, including a duplicate Idempotency-Key still being processed.
409 example
{
  "status": false,
  "message": "A request with this Idempotency-Key is still running.",
  "error": {
    "code": "conflict",
    "status": 409,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
422 A field failed validation, or an Idempotency-Key was reused with a different body.
422 example
{
  "status": false,
  "message": "A field is not valid.",
  "error": {
    "code": "validation",
    "status": 422,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
426 This booth must update Folio Live. See GET /v1/device/update.
429 Rate limited. Wait Retry-After seconds.
429 example
{
  "status": false,
  "message": "Too many requests. Wait before trying again.",
  "error": {
    "code": "rate_limited",
    "status": 429,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
500 Something went wrong. Quote error.request_id.
500 example
{
  "status": false,
  "message": "Something went wrong. Quote the request id to support.",
  "error": {
    "code": "internal",
    "status": 500,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}

Code samples

curl -X GET "$FOLIO_API_URL/v1/device/update?current=1.4.2&platform=windows" \
  -H "Authorization: Bearer $FOLIO_DEVICE_KEY" \
  -H "X-Folio-Device-Contract: 1"

Try it is available for church operations and public reads. Booth devices pair with a code, so this operation is documented but not runnable here.

POST/v1/devices/pair

Trade a pairing code for a device key#

A booth sends the six-digit code a team member created in the console. The code is single use and valid for 10 minutes. The response holds the device key (folio_booth_…) exactly once; store it securely on the booth.

This route is public because the booth has no key yet. It is rate limited per client address and per code prefix; past the limit it answers 429. Unknown, expired and used codes all answer the same 422, so a guess reveals nothing. Codes are stored hashed.

Auth. None. This route is public.

Plan. The church needs the booth feature.

Auth
No key
Plan
The church needs the booth feature.
Operation id
claimDevicePairing

Request body

Request body fields
FieldTypeDescription
codestringA machine-readable code.
Example request body
{
  "code": "482913"
}

Responses

Response data fields
FieldTypeDescription
devicestringDevice id (dev_...).
idstringThe id, with a type prefix such as tch_ or note_.
keystringThe device key (folio_booth_...). Shown once.
objectenum: device_pairingThe object type.
pairedbooleanAlways true when pairing worked.
200 Success. Trade a pairing code for a device key.
200 example
{
  "status": true,
  "message": "device paired",
  "data": {
    "object": "device_pairing",
    "paired": true,
    "id": "dev_0192f7a8-3b1c-7d2e-9a40-5c6d7e8f9012",
    "device": "Sanctuary booth",
    "key": "folio_booth_a1b2c3d4e5f60718293a4b5c6d7e8f90"
  }
}
400 The request could not be read.
400 example
{
  "status": false,
  "message": "the page cursor is not valid",
  "error": {
    "code": "invalid_argument",
    "status": 400,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
422 The code is unknown, expired or already used.
422 example
{
  "status": false,
  "message": "That code is not valid. Ask for a new one.",
  "error": {
    "code": "validation",
    "status": 422,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}
500 Something went wrong. Quote error.request_id.
500 example
{
  "status": false,
  "message": "Something went wrong. Quote the request id to support.",
  "error": {
    "code": "internal",
    "status": 500,
    "request_id": "req_b337770ecebe03b03a88a8998b59263d"
  }
}

Code samples

curl -X POST "$FOLIO_API_URL/v1/devices/pair" \
  -H "Content-Type: application/json" \
  -d '{
  "code": "482913"
}'

Verified against API version 0.0.0-dev. Generated from the live OpenAPI document.