SDKs · API v1 · Developer documentation
Build with the Handwrite SDKs.
Typed clients for every endpoint a Handwrite token can call, with retries, pagination and webhook checks built in. Pick a language: every example on this page switches with it.
- API v1
- Node 0.1.0
- Python 0.1.0
- Go 0.1.0
- Rust 0.1.0
- Java 0.1.0
- Kotlin 0.1.0
- C# 0.1.0
- C++ 0.1.0
- C 0.1.0
Node SDK
The reference site at handwrite.gitlab.io/sdk/node/ is being published from the repository and may not be live yet; the repository README documents the same API.
Python SDK
The reference site at handwrite.gitlab.io/sdk/python/ is being published from the repository and may not be live yet; the repository README documents the same API.
Go SDK
The reference site at handwrite.gitlab.io/sdk/go/ is being published from the repository and may not be live yet; the repository README documents the same API.
Rust SDK
The reference site at handwrite.gitlab.io/sdk/rust/ is being published from the repository and may not be live yet; the repository README documents the same API.
Java SDK
The reference site at handwrite.gitlab.io/sdk/java/ is being published from the repository and may not be live yet; the repository README documents the same API.
Kotlin SDK
The reference site at handwrite.gitlab.io/sdk/kotlin/ is being published from the repository and may not be live yet; the repository README documents the same API.
C# SDK
The reference site at handwrite.gitlab.io/sdk/dotnet/ is being published from the repository and may not be live yet; the repository README documents the same API.
C++ SDK
The reference site at handwrite.gitlab.io/sdk/cpp/ is being published from the repository and may not be live yet; the repository README documents the same API.
C SDK
The reference site at handwrite.gitlab.io/sdk/c/ is being published from the repository and may not be live yet; the repository README documents the same API.
Install
These SDKs are pre-releases that aren’t on package registries yet. The commands work once a release is published; until then, build from the repository.
npm install handwrites.iopip install handwrites.iouv add handwrites.iogo get gitlab.com/handwrite/sdk/go@v0.1.0cargo add handwritescargo add handwrites --features blocking<dependency>
<groupId>io.handwrites</groupId>
<artifactId>handwrites</artifactId>
<version>0.1.0</version>
</dependency>implementation("io.handwrites:handwrites:0.1.0")dependencies {
implementation("io.handwrites:handwrites-kotlin:0.1.0")
}<dependency>
<groupId>io.handwrites</groupId>
<artifactId>handwrites-kotlin</artifactId>
<version>0.1.0</version>
</dependency>dotnet add package Handwrites.IO --version 0.1.0git clone https://gitlab.com/handwrite/sdk/cpp.git && cd cpp
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DHANDWRITES_BUILD_TESTS=OFF
cmake --build build && cmake --install buildfind_package(handwrites 0.1 REQUIRED)
target_link_libraries(my_app PRIVATE handwrites::handwrites)git clone https://gitlab.com/handwrite/sdk/c.git && cd c
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DHANDWRITES_BUILD_TESTS=OFF
cmake --build build && cmake --install buildfind_package(handwrites 0.1 REQUIRED)
target_link_libraries(my_app PRIVATE handwrites::handwrites)cc app.c $(pkg-config --cflags --libs handwrites)Quickstart
Create a client with an API key from Account → Developer, list your documents and create one.
const hw = new Handwrite({ accountType: "regular", token: process.env.HANDWRITE_TOKEN! });
const me = await hw.User.get();
console.log(`Signed in as ${me.principal}`);
const page = await hw.Documents.list({ limit: 10 });
for (const doc of page.documents) {
if (!("deleted" in doc)) console.log(doc.id, doc.title);
}
const doc = await hw.Documents.create({ title: "Field notes", content: "The heron came back." });
console.log(`Created ${doc.id} at revision ${doc.revision}`);from handwrites import Handwrite
hw = Handwrite(account_type="regular", token=os.environ["HANDWRITE_TOKEN"])
me = hw.user.get()
print("Signed in as", me["principal"])
page = hw.documents.list(limit=10)
for doc in page["documents"]:
if "deleted" not in doc:
print(doc["id"], doc["title"])
doc = hw.documents.create(title="Field notes", content="The heron came back.")
print(f"Created {doc['id']} at revision {doc['revision']}")hw, err := handwrites.NewRegular(os.Getenv("HANDWRITE_TOKEN"))
if err != nil {
return err
}
me, err := hw.User.Get(ctx)
if err != nil {
return err
}
fmt.Println("Signed in as", me.Principal)
page, err := hw.Documents.List(ctx, handwrites.ListDocumentsParams{Limit: 10})
if err != nil {
return err
}
for _, doc := range page.Documents {
if !doc.Deleted {
fmt.Println(doc.ID, doc.Title)
}
}
doc, err := hw.Documents.Create(ctx, handwrites.CreateDocument{Title: "Field notes", Content: "The heron came back."})
if err != nil {
return err
}
fmt.Printf("Created %s at revision %d\n", doc.ID, doc.Revision)let hw = Client::<Regular>::new(std::env::var("HANDWRITE_TOKEN").expect("HANDWRITE_TOKEN"))?;
let me = hw.user().get().await?;
println!("Signed in as {}", me.principal);
let page = hw.documents().list(ListDocuments { limit: Some(10), ..Default::default() }).await?;
for doc in page.documents.iter().filter_map(|entry| entry.document()) {
println!("{} {}", doc.id, doc.title);
}
let doc = hw.documents().create(CreateDocument::new("Field notes").content("The heron came back.")).await?;
println!("Created {} at revision {}", doc.id, doc.revision);RegularClient hw = RegularClient.builder().token(System.getenv("HANDWRITE_TOKEN")).build();
Me me = hw.user().get();
System.out.println("Signed in as " + me.principal());
DocumentPage page = hw.documents().list(ListDocumentsParams.none().withLimit(10));
for (DocumentItem item : page.documents()) {
if (item instanceof Document doc) System.out.println(doc.id() + " " + doc.title());
}
Document doc = hw.documents().create(CreateDocument.of("Field notes", "The heron came back."));
System.out.println("Created " + doc.id() + " at revision " + doc.revision());Handwrite.regular { token = System.getenv("HANDWRITE_TOKEN") }.use { hw ->
val me = hw.user.get()
println("Signed in as ${me.principal}")
val page = hw.documents.list(limit = 10)
page.documents.filterIsInstance<Document>().forEach { doc -> println("${doc.id} ${doc.title}") }
val doc = hw.documents.create(CreateDocument("Field notes", "The heron came back."))
println("Created ${doc.id} at revision ${doc.revision}")
}using var hw = new RegularClient(Environment.GetEnvironmentVariable("HANDWRITE_TOKEN")!);
var me = await hw.User.GetAsync();
Console.WriteLine($"Signed in as {me.Principal}");
var page = await hw.Documents.ListAsync(new ListDocumentsParams { Limit = 10 });
foreach (var d in page.Documents)
{
if (!d.Deleted) Console.WriteLine($"{d.Id} {d.Title}");
}
var doc = await hw.Documents.CreateAsync(new CreateDocument { Title = "Field notes", Content = "The heron came back." });
Console.WriteLine($"Created {doc.Id} at revision {doc.Revision}");handwrites::RegularClient hw{std::getenv("HANDWRITE_TOKEN")};
auto me = hw.user.get();
std::cout << "Signed in as " << me.principal << "\n";
auto page = hw.documents.list({.limit = 10});
for (const auto& doc : page.documents) {
if (!doc.deleted) std::cout << doc.id << " " << doc.title << "\n";
}
auto doc = hw.documents.create({.title = "Field notes", .content = "The heron came back."});
std::cout << "Created " << doc.id << " at revision " << doc.revision << "\n";hw_error *err = NULL;
hw_client *hw = hw_client_new(HW_ACCOUNT_REGULAR, getenv("HANDWRITE_TOKEN"), NULL, &err);
if (!hw) {
fprintf(stderr, "%s\n", err->message);
hw_error_free(err);
return 1;
}
hw_json *me = hw_user_get(hw, NULL, &err);
if (me) printf("Signed in as %s\n", hw_json_get_string(me, "principal"));
hw_json_free(me);
hw_list_documents_params params = {.limit = 10};
hw_json *page = hw_documents_list(hw, ¶ms, NULL, &err);
const hw_json *documents = hw_json_get(page, "documents");
for (size_t i = 0; i < hw_json_size(documents); i++) {
const hw_json *doc = hw_json_at(documents, i);
if (!hw_json_get_bool(doc, "deleted", 0)) {
printf("%s %s\n", hw_json_get_string(doc, "id"), hw_json_get_string(doc, "title"));
}
}
hw_json_free(page);
hw_json *input = hw_json_object();
hw_json_set_string(input, "title", "Field notes");
hw_json_set_string(input, "content", "The heron came back.");
hw_json *doc = hw_documents_create(hw, input, NULL, &err);
if (doc) {
printf("Created %s at revision %.0f\n", hw_json_get_string(doc, "id"), hw_json_get_number(doc, "revision", 0));
}
hw_json_free(input);
hw_json_free(doc);
hw_error_free(err); // NULL is fine
hw_client_free(hw);Create a client
The account type decides which resources the client has, checked by the compiler where the language allows.
const hw = new Handwrite({ accountType: "regular", token: process.env.HANDWRITE_TOKEN! });
// Every /api/v1 resource for one person: hw.Documents, hw.Audio, hw.Webhooks…const team = new Handwrite({ accountType: "team", token: process.env.HANDWRITE_TOKEN! });
const { organisations } = await team.Orgs.list(); // let the person choose the team
// The console (team.Console.*) needs a person's own sign-in token, not an API key.
const admin = new Handwrite({ accountType: "team", token: cognitoAccessToken });const school = new Handwrite({ accountType: "classroom", token: process.env.HANDWRITE_TOKEN! });
// school.Orgs lists classes (kind "class"); school.Console.Students needs a sign-in token.
const teacher = new Handwrite({ accountType: "classroom", token: cognitoAccessToken });hw = Handwrite(account_type="regular", token=os.environ["HANDWRITE_TOKEN"])
# Every /api/v1 resource for one person: hw.documents, hw.audio, hw.webhooks…team = Handwrite(account_type="team", token=os.environ["HANDWRITE_TOKEN"])
organisations = team.orgs.list()["organisations"] # let the person choose the team
# The console (team.console.*) needs a person's own sign-in token, not an API key.
admin = Handwrite(account_type="team", token=cognito_access_token)school = Handwrite(account_type="classroom", token=os.environ["HANDWRITE_TOKEN"])
# school.orgs lists classes (kind "class"); school.console.students needs a sign-in token.
teacher = Handwrite(account_type="classroom", token=cognito_access_token)hw, err := handwrites.NewRegular(os.Getenv("HANDWRITE_TOKEN"))
// Every /api/v1 service for one person: hw.Documents, hw.Audio, hw.Webhooks…team, err := handwrites.NewTeam(os.Getenv("HANDWRITE_TOKEN"))
if err != nil {
return err
}
orgs, err := team.Orgs.List(ctx) // let the person choose the team
if err != nil {
return err
}
// The console (team.Console.*) needs a person's own sign-in token, not an API key.
admin, err := handwrites.NewTeam(cognitoAccessToken)school, err := handwrites.NewClassroom(os.Getenv("HANDWRITE_TOKEN"))
if err != nil {
return err
}
// school.Orgs lists classes (kind "class"); school.Console.Students needs a sign-in token.
teacher, err := handwrites.NewClassroom(cognitoAccessToken)let hw = Client::<Regular>::new(token)?;
// Every /api/v1 resource for one person: hw.documents(), hw.audio(), hw.webhooks()…let team = Client::<Team>::new(token)?;
let orgs = team.orgs().list().await?.organisations; // let the person choose the team
// The console (team.console()) needs a person's own sign-in token, not an API key.
let admin = Client::<Team>::new(cognito_access_token.clone())?;let school = Client::<Classroom>::new(token)?;
// school.orgs() lists classes (kind "class"); school.console().students() needs a sign-in token.
let teacher = Client::<Classroom>::new(cognito_access_token)?;RegularClient hw = RegularClient.builder().token(System.getenv("HANDWRITE_TOKEN")).build();
// Every /api/v1 resource for one person: hw.documents(), hw.audio(), hw.webhooks()…TeamClient team = TeamClient.builder().token(System.getenv("HANDWRITE_TOKEN")).build();
List<Organisation> orgs = team.orgs().list(); // let the person choose the team
// The console (team.console()) needs a person's own sign-in token, not an API key.
TeamClient admin = TeamClient.builder().token(cognitoAccessToken).build();ClassroomClient school = ClassroomClient.builder().token(System.getenv("HANDWRITE_TOKEN")).build();
// school.orgs() lists classes (kind "class"); school.console().students() needs a sign-in token.
ClassroomClient teacher = ClassroomClient.builder().token(cognitoAccessToken).build();val hw = Handwrite.regular { token = System.getenv("HANDWRITE_TOKEN") }
// Every /api/v1 resource for one person: hw.documents, hw.audio, hw.webhooks…val team = Handwrite.team { token = System.getenv("HANDWRITE_TOKEN") }
val orgs = team.orgs.list() // let the person choose the team
// The console (team.console) needs a person's own sign-in token, not an API key.
val admin = Handwrite.team { token = cognitoAccessToken }val school = Handwrite.classroom { token = System.getenv("HANDWRITE_TOKEN") }
// school.orgs lists classes (kind "class"); school.console.students needs a sign-in token.
val teacher = Handwrite.classroom { token = cognitoAccessToken }using var hw = new RegularClient(token);
// Every /api/v1 resource for one person: hw.Documents, hw.Audio, hw.Webhooks…using var team = new TeamClient(token);
var orgs = await team.Orgs.ListAsync(); // let the person choose the team
// The console (team.Console) needs a person's own sign-in token, not an API key.
using var admin = new TeamClient(cognitoAccessToken);using var school = new ClassroomClient(token);
// school.Orgs lists classes (kind "class"); school.Console.Students needs a sign-in token.
using var teacher = new ClassroomClient(cognitoAccessToken);handwrites::RegularClient hw{token};
// Every /api/v1 resource for one person: hw.documents, hw.audio, hw.webhooks…handwrites::TeamClient team{token};
auto orgs = team.orgs.list(); // let the person choose the team
// The console (team.console) needs a person's own sign-in token, not an API key.
handwrites::TeamClient admin{cognito_access_token};handwrites::ClassroomClient school{token};
// school.orgs lists classes (kind "class"); school.console.students needs a sign-in token.
handwrites::ClassroomClient teacher{cognito_access_token};hw_client *hw = hw_client_new(HW_ACCOUNT_REGULAR, token, NULL, &err);
// Every /api/v1 function for one person: hw_documents_*, hw_audio_*, hw_webhooks_*...
// hw_orgs_* and hw_console_* fail at once with code "wrong_account_type".hw_client *team = hw_client_new(HW_ACCOUNT_TEAM, token, NULL, &err);
hw_json *orgs = hw_orgs_list(team, NULL, &err); // {organisations: [...]}: let the person choose the team
// The console (hw_console_*) needs a person's own sign-in token, not an API key.
hw_client *admin = hw_client_new(HW_ACCOUNT_TEAM, cognito_access_token, NULL, &err);hw_client *school = hw_client_new(HW_ACCOUNT_CLASSROOM, token, NULL, &err);
// hw_orgs_list lists classes (kind "class"); hw_console_students_* needs a sign-in token.
hw_client *teacher = hw_client_new(HW_ACCOUNT_CLASSROOM, cognito_access_token, NULL, &err);Handle errors
Every non-2xx answer is a typed error with the HTTP status, a stable code and the request ID to quote to support.
try {
await hw.Documents.update(id, { title: "Field notes, week 2" }, { ifMatch: revision });
} catch (error) {
if (error instanceof HandwriteError) {
if (error.code === "revision_mismatch") {
// Someone else saved first: fetch the document again and retry.
} else if (error.code === "insufficient_scope") {
// The token lacks documents:write.
}
console.error(`${error.status} ${error.code}: ${error.message} (request ${error.requestId})`);
}
throw error;
}
// 429 and 5xx answers are retried for you (maxRetries, default 2), honouring Retry-After.from handwrites import HandwriteError
try:
hw.documents.update(doc_id, title="Field notes, week 2", if_match=revision)
except HandwriteError as error:
if error.code == "revision_mismatch":
... # someone else saved first: fetch the document again and retry
elif error.code == "insufficient_scope":
... # the token lacks documents:write
print(f"{error.status} {error.code}: {error.message} (request {error.request_id})")
raise
# 429 and 5xx answers are retried for you (max_retries, default 2), honouring Retry-After._, err := hw.Documents.Update(ctx, id, handwrites.UpdateDocument{Title: handwrites.Ptr("Field notes, week 2")},
handwrites.WithIfMatch(revision))
var hwErr *handwrites.HandwriteError
if errors.As(err, &hwErr) {
switch hwErr.Code {
case "revision_mismatch":
// Someone else saved first: fetch the document again and retry.
case "insufficient_scope":
// The token lacks documents:write.
}
log.Printf("%d %s: %s (request %s)", hwErr.Status, hwErr.Code, hwErr.Message, hwErr.RequestID)
}
// 429 and 5xx answers are retried for you (WithMaxRetries, default 2), honouring Retry-After.match hw.documents().update(id, UpdateDocument::title("Field notes, week 2")).if_match(revision).await {
Ok(doc) => println!("Saved revision {}", doc.revision),
Err(e) if e.code() == "revision_mismatch" => {
// Someone else saved first: fetch the document again and retry.
}
Err(e) => {
eprintln!("{} {}: {} (request {:?})", e.status(), e.code(), e.message(), e.request_id());
return Err(e);
}
}
// 429 and 5xx answers are retried for you (max_retries, default 2), honouring Retry-After.try {
hw.documents().update(id, UpdateDocument.title("Field notes, week 2"), RequestOptions.ifMatch(revision));
} catch (HandwriteException e) {
switch (e.code()) {
case "revision_mismatch" -> { /* someone else saved first: fetch again and retry */ }
case "insufficient_scope" -> { /* the token lacks documents:write */ }
default -> { }
}
System.err.println(e.status() + " " + e.code() + ": " + e.getMessage() + " (request " + e.requestId() + ")");
throw e;
}
// 429 and 5xx answers are retried for you (maxRetries, default 2), honouring Retry-After.try {
hw.documents.update(id, ifMatch = revision) { title = "Field notes, week 2" }
} catch (e: PreconditionFailedException) {
// 412 revision_mismatch: someone else saved first. Fetch the document again and retry.
} catch (e: HandwriteException) {
// Sealed: AuthenticationException, PermissionDeniedException, NotFoundException, RateLimitException…
System.err.println("${e.status} ${e.code}: ${e.message} (request ${e.requestId})")
throw e
}
// 429 and 5xx answers are retried for you (maxRetries, default 2), honouring Retry-After.try
{
await hw.Documents.UpdateAsync(id, new UpdateDocument { Title = "Field notes, week 2" }, new RequestOptions { IfMatch = revision });
}
catch (HandwriteException e) when (e.Code == "revision_mismatch")
{
// Someone else saved first: fetch the document again and retry.
}
catch (HandwriteException e)
{
Console.Error.WriteLine($"{e.Status} {e.Code}: {e.Message} (request {e.RequestId})");
throw;
}
// 429 and 5xx answers are retried for you (MaxRetries, default 2), honouring Retry-After.handwrites::CallOptions guarded;
guarded.if_match = revision;
try {
hw.documents.update(id, {.title = "Field notes, week 2"}, guarded);
} catch (const handwrites::HandwriteError& e) {
if (e.code() == "revision_mismatch") {
// Someone else saved first: fetch the document again and retry.
} else {
std::cerr << e.status() << " " << e.code() << ": " << e.message() << " (request "
<< e.request_id().value_or("-") << ")\n";
throw;
}
}
// 429 and 5xx answers are retried for you (max_retries, default 2), honouring Retry-After.hw_call guarded = HW_CALL_INIT;
guarded.has_if_match = 1;
guarded.if_match = revision; // sent as If-Match: "<revision>"
hw_json *changes = hw_json_object();
hw_json_set_string(changes, "title", "Field notes, week 2");
hw_error *err = NULL;
hw_json *renamed = hw_documents_update(hw, id, changes, &guarded, &err);
if (!renamed) {
if (strcmp(err->code, "revision_mismatch") == 0) {
// Someone else saved first: fetch the document again and retry.
} else {
fprintf(stderr, "%d %s: %s (request %s)\n", err->status, err->code, err->message,
err->request_id ? err->request_id : "-");
}
hw_error_free(err);
}
hw_json_free(changes);
hw_json_free(renamed);
// 429 and 5xx answers are retried for you (max_retries, default 2), honouring Retry-After.Paginate
Lists are cursor-paginated. Pass the cursor back yourself, or let the iterator follow it.
// One page at a time: pass the cursor back until it is null.
let cursor: string | undefined;
do {
const page = await hw.Documents.list({ limit: 100, ...(cursor && { cursor }) });
for (const doc of page.documents) console.log(doc.id);
cursor = page.cursor ?? undefined;
} while (cursor);
// Or let the SDK follow the cursor for you.
for await (const doc of hw.Documents.iterate()) {
if (!("deleted" in doc)) console.log(doc.title);
}# One page at a time: pass the cursor back until it is None.
cursor: str | None = None
while True:
page = hw.documents.list(limit=100, cursor=cursor)
for doc in page["documents"]:
print(doc["id"])
cursor = page["cursor"]
if not cursor:
break
# Or let the SDK follow the cursor for you.
for doc in hw.documents.iterate():
if "deleted" not in doc:
print(doc["title"])// One page at a time: pass the cursor back until it is nil.
params := handwrites.ListDocumentsParams{Limit: 100}
for {
page, err := hw.Documents.List(ctx, params)
if err != nil {
return err
}
for _, doc := range page.Documents {
fmt.Println(doc.ID)
}
if page.Cursor == nil {
break
}
params.Cursor = *page.Cursor
}
// Or let the SDK follow the cursor for you (Pager works on Go 1.22; Iterate needs 1.23).
p := hw.Documents.Pager(ctx, handwrites.ListDocumentsParams{})
for p.Next() {
fmt.Println(p.Item().Title)
}
if err := p.Err(); err != nil {
return err
}// One page at a time: pass the cursor back until it is None.
let mut params = ListDocuments { limit: Some(100), ..Default::default() };
loop {
let page = hw.documents().list(params.clone()).await?;
for entry in &page.documents {
println!("{}", entry.id());
}
match page.cursor {
Some(cursor) => params.cursor = Some(cursor),
None => break,
}
}
// Or let the SDK follow the cursor for you (use futures::TryStreamExt).
let mut all = hw.documents().iterate(ListDocuments::default());
while let Some(entry) = all.try_next().await? {
if let Some(doc) = entry.document() {
println!("{}", doc.title);
}
}// One page at a time: pass the cursor back until it is null.
String cursor = null;
do {
ListDocumentsParams params = ListDocumentsParams.none().withLimit(100);
DocumentPage page = hw.documents().list(cursor == null ? params : params.withCursor(cursor));
for (DocumentItem item : page.documents()) System.out.println(item.id());
cursor = page.cursor();
} while (cursor != null);
// Or let the SDK follow the cursor for you (a lazy Iterable; stream() is a Stream).
for (DocumentItem item : hw.documents().iterate()) {
if (item instanceof Document doc) System.out.println(doc.title());
}// One page at a time: pass the cursor back until it is null.
var cursor: String? = null
do {
val page = hw.documents.list(limit = 100, cursor = cursor)
page.documents.forEach { entry -> println(entry.id) }
cursor = page.cursor
} while (cursor != null)
// Or let the SDK follow the cursor for you, as a cold Flow.
hw.documents.iterate().collect { entry ->
when (entry) {
is Document -> println(entry.title)
is Tombstone -> Unit
}
}// One page at a time: pass the cursor back until it is null.
string? cursor = null;
do
{
var page = await hw.Documents.ListAsync(new ListDocumentsParams { Limit = 100, Cursor = cursor });
foreach (var d in page.Documents) Console.WriteLine(d.Id);
cursor = page.Cursor;
} while (cursor != null);
// Or let the SDK follow the cursor for you (IAsyncEnumerable).
await foreach (var d in hw.Documents.IterateAsync())
{
if (!d.Deleted) Console.WriteLine(d.Title);
}// One page at a time: pass the cursor back until it is empty.
handwrites::ListDocumentsParams params{.limit = 100};
while (true) {
auto page = hw.documents.list(params);
for (const auto& doc : page.documents) std::cout << doc.id << "\n";
if (!page.cursor || page.cursor->empty()) break;
params.cursor = page.cursor;
}
// Or let the SDK follow the cursor for you (a lazy input range).
for (const auto& doc : hw.documents.iterate()) {
if (!doc.deleted) std::cout << doc.title << "\n";
}// One page at a time: pass the cursor back until it is null.
hw_error *err = NULL;
hw_json *page = NULL;
const char *cursor = NULL; // borrowed from the previous page
do {
hw_list_documents_params params = {.limit = 100, .cursor = cursor};
hw_json *next = hw_documents_list(hw, ¶ms, NULL, &err);
hw_json_free(page); // the SDK copied the cursor before returning
page = next;
if (!page) break;
const hw_json *documents = hw_json_get(page, "documents");
for (size_t i = 0; i < hw_json_size(documents); i++) {
puts(hw_json_get_string(hw_json_at(documents, i), "id"));
}
cursor = hw_json_get_string(page, "cursor");
} while (cursor);
hw_json_free(page);
hw_error_free(err);
// Or let the SDK follow the cursor for you.
hw_documents_iter *it = hw_documents_iterate(hw, NULL, NULL);
const hw_json *doc;
while ((doc = hw_documents_iter_next(it, &err))) {
if (!hw_json_get_bool(doc, "deleted", 0)) puts(hw_json_get_string(doc, "title"));
}
hw_documents_iter_free(it);
hw_error_free(err); // set if a page failedVerify webhooks
Check the Handwrite-Signature header against the raw body before you parse it. Signatures older than five minutes are rejected.
const secret = process.env.HANDWRITE_WEBHOOK_SECRET!; // whsec_…, shown once when the webhook is created
createServer((req, res) => {
const chunks: Buffer[] = [];
req.on("data", (chunk: Buffer) => chunks.push(chunk));
req.on("end", () => {
const rawBody = Buffer.concat(chunks).toString("utf8"); // verify before JSON.parse
const signature = req.headers["handwrite-signature"];
if (!verifyWebhook(secret, typeof signature === "string" ? signature : null, rawBody)) {
res.writeHead(400).end();
return;
}
const event = JSON.parse(rawBody) as WebhookEvent;
console.log(event.type, event.data.document.id);
res.writeHead(204).end();
});
}).listen(8080);from handwrites import verify_webhook
secret = os.environ["HANDWRITE_WEBHOOK_SECRET"] # whsec_…, shown once when the webhook is created
# In your handler: pass the raw body (bytes or str) and the Handwrite-Signature header.
if not verify_webhook(secret, signature, raw_body):
return 400
event = json.loads(raw_body)
print(event["type"], event["data"]["document"]["id"])
return 204secret := os.Getenv("HANDWRITE_WEBHOOK_SECRET") // whsec_…, shown once when the webhook is created
http.HandleFunc("/webhooks/handwrite", func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body) // verify the raw body, before decoding it
if err != nil || !handwrites.VerifyWebhook(secret, r.Header.Get("Handwrite-Signature"), body) {
http.Error(w, "bad signature", http.StatusBadRequest)
return
}
var event handwrites.WebhookEvent
if err := json.Unmarshal(body, &event); err != nil {
http.Error(w, "bad body", http.StatusBadRequest)
return
}
log.Println(event.Type, event.Data.Document.ID)
w.WriteHeader(http.StatusNoContent)
})
log.Fatal(http.ListenAndServe(":8080", nil))let secret = std::env::var("HANDWRITE_WEBHOOK_SECRET").expect("whsec_…"); // shown once when the webhook is created
// In your handler: pass the raw body bytes and the Handwrite-Signature header, before parsing.
if !handwrites::verify_webhook(&secret, signature, raw_body) {
return 400;
}
let event: handwrites::WebhookEvent = match serde_json::from_slice(raw_body) {
Ok(event) => event,
Err(_) => return 400,
};
println!("{} {}", event.event_type, event.data.document.id);
204String secret = System.getenv("HANDWRITE_WEBHOOK_SECRET"); // whsec_…, shown once when the webhook is created
// In your handler: pass the raw body and the Handwrite-Signature header, before parsing.
if (!WebhookSignature.verify(secret, signature, rawBody)) {
return 400;
}
WebhookEvent event = Handwrite.objectMapper().readValue(rawBody, WebhookEvent.class);
System.out.println(event.type() + " " + event.data().document().id());
return 204;val secret = System.getenv("HANDWRITE_WEBHOOK_SECRET") // whsec_…, shown once when the webhook is created
// In your handler: pass the raw body and the Handwrite-Signature header, before parsing.
if (!verifyWebhook(secret, signature, rawBody)) return 400
val event = Json.parseToJsonElement(rawBody).jsonObject
println(event["type"]?.jsonPrimitive?.content)
return 204var secret = Environment.GetEnvironmentVariable("HANDWRITE_WEBHOOK_SECRET")!; // whsec_…, shown once at creation
// In your handler: pass the raw body and the Handwrite-Signature header, before parsing.
if (!WebhookSignature.Verify(secret, signature, rawBody)) return 400;
using var evt = JsonDocument.Parse(rawBody);
Console.WriteLine(evt.RootElement.GetProperty("type").GetString());
return 204;const char* secret = std::getenv("HANDWRITE_WEBHOOK_SECRET"); // whsec_…, shown once at creation
// In your handler: pass the raw body and the Handwrite-Signature header, before parsing.
if (!secret || !handwrites::verify_webhook(secret, signature, raw_body)) return 400;
auto event = nlohmann::json::parse(raw_body);
std::cout << event["type"].get<std::string>() << "\n";
return 204;const char *secret = getenv("HANDWRITE_WEBHOOK_SECRET"); // whsec_..., shown once at creation
// In your handler: pass the raw body and the Handwrite-Signature header, before parsing.
if (!secret || !hw_verify_webhook(secret, signature, raw_body, raw_body_length, 300, 0)) return 400;
hw_json *event = hw_json_parse(raw_body, NULL);
printf("%s\n", hw_json_get_string(event, "type"));
hw_json_free(event);
return 204;versions
API v1 and its SDKs
Every SDK targets API v1. This table is generated from each SDK repository’s version files when the docs are built.
| API | SDK | Package | Version | Minimum runtime | Changes |
|---|---|---|---|---|---|
| v1 | Node | handwrites.io | 0.1.0 · pre-release | Node 20 | Releases |
| v1 | Python | handwrites.io | 0.1.0 · pre-release | Python 3.10 | Changelog |
| v1 | Go | gitlab.com/handwrite/sdk/go | 0.1.0 · pre-release | Go 1.22 | Changelog |
| v1 | Rust | handwrites | 0.1.0 · pre-release | Rust 1.83 | Changelog |
| v1 | Java | io.handwrites:handwrites | 0.1.0 · pre-release | Java 17 | Changelog |
| v1 | Kotlin | io.handwrites:handwrites-kotlin | 0.1.0 · pre-release | Kotlin/JVM 17 | Changelog |
| v1 | C# | Handwrites.IO | 0.1.0 · pre-release | .NET 8 · .NET Standard 2.0 | Changelog |
| v1 | C++ | handwrites | 0.1.0 · pre-release | C++20 · CMake 3.21 | Changelog |
| v1 | C | libhandwrites | 0.1.0 · pre-release | C11 · CMake 3.21 | Changelog |
Prefer raw HTTP?
Each edition documents every endpoint with request and response examples: Regular accounts, Teams and Classrooms. You can also try the API in the playground.