Skip to main content

Update invitations

An IBAN that changes, a contact who leaves, a head office that moves: your directory records go stale without anything telling you. An update invitation sends your customer or supplier a signed link to a public form where they correct their own banking details, legal identifiers, addresses, and contacts. On submission, your directory record is updated, with no re-entry and no spreadsheet exchanged by email. Each invitation carries a state, from pending to submitted, that you track through the API.

What Scribee does for you​

  • Generates a signed, single-use link valid for 14 days. The invitation record keeps only a fingerprint of it, never the cleartext token - the token does, however, pass through the email processing queue, which is persisted in the database, until the message is sent.
  • Sends the invitation email in the recipient's language (fr or en), with your custom message if provided.
  • Restricts the form to the data the party can legitimately correct; an address or contact id belonging to another party is ignored.
  • Cuts off the link as soon as it is submitted, revoked, or expired: it then responds 410 and exposes nothing further.
  • Logs each submission against the invitation's recipient email address. The public link authenticates nobody: if it is forwarded, the audit trail still carries the invited address, not that of whoever actually filled the form. Do not treat that trail as proof of identity.

The journey of an invitation​

StateMeaning
pendingInvitation created, email not yet sent. On POST .../update_invitations, this state is transient: creation requests the send in the same call. On bulk sending it is durable: a row whose send failed stays in pending permanently.
sentThe invitation email has been queued for sending (sent_at).
openedThe recipient has opened the public form (opened_at).
submittedThe recipient has submitted their information (submitted_at); the record is updated.
expiredInvitation overdue without submission. A daily sweep moves pending, sent, or opened invitations whose expires_at has passed into this state.
revokedInvitation revoked by you (revoked_at). Possible from pending, sent, or opened; an invitation already submitted, expired, or revoked can no longer be revoked (422).

expires_at is authoritative, not state. expires_at is the creation date plus 14 days, and that date is what cuts the link off: past that point, the form responds 410, immediately. The state follows with a lag: the sweep that moves the invitation to expired only runs once a day, so an overdue invitation can stay in sent for up to 24 hours. To know whether an invitation is still usable, compare expires_at to the current time rather than waiting for the expired state.

Timestamps are ISO 8601 with the Europe/Paris offset (+02:00 in summer, +01:00 in winter). The API never emits a Z suffix.

Step 1: send an invitation​

This call sends a real email to the recipient_email address - there is no sandbox, and an email that has gone out cannot be recalled. To rehearse without a real party, create a test record (Customers and suppliers) and enter your own address as the recipient: you receive the email and walk through the form yourself. Revocation (step 3) disables the link, but does not recall the email.

curl -X POST https://app.scribee.tech/api/v1/workspaces/YOUR_WORKSPACE_ID/parties/YOUR_PARTY_ID/update_invitations \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"party_update_invitation": {
"recipient_email": "comptabilite@fournisseur.example",
"custom_message": "Merci de confirmer votre IBAN avant le 15 du mois.",
"communication_language": "fr"
}
}'

Response 201, abridged to the fields relevant here:

{
"data": {
"id": 8,
"party_id": 42,
"recipient_email": "comptabilite@fournisseur.example",
"state": "sent",
"custom_message": "Merci de confirmer votre IBAN avant le 15 du mois.",
"communication_language": "fr",
"sent_at": "2026-07-31T09:15:00+02:00",
"expires_at": "2026-08-14T09:15:00+02:00"
}
}

recipient_email is required. custom_message is optional and appears in the body of the email. communication_language (fr or en) is optional; by default, Scribee uses the record's communication language, then fr - the response always returns the resolved language on communication_language, on this call as on the other three (list GET, single GET, DELETE). The read write scope is required. Each call creates a distinct invitation and link, even when an open invitation already exists for the same party.

The 201 and the sent state confirm that the email was queued for sending, not that it was delivered: the send is processed in the background and its outcome is not exposed. If you omit the party_update_invitation object that wraps the request body, the response is a 400 in the usual error envelope, with error set to bad_request and the message "Le corps de la requête est manquant ou mal formé".

Step 2: track your invitations​

