Send a device's queued commands
sync.push
POST/api/v1/households/{household_id}/sync/push
- Route
Runs the commands a device queued while it was offline, in client_seq order, each as you. A sequence number the household already processed is skipped, so a resend is safe. A gap makes it stop and answer with resend_from. A command it can't run (it doesn't parse, it isn't a command, or it fails) is recorded as rejected and does not block the ones after it; the next pull says which were rejected and why. A client_id belongs to the first user who pushed with it. See the sync guide.
Parameters#
| Field | Description |
|---|---|
household_idpathstringrequired | The household's ID. |
Request body#
JSON, as application/json.
| Field | Description |
|---|---|
client_idstringrequired | An installation's ID (cli_…), made once and kept in its local database. It belongs to one user. |
commandsarray of objectsrequired | Queued commands in client_seq order. |
commands[].client_seqintegerrequired | This device's sequence number for the command, from 1. |
commands[].command_idstringrequired | The command's ID (cmd_…); the same on every retry. |
commands[].namestringrequired | The command's name, such as |
commands[].inputanyoptional | The command's input, as the device ran it. |
commands[].issued_atintegerrequired | When the person issued it on the device, UTC milliseconds. |
Response#
200 How far the household got through the commands.
| Field | Description |
|---|---|
last_client_seqintegerrequired | The highest of this device's commands the server has processed. |
resend_frominteger or nullrequired | When there was a gap, the client_seq to resend from; otherwise null. |
Errors#
Every error has the same envelope: error.code, error.message, error.details, error.recovery and error.request_id.
| Status | Code | When |
|---|---|---|
400 | validation_failed | A field is missing, misspelled or out of range. details.fields lists each problem. |
401 | unauthenticated | The request has no valid session. |
403 | Read error.code | Described above. |
404 | not_found | The household doesn't exist, or you aren't a member of it. |
Examples#
These are real. Each was recorded from a sample household, the Okafors, running through the whole API when this site was built.
Send a device's queued commands
Runs them in client_seq order, as you. One the household can't run is recorded as rejected and doesn't hold up the rest.
curl -X POST 'https://rigel.house/api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/sync/push' \
-H "Cookie: rigel_session=$RIGEL_SESSION" \
-H 'Content-Type: application/json' \
-d '{
"client_id": "cli_01M6PHONE7R3QWT8XK2VNB0Y5D",
"commands": [
{
"client_seq": 1,
"command_id": "cmd_01M6V9CS85QPQN4JDKVH308QYZ",
"name": "family.note.record",
"input": {
"person_id": "per_01M6V9CS79TMYHDAM7FTA9ZK3Z",
"text": "Nia is practicing her recorder for the winter concert."
},
"issued_at": 1794064492005
},
{
"client_seq": 2,
"command_id": "cmd_01M6V9CS85HPG7JWHT24KJWDZQ",
"name": "family.note.forget",
"input": {
"note_id": "fnt_01JC0000000000000000000000"
},
"issued_at": 1794064552005
},
{
"client_seq": 3,
"command_id": "cmd_01M6V9CS85HMC5XPRT3NDRR6QV",
"name": "family.note.record",
"input": {
"text": "The concert is the evening of Dec 10.",
"app": "calendar"
},
"issued_at": 1794064612005
}
]
}'const response = await fetch("https://rigel.house/api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/sync/push", {
method: "POST",
headers: {
"Content-Type": "application/json",
Cookie: `rigel_session=${process.env.RIGEL_SESSION}`,
},
body: JSON.stringify({
client_id: "cli_01M6PHONE7R3QWT8XK2VNB0Y5D",
commands: [
{
client_seq: 1,
command_id: "cmd_01M6V9CS85QPQN4JDKVH308QYZ",
name: "family.note.record",
input: {
person_id: "per_01M6V9CS79TMYHDAM7FTA9ZK3Z",
text: "Nia is practicing her recorder for the winter concert.",
},
issued_at: 1794064492005,
},
{
client_seq: 2,
command_id: "cmd_01M6V9CS85HPG7JWHT24KJWDZQ",
name: "family.note.forget",
input: {
note_id: "fnt_01JC0000000000000000000000",
},
issued_at: 1794064552005,
},
{
client_seq: 3,
command_id: "cmd_01M6V9CS85HMC5XPRT3NDRR6QV",
name: "family.note.record",
input: {
text: "The concert is the evening of Dec 10.",
app: "calendar",
},
issued_at: 1794064612005,
},
],
}),
});
const data = await response.json();POST /api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/sync/push HTTP/1.1
Host: rigel.house
Cookie: rigel_session=<your session>
Content-Type: application/json
{
"client_id": "cli_01M6PHONE7R3QWT8XK2VNB0Y5D",
"commands": [
{
"client_seq": 1,
"command_id": "cmd_01M6V9CS85QPQN4JDKVH308QYZ",
"name": "family.note.record",
"input": {
"person_id": "per_01M6V9CS79TMYHDAM7FTA9ZK3Z",
"text": "Nia is practicing her recorder for the winter concert."
},
"issued_at": 1794064492005
},
{
"client_seq": 2,
"command_id": "cmd_01M6V9CS85HPG7JWHT24KJWDZQ",
"name": "family.note.forget",
"input": {
"note_id": "fnt_01JC0000000000000000000000"
},
"issued_at": 1794064552005
},
{
"client_seq": 3,
"command_id": "cmd_01M6V9CS85HMC5XPRT3NDRR6QV",
"name": "family.note.record",
"input": {
"text": "The concert is the evening of Dec 10.",
"app": "calendar"
},
"issued_at": 1794064612005
}
]
}HTTP/1.1 200 OK
Content-Type: application/json
X-Request-Id: req_01M6V9CS89WF75ZWWHMZE1YNSJ
{
"last_client_seq": 3,
"resend_from": null
}Send the same commands again
A resend after a lost response changes nothing: sequence numbers the household already processed are skipped.
curl -X POST 'https://rigel.house/api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/sync/push' \
-H "Cookie: rigel_session=$RIGEL_SESSION" \
-H 'Content-Type: application/json' \
-d '{
"client_id": "cli_01M6PHONE7R3QWT8XK2VNB0Y5D",
"commands": [
{
"client_seq": 1,
"command_id": "cmd_01M6V9CS85QPQN4JDKVH308QYZ",
"name": "family.note.record",
"input": {
"person_id": "per_01M6V9CS79TMYHDAM7FTA9ZK3Z",
"text": "Nia is practicing her recorder for the winter concert."
},
"issued_at": 1794064492005
},
{
"client_seq": 2,
"command_id": "cmd_01M6V9CS85HPG7JWHT24KJWDZQ",
"name": "family.note.forget",
"input": {
"note_id": "fnt_01JC0000000000000000000000"
},
"issued_at": 1794064552005
},
{
"client_seq": 3,
"command_id": "cmd_01M6V9CS85HMC5XPRT3NDRR6QV",
"name": "family.note.record",
"input": {
"text": "The concert is the evening of Dec 10.",
"app": "calendar"
},
"issued_at": 1794064612005
}
]
}'const response = await fetch("https://rigel.house/api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/sync/push", {
method: "POST",
headers: {
"Content-Type": "application/json",
Cookie: `rigel_session=${process.env.RIGEL_SESSION}`,
},
body: JSON.stringify({
client_id: "cli_01M6PHONE7R3QWT8XK2VNB0Y5D",
commands: [
{
client_seq: 1,
command_id: "cmd_01M6V9CS85QPQN4JDKVH308QYZ",
name: "family.note.record",
input: {
person_id: "per_01M6V9CS79TMYHDAM7FTA9ZK3Z",
text: "Nia is practicing her recorder for the winter concert.",
},
issued_at: 1794064492005,
},
{
client_seq: 2,
command_id: "cmd_01M6V9CS85HPG7JWHT24KJWDZQ",
name: "family.note.forget",
input: {
note_id: "fnt_01JC0000000000000000000000",
},
issued_at: 1794064552005,
},
{
client_seq: 3,
command_id: "cmd_01M6V9CS85HMC5XPRT3NDRR6QV",
name: "family.note.record",
input: {
text: "The concert is the evening of Dec 10.",
app: "calendar",
},
issued_at: 1794064612005,
},
],
}),
});
const data = await response.json();POST /api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/sync/push HTTP/1.1
Host: rigel.house
Cookie: rigel_session=<your session>
Content-Type: application/json
{
"client_id": "cli_01M6PHONE7R3QWT8XK2VNB0Y5D",
"commands": [
{
"client_seq": 1,
"command_id": "cmd_01M6V9CS85QPQN4JDKVH308QYZ",
"name": "family.note.record",
"input": {
"person_id": "per_01M6V9CS79TMYHDAM7FTA9ZK3Z",
"text": "Nia is practicing her recorder for the winter concert."
},
"issued_at": 1794064492005
},
{
"client_seq": 2,
"command_id": "cmd_01M6V9CS85HPG7JWHT24KJWDZQ",
"name": "family.note.forget",
"input": {
"note_id": "fnt_01JC0000000000000000000000"
},
"issued_at": 1794064552005
},
{
"client_seq": 3,
"command_id": "cmd_01M6V9CS85HMC5XPRT3NDRR6QV",
"name": "family.note.record",
"input": {
"text": "The concert is the evening of Dec 10.",
"app": "calendar"
},
"issued_at": 1794064612005
}
]
}HTTP/1.1 200 OK
Content-Type: application/json
X-Request-Id: req_01M6V9CS8B5FRSM60JFSE9S885
{
"last_client_seq": 3,
"resend_from": null
}Skip ahead in the queue
A gap makes the household stop and say where to resend from.
curl -X POST 'https://rigel.house/api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/sync/push' \
-H "Cookie: rigel_session=$RIGEL_SESSION" \
-H 'Content-Type: application/json' \
-d '{
"client_id": "cli_01M6PHONE7R3QWT8XK2VNB0Y5D",
"commands": [
{
"client_seq": 6,
"command_id": "cmd_01M6V9CS8CDAX3GJBVPNZF0XRK",
"name": "family.note.record",
"input": {
"text": "Out of order."
},
"issued_at": 1794064792012
}
]
}'const response = await fetch("https://rigel.house/api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/sync/push", {
method: "POST",
headers: {
"Content-Type": "application/json",
Cookie: `rigel_session=${process.env.RIGEL_SESSION}`,
},
body: JSON.stringify({
client_id: "cli_01M6PHONE7R3QWT8XK2VNB0Y5D",
commands: [
{
client_seq: 6,
command_id: "cmd_01M6V9CS8CDAX3GJBVPNZF0XRK",
name: "family.note.record",
input: {
text: "Out of order.",
},
issued_at: 1794064792012,
},
],
}),
});
const data = await response.json();POST /api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/sync/push HTTP/1.1
Host: rigel.house
Cookie: rigel_session=<your session>
Content-Type: application/json
{
"client_id": "cli_01M6PHONE7R3QWT8XK2VNB0Y5D",
"commands": [
{
"client_seq": 6,
"command_id": "cmd_01M6V9CS8CDAX3GJBVPNZF0XRK",
"name": "family.note.record",
"input": {
"text": "Out of order."
},
"issued_at": 1794064792012
}
]
}HTTP/1.1 200 OK
Content-Type: application/json
X-Request-Id: req_01M6V9CS8CGKFBKNDMG6K2QRND
{
"last_client_seq": 3,
"resend_from": 4
}A push that doesn't fit the protocol
curl -X POST 'https://rigel.house/api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/sync/push' \
-H "Cookie: rigel_session=$RIGEL_SESSION" \
-H 'Content-Type: application/json' \
-d '{
"client_id": "",
"commands": [
{
"client_seq": 1,
"command_id": "not-a-command-id",
"name": "family.note.record",
"issued_at": 1
}
]
}'const response = await fetch("https://rigel.house/api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/sync/push", {
method: "POST",
headers: {
"Content-Type": "application/json",
Cookie: `rigel_session=${process.env.RIGEL_SESSION}`,
},
body: JSON.stringify({
client_id: "",
commands: [
{
client_seq: 1,
command_id: "not-a-command-id",
name: "family.note.record",
issued_at: 1,
},
],
}),
});
const data = await response.json();POST /api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/sync/push HTTP/1.1
Host: rigel.house
Cookie: rigel_session=<your session>
Content-Type: application/json
{
"client_id": "",
"commands": [
{
"client_seq": 1,
"command_id": "not-a-command-id",
"name": "family.note.record",
"issued_at": 1
}
]
}HTTP/1.1 400 Bad Request
Content-Type: application/json
X-Request-Id: req_01M6V9CS8GZ1PKDMPHBZC2ARF2
{
"error": {
"code": "validation_failed",
"message": "The request has invalid fields. No changes were applied.",
"details": {
"fields": [
{
"field": "client_id",
"problem": "Too small: expected string to have >=1 characters"
},
{
"field": "commands[0].command_id",
"problem": "must be a command ID starting with cmd_"
},
{
"field": "commands[0].input",
"problem": "Invalid input: expected nonoptional, received undefined"
}
]
},
"recovery": "Correct each field listed in details.fields, then send the request again.",
"request_id": "req_01M6V9CS8GZ1PKDMPHBZC2ARF2"
}
}