Skip to main content

Manage Records

Attach distribution intent and display metadata to a signature you registered, keep it up to date, and read the guidance SASHA returns when the media is looked up. This guide assumes you already have a signature_id from an embed job and an access token.

For what a Record is and what each field means, see Records. Every operation below has a reference page: Put, Get, Patch, Delete.

The gRPC examples use a signatureClient built from the PartnerAPI service definition and a metadata carrying your bearer token; see Authentication for obtaining the token. ShareState is the generated enum from sasha/common/public/share_state.proto.

Attach a Record

Attach a Record with PUT. It is a full-replace upsert: the stored Record becomes exactly what you send. You can do this any time after the signature is registered — it does not have to be at embed time.

curl -X PUT https://partner.api.sasha.eu/signature/12345678901234567890/record \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"share_state": "restricted",
"allowed_publish_urls": ["https://news.example.com/articles/"],
"allowed_from": "2026-07-01T06:00:00Z",
"byline": "Photo by Jane Doe",
"purchase_url": "https://license.example.com/photos/123"
}'

A 204 No Content with an ETag header confirms the write. Keep the ETag — it is the signature's current revision, and you pass it back to make a later write conditional (see Concurrent updates).

The Record must satisfy the consistency rules: share_state is required, and private / unrestricted Records must not carry distribution fields. A Record that breaks them is rejected with 400 Bad Request (INVALID_ARGUMENT on gRPC) and nothing is stored. The response names every offending field — see Errors.

Unknown fields are rejected rather than ignored. PUT replaces the whole Record, so a misspelled field name would otherwise be dropped silently and clear the value it was meant to set. A camelCase spelling is the usual cause: the JSON body uses the snake_case field names, while a generated gRPC client shows the same fields as purchaseUrl.

Attach a Record at embed time

An embed request can carry the Record with it, so the media is never registered without its intent. This closes the window in which the signature exists but has no Record and is therefore treated as private.

curl -X POST https://partner.api.sasha.eu/signature/embed \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"media_url": "https://example.com/image.jpg",
"media_mime_type": "image/jpeg",
"record": {
"share_state": "restricted",
"allowed_publish_urls": ["https://news.example.com/articles/"],
"byline": "Photo by Jane Doe"
}
}'

The Record is validated when the request is submitted, not when the job runs. An invalid one fails the embed request itself with 400 naming the offending fields, and no job is created — so you never end up with a protected image whose Record silently failed to attach. Violation paths are prefixed record., for example record.purchase_url.

Read a Record

Only the partner that registered a signature can read its Record back in full. Use GetSignatureRecord.

curl https://partner.api.sasha.eu/signature/12345678901234567890/record \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" -i

The response is the stored Record, and the ETag header carries the current revision:

ETag: "5"
{
"share_state": "restricted",
"allowed_publish_urls": ["https://news.example.com/articles/"],
"allowed_from": "2026-07-01T06:00:00Z",
"byline": "Photo by Jane Doe",
"purchase_url": "https://license.example.com/photos/123",
"created_at": "2026-06-15T10:00:00Z",
"updated_at": "2026-06-20T08:30:00Z"
}

A signature with no Record attached returns 404 — the Record sub-resource does not exist. That is the same status an unknown signature returns, so a 404 does not tell you which of the two it was.

created_at and updated_at are set by SASHA and cannot be written. They are accepted and ignored on a write, so you can send a Record straight back the way you read it. Any other unrecognised field is rejected — see Errors.

note

GetSignatureRecord is the only reliable way to read your own Record. Guidance and disclosed fields returned on a lookup are transient; your durable copy lives here.

Update part of a Record

To change a few fields without resending the whole Record, use PATCH (REST) or a field-scoped update mask (gRPC).

PATCH uses JSON Merge Patch: fields you send replace the stored value, fields set to null are cleared, and absent fields are left unchanged — except share_state, which is required and cannot be cleared.

curl -X PATCH https://partner.api.sasha.eu/signature/12345678901234567890/record \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/merge-patch+json" \
-d '{ "byline": "Photo by Jane Doe / Example Agency" }'

Arrays and objects are replaced wholesale, not merged — sending allowed_publish_urls replaces the entire list. The patched result must still satisfy the consistency rules, or the write is rejected and nothing changes.

Delete a Record

Deleting a Record removes the attached metadata only. The signature stays valid and reverts to the default "no Record means private" interpretation. Deleting is idempotent — deleting when nothing is attached still succeeds.

curl -X DELETE https://partner.api.sasha.eu/signature/12345678901234567890/record \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"

A 204 with an ETag confirms the delete; that ETag is the signature's new revision. If-Match works here too, so a delete can be made conditional the same way a write can.

Concurrent updates

Every read and write hands you the signature's current revision — the ETag header on REST, the revision field on gRPC. Pass it back on your next write to make the write conditional, so two clients editing the same Record can't silently clobber each other.

