API

Affichez vos albums publics sur votre propre site, ou gérez vos albums depuis un autre outil : création, envoi de photos, prix, destinataires, publication. Chaque clé n'a que les permissions choisies à sa création. Les fichiers originaux ne sont jamais exposés.

Authentification

Créez une clé dans votre espace (Clés API), puis envoyez-la dans l'en-tête Authorization. Limite par défaut : 60 requêtes par minute et par clé. Les corps de requête sont en JSON (Content-Type: application/json), les réponses aussi : { "data": … }, avec total pour les listes.

curl "https://obturo.app/api/v1/photos?tag=plage&limit=10" \
  -H "Authorization: Bearer pk_live_…"

Permissions

public:readLecture publique. Albums publics publiés, leurs photos (aperçus) et leurs prix. Pour afficher vos albums sur un autre site.
albums:readLecture de tous vos albums. Aussi les brouillons, albums sur lien et privés (jamais les fichiers originaux ni le lien secret).
albums:writeGestion des albums. Créer, modifier, supprimer, publier et dépublier. Inclut la lecture de tous vos albums et leurs liens.
photos:writeGestion des photos. Envoyer des photos, changer leur accès (offerte, aperçu payant, exclusive), les supprimer.
prices:writeGestion des prix. Ajouter et retirer les formats de prix d'un album.
recipients:writeGestion des destinataires. Lister et retirer les destinataires et invités. En ajouter donne accès à l'album (lien envoyé par email, invitation) : il faut aussi « Gestion des albums ».

Une route appelée sans la permission nécessaire répond 403 missing_scope. Les écritures appliquent exactement les règles de votre espace photographe et ne concernent que vos propres albums. Les clés créées avant l'arrivée des permissions ont public:read.

Depuis un navigateur (CORS)

Lecture : acceptée depuis tous les domaines si la liste des domaines autorisés de la clé est vide, sinon seulement depuis ceux de la liste. Écriture (POST, PATCH, DELETE) : acceptée seulement depuis un domaine explicitement listé ; sans liste, les modifications se font depuis un serveur (sans en-tête Origin). Une clé avec des permissions de gestion ne doit jamais apparaître dans le code d'une page publique.

Albums

GET/api/v1/albums?tag=&visibility=&status=&limit=&offset=Permission : public:read ou albums:read

Albums paginés (20 par défaut, 100 max). public:read : albums publics publiés. albums:read : tous vos albums, filtrables par visibility (public, unlisted, private) et status (draft, published, expired).

curl "https://obturo.app/api/v1/albums?status=draft&limit=10" \
  -H "Authorization: Bearer $OBTURO_KEY"
GET/api/v1/albums/:idPermission : public:read ou albums:read

Détail d'un album avec ses formats de prix. En albums:read : nombre de photos et ce qui empêche la publication (publish_blockers). Pour un album déjà publié, la liste montre ce qui empêche la vente (ex. photos payantes sans format de prix).

curl https://obturo.app/api/v1/albums/ALBUM_ID \
  -H "Authorization: Bearer $OBTURO_KEY"
POST/api/v1/albumsPermission : albums:write

Crée un album en brouillon. Champs : title (obligatoire, 120 caractères max), visibility (obligatoire : public, unlisted ou private), description (2000 max), tags (10 hashtags max), expires_at (AAAA-MM-JJ ou date ISO 8601, dans le futur). Réponse 201.

curl -X POST https://obturo.app/api/v1/albums \
  -H "Authorization: Bearer $OBTURO_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"Mariage Julie et Tom","visibility":"private","tags":["mariage"],"expires_at":"2027-06-30"}'
PATCH/api/v1/albums/:idPermission : albums:write

