Suppliers
A supplier is a link between two Synex teams: yours as the customer, and the company that ships you cells, casings or test results as the supplier. A link is opened by an emailed invitation, and it is the channel data requests travel over: you ask a linked supplier for properties of one of your passports, they answer, and the answers are written onto your passport with your authority.
The same row is "my supplier" to one team and "my customer" to the other, and every rendering says which reading it is with role. There is no separate supplier portal and no separate supplier identity: supplier people are ordinary Synex users working in their own team, and the same API key model applies to both sides.
Neither suppliers:read nor suppliers:write is in a key's default ability set — grant them explicitly when minting the key. Neither can be restricted to particular ontologies: a link belongs to the team, not to an ontology. See abilities.
| Endpoint | Ability | Side |
|---|---|---|
GET /v1/suppliers | suppliers:read | Either — role=customer (default) or role=supplier |
POST /v1/suppliers | suppliers:write | Customer |
GET /v1/suppliers/:id | suppliers:read | Either |
PATCH /v1/suppliers/:id | suppliers:write | Customer |
DELETE /v1/suppliers/:id | suppliers:write | Customer |
POST /v1/suppliers/:id/resend_invite | suppliers:write | Customer |
The supplier model
- Name
object- Type
- string
- Description
Always
supplier.
- Name
id- Type
- string
- Description
Unique identifier, prefixed
sup_. The same on both sides of the link.
- Name
name- Type
- string
- Description
The customer's label for the supplier — trimmed, at most 120 characters, editable with
PATCH. Always the customer's label, whichever side reads it.
- Name
status- Type
- string
- Description
pending,linkedorrevoked. See the link.
- Name
role- Type
- string
- Description
The side this rendering is read from:
customerorsupplier. Every customer-side write answers withcustomer.
- Name
customer_team- Type
- object
- Description
The team that opened the link, as
{ id, name }.idis the integer idGET /v1/teamreports;nameis null only if the team has since been deleted.
- Name
supplier_team- Type
- object
- Description
The team that accepted the invitation, in the same shape. Null while the link is
pending.
- Name
invited_email- Type
- string
- Description
The address the invitation was sent to, lowercased.
- Name
invite_status- Type
- string
- Description
The invitation's own state:
pendingwhile it may still be accepted,expiredonce its window has passed — resend to issue a fresh one — andacceptedonce it was. Null when the link carries no invitation, as on a link revoked before it was accepted.
- Name
linked_at- Type
- string
- Description
When the invitation was accepted. Null until then.
- Name
revoked_at- Type
- string
- Description
When the customer revoked the link. Null otherwise.
- Name
created_at- Type
- string
- Description
ISO 8601 timestamp of the invitation.
- Name
request_counts- Type
- object
- Description
How many data requests over the link sit in each non-cancelled status, as
{ open, submitted, changes_requested, applied }. A list of suppliers is therefore also a dashboard: "two awaiting my review, one overdue with Helios" can be drawn from the list alone, without walking the requests.
The link
A link has three states and moves through them in one direction:
pending ──(recipient accepts the invitation)──▶ linked
│ │
└──────────────(customer revokes)──────────────┴──▶ revoked
| Status | Meaning |
|---|---|
pending | Invited, not yet accepted. supplier_team is null; invite_status says whether the invitation is still pending or has expired. Requests can already be opened over it — they wait until it is accepted. |
linked | The recipient accepted. supplier_team and linked_at are set, and requests over the link reach the supplier's inbox. |
revoked | The customer ended it. revoked_at is set, every request that was in flight is cancelled, and the row stays readable. There is no undo. |
One address holds at most one pending or linked link per customer team, and one supplier team is linked to a given customer at most once. A team is never on both sides of the same link.
Inviting is the only way in
The only way a supplier comes to exist on your side is an email invitation. There is no "link by id": you cannot attach another team to yours by knowing its identifier, and nobody can attach themselves to you. The invitation is the consent step.
The recipient follows the link in the email and signs in to Synex — or registers, if they have no account. An existing user then picks which of their teams will act as the supplier, or creates a new one; a new registrant gets a fresh team named after your name for them. The moment they accept, the link flips to linked, supplier_team fills in, every request you opened over the pending link lands in their inbox, supplier.linked fires on both teams' feeds, and the person who sent the invitation is emailed.
Acceptance happens in the Synex application, not over the API. A key holding suppliers:write on the supplier's side cannot accept an invitation, because an invitation is addressed to a person, not to a team.
A team created at acceptance has no company: it answers data requests but issues no passports until Synex assigns it one. Its keys are refused with 403 company_required on the endpoints that issue passports — POST /v1/ontologies, POST /v1/ontologies/:ontology/passports and POST /v1/imports — and keep everything else they were granted.
Both sides read the same link
A link is looked up on both sides at once. GET /v1/suppliers/:id answers the customer with role: customer and the supplier team with role: supplier — same id, same status, same timestamps, same request_counts.
The list takes a side explicitly: role=customer (the default) lists the teams you buy from, and role=supplier lists the teams that buy from you — the links on which your key's team is the supplier. A pending link has no supplier team yet, so it reaches the supplier's list only once accepted.
A key sees only the links its own team is party to. A team on neither side gets 404 resource_missing, exactly as for an id that does not exist, so no endpoint here can be used to probe another tenant's relationships.
Every write is the customer's. Rename, revoke and resend are refused with 404 resource_missing from the supplier side — the same answer another tenant gets. The supplier can see the relationship and act on the requests that travel over it, but cannot change the relationship itself.
List suppliers
The links your team is party to on one side, newest first. Requires suppliers:read. Returns a paginated list.
role and status both narrow the page and both join the cursor's scope: a cursor minted under one pair is rejected with 400 invalid_cursor under another, so request the first page again after changing either. An unknown role or status is 400 parameter_invalid rather than an empty page.
To see the requests over one link rather than their counts, filter the request list by it: GET /v1/data_requests?supplier=sup_….
Optional attributes
- Name
role- Type
- string
- Description
customer(default) — the teams you buy from;supplier— the teams that buy from you. Every row carries theroleyou asked for.
- Name
status- Type
- string
- Description
Only links in this status:
pending,linkedorrevoked.
- Name
limit- Type
- integer
- Description
Page size, 1–100 (default 10).
- Name
cursor- Type
- string
- Description
A pagination cursor from a previous response's
next_cursor.
Request
curl -G https://api.synexcloud.com/v1/suppliers \
-H "Authorization: Bearer $SYNEX_API_KEY" \
-d "role=customer"
Response
{
"object": "list",
"data": [
{
"object": "supplier",
"id": "sup_7c2e9a4f1b8d3506",
"name": "Nordvolt Cells",
"status": "linked",
"role": "customer",
"customer_team": { "id": 4821, "name": "Kestrel Energy" },
"supplier_team": { "id": 5107, "name": "Nordvolt Cells AB" },
"invited_email": "quality@nordvolt.example",
"invite_status": "accepted",
"linked_at": "2026-09-02T09:41:17Z",
"revoked_at": null,
"created_at": "2026-09-01T14:10:43Z",
"request_counts": {
"open": 2,
"submitted": 1,
"changes_requested": 0,
"applied": 7
}
},
{
"object": "supplier",
"id": "sup_3f8b1d6e0a2c7495",
"name": "Helios Casings",
"status": "pending",
"role": "customer",
"customer_team": { "id": 4821, "name": "Kestrel Energy" },
"supplier_team": null,
"invited_email": "procurement@helios-casings.example",
"invite_status": "pending",
"linked_at": null,
"revoked_at": null,
"created_at": "2026-09-18T08:02:55Z",
"request_counts": {
"open": 1,
"submitted": 0,
"changes_requested": 0,
"applied": 0
}
}
],
"has_more": false,
"next_cursor": null,
"url": "/v1/suppliers"
}
Invite a supplier
Opens a pending link and emails the invitation to email. Requires suppliers:write. Refused with 403 team_suspended while your team is suspended at the create tier or above.
This call sends the email, synchronously, inside the create's transaction. A 201 means the mail transport accepted the message; a transport failure is a 5xx with no link opened, so a link never outlives a failed invitation. Accepted is not delivered, though — whether it reaches an inbox is the recipient's mail system's business.
The response never carries the invitation token. It is the bearer credential for becoming this supplier, it travels in the email alone, and nothing you can read over the API hands it out.
Both fields are validated before anything is written, so a malformed address cannot leave an orphan link behind. An address that already holds a pending or linked link with your team is 409 supplier_already_linked; a revoked link releases its address.
Emits supplier.invited on your team's feed.
Required attributes
- Name
email- Type
- string
- Description
Where to send the invitation, at most 255 characters. Lowercased before it is stored.
- Name
name- Type
- string
- Description
Your label for the supplier — what your team calls them. Trimmed; 1–120 characters.
Request
curl https://api.synexcloud.com/v1/suppliers \
-H "Authorization: Bearer $SYNEX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "procurement@helios-casings.example",
"name": "Helios Casings"
}'
Response
{
"object": "supplier",
"id": "sup_3f8b1d6e0a2c7495",
"name": "Helios Casings",
"status": "pending",
"role": "customer",
"customer_team": { "id": 4821, "name": "Kestrel Energy" },
"supplier_team": null,
"invited_email": "procurement@helios-casings.example",
"invite_status": "pending",
"linked_at": null,
"revoked_at": null,
"created_at": "2026-09-18T08:02:55Z",
"request_counts": {
"open": 0,
"submitted": 0,
"changes_requested": 0,
"applied": 0
}
}
Retrieve a supplier
One link, from whichever side your key's team is on. Requires suppliers:read.
Unlike the list, this read takes no role: the link is looked up on both sides at once and role in the response says which one you are. The supplier team reading the same link gets the same object with role: supplier.
A team on neither side gets 404 resource_missing.
Request
curl https://api.synexcloud.com/v1/suppliers/sup_7c2e9a4f1b8d3506 \
-H "Authorization: Bearer $SYNEX_API_KEY"
Response
{
"object": "supplier",
"id": "sup_7c2e9a4f1b8d3506",
"name": "Nordvolt Cells",
"status": "linked",
"role": "customer",
"customer_team": { "id": 4821, "name": "Kestrel Energy" },
"supplier_team": { "id": 5107, "name": "Nordvolt Cells AB" },
"invited_email": "quality@nordvolt.example",
"invite_status": "accepted",
"linked_at": "2026-09-02T09:41:17Z",
"revoked_at": null,
"created_at": "2026-09-01T14:10:43Z",
"request_counts": {
"open": 2,
"submitted": 1,
"changes_requested": 0,
"applied": 7
}
}
Rename a supplier
Changes your label for the supplier. Requires suppliers:write; the customer's call alone.
name is the only writable field on a link. A pending or linked link may be renamed; a revoked one is 409 link_revoked, since there is nothing left to label. Nothing else moves: no email, no event, no change to the invitation.
The name is also the folder supplied files are copied into on your Drive — see file and image values — so renaming a supplier changes where their future files land, not where the earlier ones are.
Required attributes
- Name
name- Type
- string
- Description
The new label. Trimmed; 1–120 characters.
Request
curl -X PATCH https://api.synexcloud.com/v1/suppliers/sup_7c2e9a4f1b8d3506 \
-H "Authorization: Bearer $SYNEX_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Nordvolt (prismatic cells)" }'
Response
{
"object": "supplier",
"id": "sup_7c2e9a4f1b8d3506",
"name": "Nordvolt (prismatic cells)",
"status": "linked",
"role": "customer",
"customer_team": { "id": 4821, "name": "Kestrel Energy" },
"supplier_team": { "id": 5107, "name": "Nordvolt Cells AB" },
"invited_email": "quality@nordvolt.example",
"invite_status": "accepted",
"linked_at": "2026-09-02T09:41:17Z",
"revoked_at": null,
"created_at": "2026-09-01T14:10:43Z",
"request_counts": {
"open": 2,
"submitted": 1,
"changes_requested": 0,
"applied": 7
}
}
Resend an invitation
Issues a fresh invitation for a pending link and emails it again. Requires suppliers:write; the customer's call alone.
Use it when the first email was lost, went to someone who has since left, or when invite_status reads expired. The previous token stops working the moment the new one is issued — there is never more than one live invitation per link — and the expiry window restarts. The link itself does not change: same id, same name, same created_at, still pending.
Only a pending link has an invitation to resend. A linked one is 409 supplier_already_linked and a revoked one is 409 link_revoked; nothing is mailed on either.
A resend emits no event — it is the same invitation, not a new relationship. The one exception is a pending link that never carried an invitation, which is what a data request opened with a recipient leaves behind when the mail transport fails after the request has committed: the resend issues the link's first invitation, and that emits supplier.invited as the create would have. That first issue re-checks the stored address, and one that no longer passes validation is 400 parameter_invalid rather than a mail to nowhere.
Request
curl -X POST https://api.synexcloud.com/v1/suppliers/sup_3f8b1d6e0a2c7495/resend_invite \
-H "Authorization: Bearer $SYNEX_API_KEY"
Response
{
"object": "supplier",
"id": "sup_3f8b1d6e0a2c7495",
"name": "Helios Casings",
"status": "pending",
"role": "customer",
"customer_team": { "id": 4821, "name": "Kestrel Energy" },
"supplier_team": null,
"invited_email": "procurement@helios-casings.example",
"invite_status": "pending",
"linked_at": null,
"revoked_at": null,
"created_at": "2026-09-18T08:02:55Z",
"request_counts": {
"open": 1,
"submitted": 0,
"changes_requested": 0,
"applied": 0
}
}
Revoke a supplier
Ends the relationship. Requires suppliers:write; the customer's call alone. Three things happen in one transaction:
- the link goes
revokedwithrevoked_atstamped; - an invitation that was never accepted is withdrawn, so the emailed token stops working;
- every data request still in flight over the link is cancelled, each with
closed_reason: link_revoked. Requests alreadyappliedorcancelledare left as they are — what was written to your passports stays written.
The row itself is kept, so the response is the revoked link, not a deletion acknowledgement: its history and request_counts stay readable, and it keeps appearing under status=revoked.
Revoking is idempotent. Revoking a link that is already revoked answers 200 with the link exactly as it is — the same revoked_at, no second event.
There is no undo. To work with the same supplier again, invite them afresh with POST /v1/suppliers; the revoke has released their address. The new link is a new sup_ id with its own history.
Emits supplier.revoked on your feed and, when the link had been accepted, on the supplier team's. Each cancelled request emits data_request.cancelled on your feed, and on the supplier's only when a supplier team already held the request.
Request
curl -X DELETE https://api.synexcloud.com/v1/suppliers/sup_7c2e9a4f1b8d3506 \
-H "Authorization: Bearer $SYNEX_API_KEY"
Response
{
"object": "supplier",
"id": "sup_7c2e9a4f1b8d3506",
"name": "Nordvolt Cells",
"status": "revoked",
"role": "customer",
"customer_team": { "id": 4821, "name": "Kestrel Energy" },
"supplier_team": { "id": 5107, "name": "Nordvolt Cells AB" },
"invited_email": "quality@nordvolt.example",
"invite_status": "accepted",
"linked_at": "2026-09-02T09:41:17Z",
"revoked_at": "2026-09-19T10:22:08Z",
"created_at": "2026-09-01T14:10:43Z",
"request_counts": {
"open": 0,
"submitted": 0,
"changes_requested": 0,
"applied": 7
}
}
Events
| Type | Fires when | Feed |
|---|---|---|
supplier.invited | POST /v1/suppliers opens a pending link and mails the invitation; so does POST /v1/data_requests with a recipient; and the first invitation a resend issues to a link that never had one. | Customer |
supplier.linked | The recipient accepts the invitation in the Synex application. | Both |
supplier.revoked | A link is revoked. A second revoke emits nothing. | Customer; supplier too when the link had been accepted |
A rename and an ordinary resend emit nothing. The snapshot on each feed is the link as that side reads it — role: customer on yours, role: supplier on theirs — and the actor is the real person who acted, on both feeds. See two teams, one change.
Errors
Beyond the standard error envelope, this surface has these codes:
| Code | Status | Meaning |
|---|---|---|
supplier_already_linked | 409 | The address already holds a pending or linked link with your team; or a resend on a link that has already been accepted. A revoked link releases the address. |
link_revoked | 409 | Rename or resend on a revoked link. |
team_suspended | 403 | Your team is suspended: at the create tier, POST /v1/suppliers is refused; at the write tier, every write here is. |
ability_not_scoped_to_resource | 403 | The key's suppliers:* grant carries an ontology scope. Suppliers are team-wide; grant the ability outright. |
parameter_invalid | 400 | A malformed email or name; an unknown role or status on the list; on a first-time resend, a stored address that no longer passes validation. |
resource_missing | 404 | A link your team is not party to — or, for a write, one your team is not the customer of — whether or not it exists. |