Every accepted write bumps the revision, including one that changes nothing. The revision tracks writes, not content, so it is a safe concurrency token but not a change detector.

Send the revision you last saw in an If-Match header, exactly as the ETag gave it to you. If the Record has moved on since, the write is rejected with 412 Precondition Failed and nothing changes — re-read and retry:

async function patchByline(signatureId, byline) {
for (let attempt = 0; attempt < 3; attempt++) {
const read = await fetch(`${BASE}/signature/${signatureId}/record`, {
headers: { Authorization: `Bearer ${token}` },
});
const etag = read.headers.get("ETag");

const write = await fetch(`${BASE}/signature/${signatureId}/record`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/merge-patch+json",
"If-Match": etag,
},
body: JSON.stringify({ byline }),
});

if (write.status !== 412) return write; // applied (or a real error)
// 412: someone else wrote first — loop to re-read and retry.
}
throw new Error("record update kept losing the race");
}

Bound the loop as above rather than retrying until it succeeds. A 412 is not always a lost race: a revoked signature can no longer be modified, and when the write carried an If-Match that rejection also surfaces as 412, so an unbounded loop on a revoked signature would never terminate.

Two details on the header itself:

  • Send the ETag value verbatim, quotes included. A weak validator (W/"5") or any non-numeric value is rejected with 400.
  • If-Match: * asserts only that the signature exists, not which revision it is at, so it never produces a 412. Use it to require existence; use a concrete revision to require that revision.

Omit If-Match entirely for last-write-wins.

Read guidance on a lookup

When you look up an image, supply where you found it — publish_url and source_url. SASHA evaluates the found signature's Record against that context and returns guidance on the completed job.

curl https://partner.api.sasha.eu/signature/lookup \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"media_url": "https://example.com/image.jpg",
"media_mime_type": "image/jpeg",
"publish_url": "https://news.example.com/articles/123",
"source_url": "https://cdn.example.com/images/abc.jpg"
}'

The completed job — read with GetJob or delivered by a callback — carries guidance:

{
"job_id": "job_12345",
"type": "lookup-signature",
"status": "completed",
"created_at": "2026-07-01T09:00:00Z",
"updated_at": "2026-07-01T09:00:12Z",
"signature_id": "12345678901234567890",
"creator_id": 1234567890,
"guidance": {
"share_state": "restricted",
"advice": "not_allowed"
}
}

Branch your logic on advice and share_state. A restricted Record that carries URL patterns can only return inconclusive when you supply neither URL — but a condition that can be evaluated still decides: an embargo that has not opened yet returns not_allowed with no URL context at all.

If the looked-up signature is one you own (or one whose owner granted you fields, or that has public fields), the completed job may also carry a field-filtered record. That disclosure is transient — for a durable read of your own Record, use Read a Record.

Errors

RESTgRPCMeaningWhat to do
400INVALID_ARGUMENTThe Record breaks a consistency rule, carries a field the endpoint does not define, or the mask names an unknown field.Fix the named fields. Do not retry — the same request fails the same way.
403PERMISSION_DENIEDThe access token lacks the scope this operation requires.Do not retry with the same token.
404NOT_FOUNDNo such signature, or one your partner account did not register. The two are deliberately indistinguishable, so that the API does not confirm the existence of another creator's signature.Do not retry.
409FAILED_PRECONDITIONThe signature is revoked; its Record can no longer be modified.Do not retry.
412FAILED_PRECONDITIONThe If-Match / expected_revision precondition did not match.Re-read and retry, bounded — see Concurrent updates.

A 400 names every offending field, so one request tells you everything that is wrong rather than one problem at a time:

{
"message": "purchase_url must use the https: protocol (+1 more)",
"request_id": "8f14e45f-ceea-467a-9f0b-2b3a2c5a7d61",
"details": [
{ "field": "purchase_url", "reason": "UNSUPPORTED_SCHEME",
"description": "purchase_url must use the https: protocol" },
{ "field": "allowed_publish_urls[1]", "reason": "INVALID_PATTERN",
"description": "allowed_publish_urls[1]: wildcard is only allowed as the leftmost label" }
]
}

Branch on reason, never on message text, and treat a reason you do not recognise as a generic validation failure. Error Handling has the full catalogue; Handle API Errors covers routing each violation to the right place.

Revoked signatures

When a write against a revoked signature carries an If-Match, it currently reports 412 rather than 409. Both mean the write was rejected and nothing changed, but a retry loop keyed on 412 will not make progress — which is why the loop above is bounded.

C2PA manifest store

A Record's c2pa_manifest_store cannot be set through the Partner API — that field is left unset here and is managed via the SDK API. Everything else on the Record is fully editable through this surface.

See also

REST reference: Get · Put · Patch · Delete