Modifie les champs envoyés : title, description, tags (remplace la liste), expires_at (null pour l'enlever). Le type d'album (visibility) ne change pas après la création.

curl -X PATCH https://obturo.app/api/v1/albums/ALBUM_ID \
  -H "Authorization: Bearer $OBTURO_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"Mariage Julie & Tom","description":null,"tags":["mariage","ete"]}'
DELETE/api/v1/albums/:idPermission : albums:write

Supprime l'album, ses photos et leurs fichiers. Refusé (409 has_orders) si une photo a été vendue.

curl -X DELETE https://obturo.app/api/v1/albums/ALBUM_ID \
  -H "Authorization: Bearer $OBTURO_KEY"
POST/api/v1/albums/:id/publishPermission : albums:write

Publie l'album, avec les mêmes règles que votre espace : compte actif, au moins une photo, et si des photos sont payantes, au moins un format de prix (sinon 409 publish_blocked, détail dans details). Le compte Stripe n'est pas exigé : sans lui, les photos payantes s'affichent sans bouton d'achat. Les destinataires pas encore prévenus reçoivent l'email ; emails_sent vaut null si l'envoi n'est pas configuré. Ces règles ne bloquent que la publication : ensuite, une photo payante sans format de prix actif ou sans compte Stripe qui encaisse reste affichée, sans bouton d'achat.

curl -X POST https://obturo.app/api/v1/albums/ALBUM_ID/publish \
  -H "Authorization: Bearer $OBTURO_KEY"
POST/api/v1/albums/:id/unpublishPermission : albums:write

Repasse l'album en brouillon : il n'est plus visible, ses liens ne fonctionnent plus jusqu'à la prochaine publication.

curl -X POST https://obturo.app/api/v1/albums/ALBUM_ID/unpublish \
  -H "Authorization: Bearer $OBTURO_KEY"

Photos

Envoi en trois étapes : demander une URL d'envoi par fichier, envoyer le fichier sur cette URL (PUT, valable 15 minutes), puis finaliser. La taille déclarée fait partie de la signature : le stockage refuse un fichier d'une autre taille. La finalisation génère l'aperçu (filigrané sauf photo offerte) et la miniature floutée, sans métadonnées EXIF/GPS, puis range l'original hors de portée de l'URL d'envoi. Un envoi jamais finalisé peut être supprimé au bout de 24 heures. Formats acceptés : JPEG, PNG, WebP, TIFF, 80 Mo maximum. L'envoi sur l'URL se fait depuis un serveur : le stockage n'accepte pas les envois directs depuis le navigateur d'un autre site.

POST/api/v1/albums/:id/uploadsPermission : photos:write

Demande les URL d'envoi (1 à 50 fichiers : name, type, size = taille exacte en octets). Chaque fichier reçoit key, upload_url, method, headers (Content-Type et Content-Length à envoyer) et expires_at, ou un message error s'il est refusé.

curl -X POST https://obturo.app/api/v1/albums/ALBUM_ID/uploads \
  -H "Authorization: Bearer $OBTURO_KEY" \
  -H "Content-Type: application/json" \
  -d '{"files":[{"name":"IMG_0001.jpg","type":"image/jpeg","size":5242880}]}'

# puis, pour chaque fichier (même Content-Type et même taille que déclarés) :
curl -X PUT "UPLOAD_URL" -H "Content-Type: image/jpeg" --data-binary @IMG_0001.jpg
POST/api/v1/albums/:id/photosPermission : photos:write

Finalise un envoi : key (reçue à l'étape 1), access (free, preview ou paywall ; preview par défaut). Réponse 201 avec la photo. 422 upload_missing si le fichier n'a pas été envoyé, 409 already_finalized si la clé est déjà enregistrée.

curl -X POST https://obturo.app/api/v1/albums/ALBUM_ID/photos \
  -H "Authorization: Bearer $OBTURO_KEY" \
  -H "Content-Type: application/json" \
  -d '{"key":"KEY","access":"preview"}'
GET/api/v1/albums/:id/photos?limit=&offset=Permission : public:read ou albums:read

Photos d'un album, dans l'ordre de l'album.

curl "https://obturo.app/api/v1/albums/ALBUM_ID/photos?limit=50" \
  -H "Authorization: Bearer $OBTURO_KEY"
GET/api/v1/photos?tag=&limit=&offset=Permission : public:read ou albums:read

Vos photos, les plus récentes d'abord, filtrables par hashtag d'album.

curl "https://obturo.app/api/v1/photos?tag=plage&limit=10" \
  -H "Authorization: Bearer $OBTURO_KEY"
GET/api/v1/photos/:idPermission : public:read ou albums:read

Détail d'une photo.

curl https://obturo.app/api/v1/photos/PHOTO_ID \
  -H "Authorization: Bearer $OBTURO_KEY"
PATCH/api/v1/photos/:idPermission : photos:write

Change l'accès : free (offerte, aperçu sans filigrane), preview (aperçu payant filigrané) ou paywall (exclusive, floutée). L'aperçu est régénéré quand la photo devient ou cesse d'être offerte. Comme dans votre espace, aucun format de prix n'est exigé ici (seulement à la publication) : sans format actif, une photo payante s'affiche sans bouton d'achat.

curl -X PATCH https://obturo.app/api/v1/photos/PHOTO_ID \
  -H "Authorization: Bearer $OBTURO_KEY" \
  -H "Content-Type: application/json" \
  -d '{"access":"free"}'
DELETE/api/v1/photos/:idPermission : photos:write

Supprime la photo et ses fichiers. Refusé (409 has_orders) si elle a été vendue.

curl -X DELETE https://obturo.app/api/v1/photos/PHOTO_ID \
  -H "Authorization: Bearer $OBTURO_KEY"

Prix

Formats proposés à l'achat pour les photos payantes et exclusives d'un album : un nom et un prix, fichier original livré.

GET/api/v1/albums/:id/pricesPermission : public:read, albums:read ou prices:write

Formats actifs de l'album.

curl https://obturo.app/api/v1/albums/ALBUM_ID/prices \
  -H "Authorization: Bearer $OBTURO_KEY"
GET/api/v1/albums/:id/prices/:priceIdPermission : public:read, albums:read ou prices:write

Un format actif (adresse renvoyée dans l'en-tête Location après sa création).

curl https://obturo.app/api/v1/albums/ALBUM_ID/prices/PRICE_ID \
  -H "Authorization: Bearer $OBTURO_KEY"
POST/api/v1/albums/:id/pricesPermission : prices:write

Ajoute un format : name (40 caractères max), price_cents (entier en centimes, de 0 à 1 000 000). Réponse 201.

curl -X POST https://obturo.app/api/v1/albums/ALBUM_ID/prices \
  -H "Authorization: Bearer $OBTURO_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Fichier HD","price_cents":1500}'
DELETE/api/v1/albums/:id/prices/:priceIdPermission : prices:write

Retire un format. S'il figure dans une commande, il est désactivé (deactivated: true) plutôt que supprimé ; les commandes gardent de toute façon son nom et son prix. Retirer le dernier format d'un album publié ne le dépublie pas : ses photos payantes s'affichent alors sans bouton d'achat.

curl -X DELETE https://obturo.app/api/v1/albums/ALBUM_ID/prices/PRICE_ID \
  -H "Authorization: Bearer $OBTURO_KEY"

Destinataires

Albums sur lien : les destinataires reçoivent le lien et le QR code par email. Albums privés : seuls les invités peuvent ouvrir l'album, après connexion avec leur email. Les albums publics n'ont pas de destinataires. Ajouter une adresse lui ouvre donc l'album : il faut recipients:write et albums:write, la permission qui donne aussi le lien secret. Une adresse désinscrite des emails d'un album le reste, même retirée puis ajoutée de nouveau.

GET/api/v1/albums/:id/recipientsPermission : recipients:write

Destinataires de l'album, avec leur état : pending (pas encore prévenu), notified (lien envoyé), unsubscribed (désinscrit des emails).

curl https://obturo.app/api/v1/albums/ALBUM_ID/recipients \
  -H "Authorization: Bearer $OBTURO_KEY"
POST/api/v1/albums/:id/recipientsPermission : recipients:write et albums:write

Ajoute une adresse (201). Album publié : l'email part tout de suite, sinon à la publication. Le champ notification de la réponse vaut sent, failed (envoi refusé), not_configured (emails non configurés), on_publish, ou unsubscribed (adresse désinscrite des emails de cet album : rien n'est envoyé). 409 duplicate si l'adresse est déjà enregistrée. 200 ajouts réussis par heure maximum, espace photographe compris (429 rate_limited).

curl -X POST https://obturo.app/api/v1/albums/ALBUM_ID/recipients \
  -H "Authorization: Bearer $OBTURO_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"invite@exemple.fr"}'
DELETE/api/v1/albums/:id/recipients/:recipientIdPermission : recipients:write

Retire le destinataire (album privé : il perd l'accès). Sa désinscription éventuelle est conservée.

curl -X DELETE https://obturo.app/api/v1/albums/ALBUM_ID/recipients/RECIPIENT_ID \
  -H "Authorization: Bearer $OBTURO_KEY"

Hashtags

GET/api/v1/hashtagsPermission : public:read ou albums:read

Hashtags de vos albums lisibles par la clé, avec le nombre d'albums.

curl https://obturo.app/api/v1/hashtags \
  -H "Authorization: Bearer $OBTURO_KEY"

Objets

Photo :

{
  "id": "…",
  "album_id": "…",
  "width": 1600, "height": 2000,
  "access": "preview",          // free | preview | paywall
  "preview_url": "https://…",   // null pour une photo exclusive
  "thumb_url": "https://…",
  "watermarked": true,
  "buy_url": "https://obturo.app/albums/…#photo-…", // ou null, voir ci-dessous
  "created_at": "…"
}

buy_url suit le lien de l'album : /albums/… (public), /album/… (privé, réservé aux invités connectés) ou /a/… (sur lien). Il vaut null pour une photo d'un album sur lien lue par une clé sans albums:write, qui n'a pas le lien secret.

Album. Clé public:read seule : id, slug, title, description, published_at, expires_at, url, tags (et formats dans le détail). Avec albums:read : tous les champs ci-dessous.

{
  "id": "…", "slug": "…", "title": "…", "description": null,
  "visibility": "unlisted",     // public | unlisted | private
  "status": "draft",            // draft | published | expired
  "published_at": null, "expires_at": null,
  "url": "https://obturo.app/a/…",   // album sur lien : fourni seulement aux clés albums:write
  "tags": ["plage"],
  "cover_photo_id": "…", "created_at": "…", "updated_at": "…",
  "formats": [{ "id": "…", "name": "Fichier HD", "price_cents": 1500, "currency": "EUR", … }],
  "photo_count": 12,            // détail d'un album
  "publish_blockers": []        // ce qui empêche la publication (album publié : la vente)
}

Erreurs

Toute erreur répond en JSON avec un message en français (error), un code stable (code) et, selon le cas, le champ en cause (field) ou des précisions (details) :

HTTP/1.1 409 Conflict
{
  "error": "Publication impossible. Il reste à ajouter un format de prix (3 photos sont payantes), ou passer les photos en « Offerte ».",
  "code": "publish_blocked",
  "details": [{ "code": "no_price", "message": "ajouter un format de prix (3 photos sont payantes), ou passer les photos en « Offerte »" }]
}
HTTPcodeCas
400bad_request, unknown_fieldJSON invalide, champ inconnu, paramètre de recherche invalide, aucun champ à modifier
401unauthorizedClé absente, invalide ou révoquée
403missing_scopeLa clé n'a pas la permission demandée par la route (details liste les permissions attendues)
403origin_not_allowedAppel depuis un navigateur d'un domaine non autorisé (voir ci-dessus)
403account_suspendedCompte photographe suspendu : aucune modification ni lecture de ses albums (une clé public:read reçoit des listes vides)
404not_foundRessource inexistante, d'un autre photographe, ou non lisible avec cette clé
405Méthode non prise en charge par la route
409publish_blocked, duplicate, has_orders, already_finalized, public_albumConflit avec l'état de la ressource
413payload_too_largeCorps de requête de plus de 64 Ko
415unsupported_media_typeCorps envoyé sans Content-Type: application/json
422invalid_field, upload_missing, file_too_large, unreadable_imageValeur refusée (field indique le champ en cause)
429rate_limitedLimite de requêtes dépassée (en-tête Retry-After)
500server_errorErreur interne : réessayez