Skip to content

Webhook endpoint — import an offer

The webhook endpoint creates a single offer as a draft in your Flino organisation. It is idempotent over the combination external_source + external_id, so the same call sent multiple times does not produce a duplicate.

POST https://<your-project>.supabase.co/functions/v1/zapier-offer-import

You find the <your-project> part in Settings → Integrations → API tokens, in the “Zapier webhook URL” section.

This Supabase Functions URL is the canonical and permanent endpoint address for the webhook. A separate API subdomain (e.g. api.flino.io) is not planned at the moment — use the URL shown here as the stable address.

Headers:

x-flino-token: <your-api-token>
Content-Type: application/json

Generate <your-api-token> as described in API tokens. Per token the scope is fixed to offer_import:write.

Only POST is accepted. Other methods return 405 Method not allowed.

The body must be a JSON object. Arrays or strings are rejected with 400 Request body must be a JSON object.

FieldTypeDescription
titlestring, non-emptyOffer title, e.g. “Website redesign Mustermann GmbH”
external_idstring, non-emptyUnique ID from your system — the key for idempotency
external_sourcestring, non-emptySource system, e.g. zoho, pipedrive, zapier, myform — stored internally in lowercase
FieldTypeDescription / default
amountnumber | nullOffer amount in main currency unit (e.g. 1500.00)
currencystringEUR (default), USD, or CHF. Other values are rejected with 400.
contact_namestring | nullFull name. Flino splits it automatically: the last word becomes the last name, the rest the first name. Only used together with contact_email.
contact_emailstring | nullMust be in valid email format. Surrounding spaces are removed, it is stored in lowercase. Without contact_email the offer gets no contact.
company_namestring | nullCompany name; Flino creates a new company or reuses an existing one
offer_numberstring | nullYour own offer number
valid_untilstring | nullISO date, e.g. 2026-12-31. Invalid dates are ignored (value stays null).
  • Salutation and language are not transmitted in the Phase 1 webhook — they are added on the contact manually or in the first send dialog.
  • Address is not transmitted in the Phase 1 webhook.

For each call, Flino checks whether an offer with the same external_source + external_id already exists in your organisation.

  • If it exists: no new offer, response 200 with created: false, the existing offer_id, and its current status. Such a call does not use a place in your offer quota.
  • If it does not exist: a new offer is created, response 200 with created: true.

Useful when your source system retries — you don’t end up with duplicates.

An offer newly created via the webhook counts against your plan’s offer quota just like an offer created in the app (see Billing). Flino reserves the place before the offer is created. If you delete the draft without sending it, the place becomes free again.

If the quota is used up, Flino creates nothing and responds with 403:

{
"success": false,
"error": "OFFER_QUOTA_EXHAUSTED",
"code": "OFFER_QUOTA_EXHAUSTED",
"message": "Dein Angebotskontingent für diesen Monat ist aufgebraucht. Es erneuert sich mit dem nächsten Abrechnungszeitraum. Mehr Kontingent gibt es mit einem höheren Plan."
}

The message is always German. The response also contains a balances field with the current state. Send the same call again after the quota renews or after switching to a larger plan; thanks to idempotency this does not create a duplicate.

Status 200 OK:

{
"success": true,
"created": true,
"offer_id": "8d3f7e09-1e0e-44e5-b6c3-49e3a7c2a131",
"status": "draft"
}

On idempotency hit:

{
"success": true,
"created": false,
"offer_id": "8d3f7e09-1e0e-44e5-b6c3-49e3a7c2a131",
"status": "draft"
}
POST /functions/v1/zapier-offer-import HTTP/1.1
Host: <your-project>.supabase.co
Content-Type: application/json
x-flino-token: 9a3f...c1e2
{
"title": "Website redesign Mustermann GmbH",
"amount": 4900,
"currency": "EUR",
"contact_name": "Anna Mustermann",
"contact_email": "[email protected]",
"company_name": "Mustermann GmbH",
"offer_number": "ANG-2026-0042",
"valid_until": "2026-06-30",
"external_id": "deal_19238",
"external_source": "zoho"
}
{
"success": true,
"created": true,
"offer_id": "8d3f7e09-1e0e-44e5-b6c3-49e3a7c2a131",
"status": "draft"
}
HTTP statusBody errorMeaning
400Invalid JSON bodyBody is not valid JSON
400Request body must be a JSON objectBody is an array or a string
400Field 'title' is required and must be a non-empty stringtitle missing or empty
400Field 'external_id' is required and must be a non-empty stringexternal_id missing or empty
400Field 'external_source' is required and must be a non-empty stringexternal_source missing or empty
400Field 'currency' must be one of: EUR, USD, CHFunsupported currency
400Field 'amount' must be a numberamount sent as string or boolean
400Field 'contact_email' is not a valid email addressinvalid email format
401Missing x-flino-token headerHeader missing
401Invalid or unknown tokenToken not found
401Token has been revokedToken has been revoked
403Token lacks required scope: offer_import:writeScope mismatch
403OFFER_QUOTA_EXHAUSTEDOffer quota used up, see above
405Method not allowedOnly POST allowed
429Too many requestsLimit exceeded, see Rate limits
500Organisation owner not foundOrg configuration issue — contact support
500Failed to create offer: ...Database error
503Offer quota reservation failedThe quota could not be checked just now, nothing was created; send again later

All error responses follow this schema:

{
"success": false,
"error": "<plain-text description>"
}

Some errors also carry a code field for automated handling: RATE_LIMITED (429), OFFER_QUOTA_EXHAUSTED (403), and OFFER_QUOTA_ERROR (503).

  1. Validate the token — SHA-256 lookup, revoke check, scope check, then the per-token limit.
  2. Check the body — required fields, currency, amount, email format.
  3. Check idempotency — does the offer already exist? If so, return the existing one.
  4. Create or reuse the company — if company_name is set, Flino looks for an existing company with the same name in your org; upper/lower case and legal-form suffixes such as “GmbH” or “AG” are ignored. If none, a new one is created.
  5. Create or reuse the contact — only if contact_email is set, with the email address as the key. For an existing contact Flino only fills empty fields (such as the name) and never overwrites anything. Without contact_email the offer has no contact.
  6. Reserve quota — one place in the offer quota, see above.
  7. Create the offer as draft — with send status not_sent, source zapier, your external_id and external_source for the next idempotency check.
  • The offer appears under Offers with the note Zapier import and in the Pipeline with status New.
  • You open it manually, fill in missing data (contact, salutation, email address, description), and send it from Flino as usual.
  • No sequence is started, no email is sent — the webhook only creates the offer.

Each token may send 60 requests per minute. Beyond that, the endpoint responds with 429:

{
"success": false,
"error": "Too many requests",
"code": "RATE_LIMITED"
}

Wait until the next minute and send the call again. Requests with unknown tokens also share a common limit; once it is exceeded, they get 429 instead of 401 as well.

Retries are harmless: idempotency via external_source + external_id makes sure a repeated call does not create a duplicate offer.

If you plan a bulk import (several hundred offers at once), spread the calls out below the limit and check beforehand whether your offer quota covers it.