GET .../parties/{party_id}/update_invitations returns all of the party's invitations, without pagination: the response contains data with no meta object. The read scope is enough.

curl https://app.scribee.tech/api/v1/workspaces/YOUR_WORKSPACE_ID/parties/YOUR_PARTY_ID/update_invitations \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
{
"data": [
{
"id": 8,
"party_id": 42,
"state": "submitted",
"submitted_at": "2026-08-02T14:03:00+02:00",
"expires_at": "2026-08-14T09:15:00+02:00"
}
]
}

A single invitation is read at GET /api/v1/party_update_invitations/{id} - note the path with no workspace_id: the invitation is referenced directly by its id (Your first call). This is the call to poll to observe the transition to submitted: no webhook is emitted for this event, webhooks cover invoicing events (see Webhooks).

Step 3: revoke an invitation​

This call disables the link immediately and permanently: the form then responds 410 to the recipient. It does not delete the invitation, which remains readable through GET with the state revoked. The call fails on an invitation already submitted, expired, or revoked: those three states are terminal and the call responds 422 without changing anything (see below).

curl -X DELETE https://app.scribee.tech/api/v1/party_update_invitations/8 \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200 with the updated invitation:

{
"data": {
"id": 8,
"party_id": 42,
"state": "revoked",
"revoked_at": "2026-08-01T10:00:00+02:00"
}
}

The read write scope is enough.

Step 4: invite in bulk​

This call sends a real email to each eligible party in the batch, to their primary contact's address - up to 500 identifiers per call. Processing is asynchronous: the 202 response confirms that the request was accepted, not that it was delivered. The read write scope is required.

curl -X POST https://app.scribee.tech/api/v1/workspaces/YOUR_WORKSPACE_ID/parties/update_invitations/batch \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"batch": {
"party_ids": [42, 43, 57],
"custom_message": "Merci de vérifier vos informations avant le 1er septembre."
}
}'

Response 202:

{
"enqueued_count": 2,
"skipped_count": 1,
"message": "Envoi d'invitations lancé pour 2 tiers."
}

Several exclusion cases feed skipped_count, notably: the party has no email address on its primary contact, or an open invitation (pending, sent, or opened, not expired) already exists for it. An identifier that does not exist in the workspace appears in neither counter: compare the sum of the two to the size of your batch. The response does not detail which parties were skipped; the per-party state is read with the GET from step 2.

enqueued_count counts the parties accepted for processing, not the emails that went out. A failure on a given party is silent for an OAuth caller - no error, no notification - and leaves its invitation in pending. Always reconcile the batch against the GET from step 2.

party_ids must be present and must be a JSON array. Sent as a scalar, or omitted while batch carries another usable field (custom_message), it is dropped before validation and the batch is treated as empty: the call responds 422 (see below). When batch contains no other usable field, the response is a 400 in the usual error envelope, with error set to bad_request and the message "Le corps de la requête est manquant ou mal formé". The same applies if you omit the batch object itself.

What your party sees​

