Skip to content

Writing records

Five ways to change a record:

POST …/records/Create one. Returns 201 and a Location header
PATCH …/records/{id}/Update named fields on one record
PUT …/records/?matching_property=XUpsert, matched on X. 201 on create, 200 on update
DELETE …/records/{id}/Delete one
PUT …/records/{id}/associations/…Link two records

For many records at once, see batch operations.

This distinction catches people out:

{ "properties": { "ahoy_email": null } }

An explicit null clears the field. A field you simply leave out is left unchanged. So a partial update is safe — you never have to send the whole record back to avoid wiping it — but you must be deliberate about null.

A write whose values already match what’s stored is a success, not an error.

Ahoy’s underlying pipeline rejects no-change events, but this API treats that as an idempotent write and returns the current record. In practice:

  • A retried PATCH after a network timeout won’t fail.
  • A replayed PUT upsert returns the same record.
  • A batch containing the same record twice doesn’t error on the second.

That makes at-least-once delivery from your side safe to build on — you don’t need distributed-transaction machinery to avoid double-writes.

matching_property names the property used to find an existing record. It must be readable, and present and non-null in the body — otherwise you get request_invalid with reason: "invalid_matching_property".

If the value matches two or more records, there is no single record to update, and the API returns conflict with reason: "ambiguous_match" rather than guessing.

Terminal window
curl -X PUT 'https://api.ahoy.ai/rest/v1/objects/ahoy_contact/records/?matching_property=ahoy_email' \
-H "Authorization: Bearer $AHOY_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"properties": {"ahoy_email": ["ada@example.com"], "ahoy_first_name": "Ada", "ahoy_last_name": "Lovelace"}}'

An external id from your own system is usually a better matching property than an email address, which people share and change.

Associations are validated before the write

Section titled “Associations are validated before the write”

Linking to a target that is unknown, deleted, or invisible to your credential returns one byte-identical 422 with field: /target_id — the same answer in all three cases, so a missing record can’t be distinguished from one you simply can’t see.

Permission is checked before the target is looked up, so a credential without write access learns nothing about whether the target exists.

Two separate gates apply, in this order:

  1. The object type’s own rules. Some types are read-only or link-only for every credential — synced data like meetings and email threads, and Ahoy’s own generated signals. Failing this returns permission_denied with reason: "type_read_only". Because it’s checked first, the answer is identical for every credential.
  2. Your credential’s grants. Failing this returns the same status with reason: "write_forbidden".

Read writable_operations from GET /rest/v1/objects/ rather than hard-coding either. See Objects and properties.

A body that doesn’t satisfy the object type’s schema returns 422 with reason: "schema_invalid" and a field pointer to the offending property. When more than one field is wrong, an errors array lists every {field, detail, reason} — so you can fix them all in one pass rather than discovering them one request at a time.