The email contains a button to the public form hosted by Scribee - a web page, distinct from the /api/v1/** API. Opening the form moves the invitation to opened; submitting it updates the record and moves the invitation to submitted. PUT /api/v1/p/parties/{token} documents the same mechanism on the partner API side: an endpoint with no OAuth authentication, where the URL's signed token is the only authentication factor. The token is never communicated by the API - it exists only in the link sent to the party - so your integration never calls it itself.

The form lets the party correct: iban, bic, vat_identifier, legal_registration_id, legal_registration_scheme_id, their addresses (party_addresses_attributes: create, update, delete) and their contacts (party_contacts_attributes: name, email, phone). The record's name and the rest of your data do not pass through this channel. On submission, the recipient sees "Vos informations ont été mises à jour. Merci." and the link becomes unusable.

What happens next​

  • The party's record is updated at the moment of submission, in the same transaction as the transition to submitted: when you read state: "submitted", the new data is already on the record (Customers and suppliers).
  • Nothing is sent to the PPF (Portail Public de Facturation) nor to the Peppol network: an invitation only touches your directory, and invoices already issued are not modified.
  • After 14 days without submission, the link responds 410 immediately; the invitation's state moves to expired at the next daily sweep, up to 24 hours later. Send a new invitation then: each send creates a distinct link.

Errors and edge cases​

422: missing recipient address​

recipient_email empty at creation:

{
"error": "unprocessable_entity",
"code": "validation_failed",
"message": "La validation a échoué",
"details": {
"base": ["Une adresse e-mail destinataire est requise pour créer une invitation."]
}
}

Provide a valid address and replay the call.

422: invalid batch​

On the bulk-send endpoint, the batch is refused as soon as it designates no party: party_ids empty, absent, or sent as anything other than a JSON array - all three cases give the same response.

{
"error": "unprocessable_entity",
"code": "validation_failed",
"message": "La validation a échoué",
"details": {
"base": ["Au moins un identifiant de tiers est requis."]
}
}

More than 500 identifiers returns "Trop de tiers sélectionnés. Maximum 500 par lot.", in the same details.base format. Send party_ids as a non-empty JSON array, split the batch if needed, and replay.

422: non-revocable invitation​

On DELETE /api/v1/party_update_invitations/{id}, an invitation already submitted, expired, or revoked responds 422 instead of moving to revoked:

{
"error": "unprocessable_entity",
"code": "validation_failed",
"message": "La validation a échoué",
"details": {
"base": ["Cette invitation ne peut pas être révoquée dans son état actuel."]
}
}

These three states are terminal: once reached, the invitation never becomes actionable again, including for revocation. The state and the rest of the record stay unchanged. An invitation that is overdue but not yet swept stays in sent and can still be revoked, which changes nothing for the recipient: their link already responds 410.

404 Not Found​

The party or invitation does not exist, or the party is a customer whose company is not enabled for sales:

{
"error": "not_found",
"message": "La ressource demandée est introuvable"
}

On the two flat routes - GET and DELETE /api/v1/party_update_invitations/{id} - the invitation is looked up only in the workspaces linked to your application whose IP allowlist permits your source address. Anything outside that perimeter is indistinguishably not found: an IP address outside the allowlist therefore produces 404 here, where the three workspace-scoped routes produce 403.

Check the identifier and the workspace attachment; if a known identifier suddenly responds 404, check your source IP address before the identifier (Your first call).

403: workspace or IP address refused​

On the three workspace-scoped routes - list, create, and bulk send - the workspace is resolved before the action, and two refusals return 403. Your application is not allowed to reach that workspace:

{
"error": "forbidden",
"message": "L'application n'a pas accès à cet espace de travail"
}

The workspace has an IP allowlist that does not cover your source address:

{
"error": "forbidden",
"message": "Cette adresse IP n'est pas autorisée pour cet espace de travail"
}

A workspace_id matching no workspace receives the first refusal above, word for word: nothing tells a workspace that does not exist apart from a workspace your application is not allowed to reach. Both messages point to a workspace attachment or a source IP problem, not a scope problem: GET /api/v1/workspaces lists the workspaces your application can reach, and workspace and IP access are configured by Scribee - contact your Scribee representative. On GET and DELETE /api/v1/party_update_invitations/{id}, which carry no workspace_id, neither case is distinguishable: both respond 404.

403 Forbidden: insufficient scope​

The read write scope creates an invitation, sends it in bulk, and revokes it; on revocation, read destroy works just as well. Reading requires read:

{
"error": "forbidden",
"message": "Vous n'êtes pas autorisé à effectuer cette action"
}

Request a new token with the required scopes (Authentication).

A revoked, expired, or already-used link responds to the recipient:

{
"error": "invalid_or_expired_token",
"message": "Ce lien de mise à jour est invalide, expiré ou déjà utilisé."
}

invitation_not_actionable does not cover every no-longer-submittable invitation: one already submitted, revoked, or expired is rejected earlier, at token resolution, with invalid_or_expired_token. invitation_not_actionable, and its message "Cette invitation ne peut pas être soumise dans son état actuel.", only covers the remaining states that reach submission. Your API is not affected; keep these messages in mind when supporting your parties.